summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Evans <grknight@gentoo.org>2018-12-20 09:26:25 -0500
committerBrian Evans <grknight@gentoo.org>2018-12-20 09:26:25 -0500
commit0e06ce158ac74849133e076fdc7320b4f1058888 (patch)
treed98171b4bd45d30ab8c504c552af996c4e4e5f9a /CommentStreams
parentGentooPackages: Drop global/local descriptors (diff)
downloadextensions-0e06ce158ac74849133e076fdc7320b4f1058888.tar.gz
extensions-0e06ce158ac74849133e076fdc7320b4f1058888.tar.bz2
extensions-0e06ce158ac74849133e076fdc7320b4f1058888.zip
Add CommentStreams
Signed-off-by: Brian Evans <grknight@gentoo.org>
Diffstat (limited to 'CommentStreams')
-rw-r--r--CommentStreams/.gitignore3
-rw-r--r--CommentStreams/.gitreview6
-rw-r--r--CommentStreams/CODE_OF_CONDUCT.md1
-rw-r--r--CommentStreams/COPYING21
-rw-r--r--CommentStreams/Gruntfile.js21
-rw-r--r--CommentStreams/extension.json160
-rw-r--r--CommentStreams/gitinfo.json1
-rw-r--r--CommentStreams/i18n/en.json149
-rw-r--r--CommentStreams/i18n/qqq.json149
-rw-r--r--CommentStreams/images/CREDITS5
-rw-r--r--CommentStreams/images/cancel.pngbin0 -> 2004 bytes
-rw-r--r--CommentStreams/images/collapse.pngbin0 -> 1319 bytes
-rw-r--r--CommentStreams/images/comment_add.pngbin0 -> 1704 bytes
-rw-r--r--CommentStreams/images/comment_delete.pngbin0 -> 1699 bytes
-rw-r--r--CommentStreams/images/comment_edit.pngbin0 -> 1653 bytes
-rw-r--r--CommentStreams/images/comment_moderator_delete.pngbin0 -> 1517 bytes
-rw-r--r--CommentStreams/images/comment_moderator_edit.pngbin0 -> 1488 bytes
-rw-r--r--CommentStreams/images/comment_reply.pngbin0 -> 1844 bytes
-rw-r--r--CommentStreams/images/downvote-disabled.pngbin0 -> 666 bytes
-rw-r--r--CommentStreams/images/downvote-enabled.pngbin0 -> 1181 bytes
-rw-r--r--CommentStreams/images/expand.pngbin0 -> 749 bytes
-rw-r--r--CommentStreams/images/link.pngbin0 -> 1261 bytes
-rw-r--r--CommentStreams/images/notwatching.pngbin0 -> 760 bytes
-rw-r--r--CommentStreams/images/submit.pngbin0 -> 1249 bytes
-rw-r--r--CommentStreams/images/upvote-disabled.pngbin0 -> 637 bytes
-rw-r--r--CommentStreams/images/upvote-enabled.pngbin0 -> 1113 bytes
-rw-r--r--CommentStreams/images/watching.pngbin0 -> 1265 bytes
-rw-r--r--CommentStreams/includes/ApiCSBase.php127
-rw-r--r--CommentStreams/includes/ApiCSDeleteComment.php114
-rw-r--r--CommentStreams/includes/ApiCSEditComment.php129
-rw-r--r--CommentStreams/includes/ApiCSPostComment.php228
-rw-r--r--CommentStreams/includes/ApiCSQueryComment.php42
-rw-r--r--CommentStreams/includes/ApiCSUnwatch.php76
-rw-r--r--CommentStreams/includes/ApiCSVote.php93
-rw-r--r--CommentStreams/includes/ApiCSWatch.php76
-rw-r--r--CommentStreams/includes/Comment.php958
-rw-r--r--CommentStreams/includes/CommentStreams.php282
-rw-r--r--CommentStreams/includes/CommentStreamsAllComments.alias.php30
-rw-r--r--CommentStreams/includes/CommentStreamsAllComments.php177
-rw-r--r--CommentStreams/includes/CommentStreamsHooks.php436
-rw-r--r--CommentStreams/includes/EchoCSPresentationModel.php93
-rw-r--r--CommentStreams/package.json11
-rw-r--r--CommentStreams/resources/CommentStreams.css191
-rw-r--r--CommentStreams/resources/CommentStreams.js1296
-rw-r--r--CommentStreams/resources/CommentStreamsAllComments.css30
-rw-r--r--CommentStreams/resources/CommentStreamsQuerier.js150
-rw-r--r--CommentStreams/resources/spin.min.js2
-rw-r--r--CommentStreams/sql/commentData.sql9
-rw-r--r--CommentStreams/sql/votes.sql7
-rw-r--r--CommentStreams/sql/watch.sql6
-rw-r--r--CommentStreams/version4
51 files changed, 5083 insertions, 0 deletions
diff --git a/CommentStreams/.gitignore b/CommentStreams/.gitignore
new file mode 100644
index 00000000..a7abe44f
--- /dev/null
+++ b/CommentStreams/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+vendor/
+
diff --git a/CommentStreams/.gitreview b/CommentStreams/.gitreview
new file mode 100644
index 00000000..b3f8a452
--- /dev/null
+++ b/CommentStreams/.gitreview
@@ -0,0 +1,6 @@
+[gerrit]
+host=gerrit.wikimedia.org
+port=29418
+project=mediawiki/extensions/CommentStreams.git
+track=1
+defaultrebase=0
diff --git a/CommentStreams/CODE_OF_CONDUCT.md b/CommentStreams/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..d8e5d087
--- /dev/null
+++ b/CommentStreams/CODE_OF_CONDUCT.md
@@ -0,0 +1 @@
+The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Code_of_Conduct).
diff --git a/CommentStreams/COPYING b/CommentStreams/COPYING
new file mode 100644
index 00000000..5fe3e109
--- /dev/null
+++ b/CommentStreams/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 The MITRE Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+*
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/CommentStreams/Gruntfile.js b/CommentStreams/Gruntfile.js
new file mode 100644
index 00000000..a45071e1
--- /dev/null
+++ b/CommentStreams/Gruntfile.js
@@ -0,0 +1,21 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+ grunt.loadNpmTasks( 'grunt-jsonlint' );
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
+
+ grunt.initConfig( {
+ banana: {
+ all: 'i18n/'
+ },
+ jsonlint: {
+ all: [
+ '**/*.json',
+ '!node_modules/**',
+ '!vendor/**'
+ ]
+ }
+ } );
+
+ grunt.registerTask( 'test', [ 'jsonlint', 'banana' ] );
+ grunt.registerTask( 'default', 'test' );
+};
diff --git a/CommentStreams/extension.json b/CommentStreams/extension.json
new file mode 100644
index 00000000..7df4da33
--- /dev/null
+++ b/CommentStreams/extension.json
@@ -0,0 +1,160 @@
+{
+ "name": "CommentStreams",
+ "version": "3.9",
+ "author": [
+ "[http://www.mediawiki.org/wiki/User:Jji Jason Ji]",
+ "[http://www.mediawiki.org/wiki/User:Cindy.cicalese Cindy Cicalese]"
+ ],
+ "url": "https://www.mediawiki.org/wiki/Extension:CommentStreams",
+ "descriptionmsg": "commentstreams-desc",
+ "manifest_version": 1,
+ "type": "parserhook",
+ "SpecialPages": {
+ "CommentStreamsAllComments": "CommentStreamsAllComments"
+ },
+ "MessagesDirs": {
+ "CommentStreams": [
+ "i18n"
+ ]
+ },
+ "ExtensionMessagesFiles": {
+ "CommentStreamsAlias": "includes/CommentStreamsAllComments.alias.php"
+ },
+ "ResourceModules": {
+ "ext.CommentStreams": {
+ "styles": [
+ "CommentStreams.css"
+ ],
+ "scripts": [
+ "CommentStreamsQuerier.js",
+ "CommentStreams.js",
+ "spin.min.js"
+ ],
+ "dependencies": [
+ "oojs-ui"
+ ],
+ "position": "top",
+ "targets": [
+ "desktop",
+ "mobile"
+ ],
+ "messages": [
+ "commentstreams-api-error-notloggedin",
+ "commentstreams-api-error-commentnotfound",
+ "commentstreams-api-error-notacomment",
+ "commentstreams-api-error-missingcommenttitle",
+ "commentstreams-api-error-post-permissions",
+ "commentstreams-api-error-post-parentandtitle",
+ "commentstreams-api-error-post-parentpagedoesnotexist",
+ "commentstreams-api-error-post-associatedpageidmismatch",
+ "commentstreams-api-error-post-associatedpagedoesnotexist",
+ "commentstreams-api-error-post",
+ "commentstreams-api-error-edit-notloggedin",
+ "commentstreams-api-error-edit-permissions",
+ "commentstreams-api-error-edit",
+ "commentstreams-api-error-delete-notloggedin",
+ "commentstreams-api-error-delete-permissions",
+ "commentstreams-api-error-delete-haschildren",
+ "commentstreams-api-error-delete",
+ "commentstreams-api-error-vote-notloggedin",
+ "commentstreams-api-error-vote-novoteonreply",
+ "commentstreams-api-error-vote",
+ "commentstreams-api-error-watch-notloggedin",
+ "commentstreams-api-error-watch-nowatchreply",
+ "commentstreams-api-error-watch",
+ "commentstreams-api-error-unwatch-notloggedin",
+ "commentstreams-api-error-unwatch-nounwatchreply",
+ "commentstreams-api-error-unwatch",
+ "commentstreams-validation-error-nocommenttitle",
+ "commentstreams-validation-error-nocommenttext",
+ "commentstreams-buttontooltip-add",
+ "commentstreams-buttontooltip-reply",
+ "commentstreams-buttontooltip-edit",
+ "commentstreams-buttontooltip-moderator-edit",
+ "commentstreams-buttontooltip-delete",
+ "commentstreams-buttontooltip-moderator-delete",
+ "commentstreams-buttontooltip-permalink",
+ "commentstreams-buttontooltip-collapse",
+ "commentstreams-buttontooltip-expand",
+ "commentstreams-buttontooltip-upvote",
+ "commentstreams-buttontooltip-downvote",
+ "commentstreams-buttontooltip-watch",
+ "commentstreams-buttontooltip-unwatch",
+ "commentstreams-buttontooltip-submit",
+ "commentstreams-buttontooltip-cancel",
+ "commentstreams-dialog-delete-message",
+ "commentstreams-dialog-buttontext-ok",
+ "commentstreams-dialog-buttontext-yes",
+ "commentstreams-dialog-buttontext-no",
+ "commentstreams-urldialog-instructions",
+ "commentstreams-datetext-postedon",
+ "commentstreams-datetext-lasteditedon",
+ "commentstreams-datetext-moderated",
+ "commentstreams-title-field-placeholder",
+ "commentstreams-body-field-placeholder"
+ ]
+ },
+ "ext.CommentStreamsAllComments": {
+ "styles": [
+ "CommentStreamsAllComments.css"
+ ],
+ "targets": [
+ "desktop",
+ "mobile"
+ ]
+ }
+ },
+ "ResourceFileModulePaths": {
+ "localBasePath": "resources",
+ "remoteExtPath": "CommentStreams/resources"
+ },
+ "AutoloadClasses": {
+ "CommentStreamsHooks": "includes/CommentStreamsHooks.php",
+ "CommentStreams": "includes/CommentStreams.php",
+ "Comment": "includes/Comment.php",
+ "ApiCSBase": "includes/ApiCSBase.php",
+ "ApiCSPostComment": "includes/ApiCSPostComment.php",
+ "ApiCSQueryComment": "includes/ApiCSQueryComment.php",
+ "ApiCSEditComment": "includes/ApiCSEditComment.php",
+ "ApiCSDeleteComment": "includes/ApiCSDeleteComment.php",
+ "ApiCSVote": "includes/ApiCSVote.php",
+ "ApiCSWatch": "includes/ApiCSWatch.php",
+ "ApiCSUnwatch": "includes/ApiCSUnwatch.php",
+ "EchoCSPresentationModel": "includes/EchoCSPresentationModel.php",
+ "CommentStreamsAllComments": "includes/CommentStreamsAllComments.php"
+ },
+ "APIModules": {
+ "csPostComment": "ApiCSPostComment",
+ "csQueryComment": "ApiCSQueryComment",
+ "csEditComment": "ApiCSEditComment",
+ "csDeleteComment": "ApiCSDeleteComment",
+ "csVote": "ApiCSVote",
+ "csWatch": "ApiCSWatch",
+ "csUnwatch": "ApiCSUnwatch"
+ },
+ "Hooks": {
+ "LoadExtensionSchemaUpdates": "CommentStreamsHooks::addCommentTableToDatabase",
+ "CanonicalNamespaces": "CommentStreamsHooks::addCommentStreamsNamespaces",
+ "MediaWikiPerformAction": "CommentStreamsHooks::onMediaWikiPerformAction",
+ "MovePageIsValidMove": "CommentStreamsHooks::onMovePageIsValidMove",
+ "userCan": "CommentStreamsHooks::userCan",
+ "ParserFirstCallInit": "CommentStreamsHooks::onParserSetup",
+ "BeforePageDisplay": "CommentStreamsHooks::addCommentsAndInitializeJS",
+ "ShowSearchHitTitle": "CommentStreamsHooks::showSearchHitTitle",
+ "smwInitProperties": "CommentStreamsHooks::initProperties",
+ "SMWStore::updateDataBefore": "CommentStreamsHooks::updateData",
+ "BeforeCreateEchoEvent": "CommentStreamsHooks::onBeforeCreateEchoEvent"
+ },
+ "callback" : "CommentStreamsHooks::onRegistration",
+ "config": {
+ "CommentStreamsNamespaceIndex": 844,
+ "CommentStreamsAllowedNamespaces" : null,
+ "CommentStreamsEnableTalk": false,
+ "CommentStreamsNewestStreamsOnTop": true,
+ "CommentStreamsModeratorFastDelete": false,
+ "CommentStreamsEnableVoting": false,
+ "CommentStreamsInitiallyCollapsedNamespaces" : [],
+ "CommentStreamsUserRealNamePropertyName" : null,
+ "CommentStreamsUserAvatarPropertyName" : null
+ }
+}
diff --git a/CommentStreams/gitinfo.json b/CommentStreams/gitinfo.json
new file mode 100644
index 00000000..5eefb7ec
--- /dev/null
+++ b/CommentStreams/gitinfo.json
@@ -0,0 +1 @@
+{"headSHA1": "08aec406a1dd80fce03133e93c07656a16f26437\n", "head": "08aec406a1dd80fce03133e93c07656a16f26437\n", "remoteURL": "https://gerrit.wikimedia.org/r/mediawiki/extensions/CommentStreams", "branch": "08aec406a1dd80fce03133e93c07656a16f26437\n", "headCommitDate": "1504900131"} \ No newline at end of file
diff --git a/CommentStreams/i18n/en.json b/CommentStreams/i18n/en.json
new file mode 100644
index 00000000..d6cff7db
--- /dev/null
+++ b/CommentStreams/i18n/en.json
@@ -0,0 +1,149 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jason Ji",
+ "Cindy Cicalese"
+ ]
+ },
+ "commentstreams-desc": "Allows commenting on wiki pages",
+ "commentstreams-error-prohibitedaction": "Action $1 is not allowed on comment pages.",
+ "apihelp-csQueryComment-description": "Return the title, user, creation timestamp, and wikitext of a comment. Either pageid or title must be provided.",
+ "apihelp-csQueryComment-summary": "Return the title, user, creation timestamp, and wikitext of a comment. Either pageid or title must be provided.",
+ "apihelp-csQueryComment-param-pageid": "page ID of the page which holds the comment to query",
+ "apihelp-csQueryComment-param-title": "title of the page which holds the comment to query",
+ "apihelp-csQueryComment-pageid-example": "query comment with page ID 3 in wikitext",
+ "apihelp-csQueryComment-title-example": "query comment with page title CommentStreams:3 in wikitext",
+ "apihelp-csDeleteComment-description": "Delete a comment. Either pageid or title must be provided.",
+ "apihelp-csDeleteComment-summary": "Delete a comment. Either pageid or title must be provided.",
+ "apihelp-csDeleteComment-param-pageid": "page ID of the page which holds the comment to delete",
+ "apihelp-csDeleteComment-param-title": "title of the page which holds the comment to delete",
+ "apihelp-csDeleteComment-pageid-example": "delete comment with page ID 3",
+ "apihelp-csDeleteComment-title-example": "delete comment with page title CommentStreams:3",
+ "apihelp-csPostComment-description": "Post a new comment.",
+ "apihelp-csPostComment-summary": "Post a new comment.",
+ "apihelp-csPostComment-param-commenttitle": "optional title for comment.",
+ "apihelp-csPostComment-param-wikitext": "wikitext for comment.",
+ "apihelp-csPostComment-param-associatedid": "page with which this comment is associated.",
+ "apihelp-csPostComment-param-parentid": "page ID of parent comment if this is a reply.",
+ "apihelp-csEditComment-description": "Edit an existing comment. Either pageid or title must be provided.",
+ "apihelp-csEditComment-summary": "Edit an existing comment. Either pageid or title must be provided.",
+ "apihelp-csEditComment-param-pageid": "page ID of the page which holds the comment to edit",
+ "apihelp-csEditComment-param-title": "title of the page which holds the comment to edit",
+ "apihelp-csEditComment-param-commenttitle": "optional title for comment.",
+ "apihelp-csEditComment-param-wikitext": "wikitext for comment.",
+ "apihelp-csVote-description": "Vote (up, down, or neutral) on a comment.",
+ "apihelp-csVote-summary": "Vote (up, down, or neutral) on a comment.",
+ "apihelp-csVote-param-pageid": "page ID of the page which holds the comment to be voted on",
+ "apihelp-csVote-param-title": "title of the page which holds the comment to be voted on",
+ "apihelp-csVote-param-vote": "vote (1, -1, or 0).",
+ "apihelp-csVote-pageid-example": "vote on comment with page ID 3",
+ "apihelp-csVote-title-example": "vote on comment with page title CommentStreams:3",
+ "apihelp-csWatch-description": "Watch a comment to be notified when it receives replies",
+ "apihelp-csWatch-summary": "Watch a comment to be notified when it receives replies",
+ "apihelp-csWatch-param-pageid": "page ID of the page which holds the comment to be watched",
+ "apihelp-csWatch-param-title": "title of the page which holds the comment to be watched",
+ "apihelp-csWatch-pageid-example": "watch comment with page ID 3",
+ "apihelp-csWatch-title-example": "watch comment with page title CommentStreams:3",
+ "apihelp-csUnwatch-description": "Unwatch a comment to no longer be notified when it receives replies",
+ "apihelp-csUnwatch-summary": "Unwatch a comment to no longer be notified when it receives replies",
+ "apihelp-csUnwatch-param-pageid": "page ID of the page which holds the comment to be unwatched",
+ "apihelp-csUnwatch-param-title": "title of the page which holds the comment to be unwatched",
+ "apihelp-csUnwatch-pageid-example": "unwatch comment with page ID 3",
+ "apihelp-csUnwatch-title-example": "unwatch comment with page title CommentStreams:3",
+ "commentstreams-api-error-notloggedin": "You must be logged in.",
+ "commentstreams-api-error-commentnotfound": "The requested comment was not found.",
+ "commentstreams-api-error-notacomment": "The supplied page ID does not refer to a valid comment.",
+ "commentstreams-api-error-missingcommenttitle": "A comment title must be supplied for comments that are not replies.",
+ "commentstreams-api-error-post-permissions": "User does not have permission to post a comment.",
+ "commentstreams-api-error-post-parentandtitle": "You may not specify both the parent id and the comment title.",
+ "commentstreams-api-error-post-parentpagedoesnotexist": "The comment being replied to does not exist.",
+ "commentstreams-api-error-post-associatedpageidmismatch": "The page being commented on does not match the page the parent comment is commenting on.",
+ "commentstreams-api-error-post-associatedpagedoesnotexist": "The page being commented on does not exist.",
+ "commentstreams-api-error-post": "Error adding comment.",
+ "commentstreams-api-error-edit-notloggedin": "You must be logged in to edit.",
+ "commentstreams-api-error-edit-permissions": "User does not have permission to edit the comment.",
+ "commentstreams-api-error-edit": "Error editing comment.",
+ "commentstreams-api-error-delete-notloggedin": "You must be logged in to delete.",
+ "commentstreams-api-error-delete-permissions": "User does not have permission to delete the comment.",
+ "commentstreams-api-error-delete-haschildren": "Cannot delete a topic that has replies. Please refresh the page to see updated comment stream.",
+ "commentstreams-api-error-delete": "Error deleting comment.",
+ "commentstreams-api-error-vote-notloggedin": "You must be logged in to vote.",
+ "commentstreams-api-error-vote-novoteonreply": "Voting on replies is not allowed.",
+ "commentstreams-api-error-vote": "Error voting on comment.",
+ "commentstreams-api-error-watch-notloggedin": "You must be logged in to watch a comment.",
+ "commentstreams-api-error-watch-nowatchreply": "Watching replies is not allowed.",
+ "commentstreams-api-error-watch": "Error watching comment.",
+ "commentstreams-api-error-unwatch-notloggedin": "You must be logged in to unwatch a comment.",
+ "commentstreams-api-error-unwatch-nounwatchreply": "Unwatching replies is not allowed.",
+ "commentstreams-api-error-unwatch": "Error unwatching comment.",
+ "commentstreams-validation-error-nocommenttitle": "You must enter a comment title.",
+ "commentstreams-validation-error-nocommenttext": "You must enter comment text.",
+ "commentstreams-buttontooltip-add": "add a comment",
+ "commentstreams-buttontooltip-reply": "reply",
+ "commentstreams-buttontooltip-edit": "edit",
+ "commentstreams-buttontooltip-moderator-edit": "moderator edit",
+ "commentstreams-buttontooltip-delete": "delete",
+ "commentstreams-buttontooltip-moderator-delete": "moderator delete",
+ "commentstreams-buttontooltip-permalink": "permalink",
+ "commentstreams-buttontooltip-collapse": "collapse",
+ "commentstreams-buttontooltip-expand": "expand",
+ "commentstreams-buttontooltip-upvote": "up vote",
+ "commentstreams-buttontooltip-downvote": "down vote",
+ "commentstreams-buttontooltip-watch": "watch",
+ "commentstreams-buttontooltip-unwatch": "unwatch",
+ "commentstreams-buttontooltip-submit": "submit",
+ "commentstreams-buttontooltip-cancel": "cancel",
+ "commentstreams-dialog-delete-message": "Are you sure you want to delete this comment?",
+ "commentstreams-dialog-buttontext-ok": "OK",
+ "commentstreams-dialog-buttontext-yes": "Yes",
+ "commentstreams-dialog-buttontext-no": "No",
+ "commentstreams-urldialog-instructions": "Copy and paste the URL below to share a permalink to this comment. Press escape to dismiss this dialog.",
+ "commentstreams-datetext-postedon": "Posted on",
+ "commentstreams-datetext-lasteditedon": "Last edited on",
+ "commentstreams-datetext-moderated": "moderated",
+ "commentstreams-title-field-placeholder": "Enter title...",
+ "commentstreams-body-field-placeholder": "Enter new comment text...",
+ "echo-category-title-commentstreams-notification-category": "New comments and replies",
+ "notification-header-commentstreams-comment-on-watched-page": "$1 {{GENDER:$4|commented}} \"<i>$2</i>\" on page \"<i>$3</i>\", which {{GENDER:$6|you}} are watching.",
+ "notification-header-commentstreams-reply-on-watched-page": "$1 {{GENDER:$4|replied}} to comment \"<i>$2</i>\" on page \"<i>$3</i>\", which {{GENDER:$6|you}} are watching.",
+ "notification-header-commentstreams-reply-to-watched-comment": "$1 {{GENDER:$4|replied}} to comment \"<i>$2</i>\", which {{GENDER:$6|you}} are watching, on page \"<i>$3</i>\".",
+ "notification-subject-commentstreams-comment-on-watched-page": "Somebody has commented on a page that you are watching",
+ "notification-subject-commentstreams-reply-on-watched-page": "Somebody has replied to a comment on a page that you are watching",
+ "notification-subject-commentstreams-reply-to-watched-comment": "Somebody has replied to a comment that you are watching",
+ "notification-body-commentstreams-comment-on-watched-page": "The comment is:\n\n\n$5",
+ "notification-body-commentstreams-reply-on-watched-page": "The reply is:\n\n\n$5",
+ "notification-body-commentstreams-reply-to-watched-comment": "The reply is:\n\n\n$5",
+ "notification-link-label-commentstreams-comment-on-watched-page": "Visit comment",
+ "notification-link-label-commentstreams-reply-on-watched-page": "Visit reply",
+ "notification-link-label-commentstreams-reply-to-watched-comment": "Visit reply",
+ "group-csmoderator": "Moderators (CommentStreams)",
+ "group-csmoderator-member": "{{GENDER:$1|moderator (CommentStreams)}}",
+ "grouppage-csmoderator": "{{ns:project}}:Moderators (CommentStreams)",
+ "right-cs-moderator-edit": "Edit comments by any user",
+ "action-cs-moderator-edit": "edit comments by other users",
+ "right-cs-moderator-delete": "Delete comments by any user",
+ "action-cs-moderator-delete": "delete comments by other users",
+ "log-name-commentstreams": "CommentStreams log",
+ "log-description-commentstreams": "These events track when CommentStreams events happen.",
+ "logentry-commentstreams-comment-create": "$1 {{GENDER:$2|created}} comment $3",
+ "logentry-commentstreams-reply-create": "$1 {{GENDER:$2|created}} reply $3",
+ "logentry-commentstreams-comment-edit": "$1 {{GENDER:$2|edited}} comment $3",
+ "logentry-commentstreams-comment-moderator-edit": "$1 {{GENDER:$2|(moderator) edited}} comment $3",
+ "logentry-commentstreams-reply-edit": "$1 {{GENDER:$2|edited}} reply $3",
+ "logentry-commentstreams-reply-moderator-edit": "$1 {{GENDER:$2|(moderator) edited}} reply $3",
+ "logentry-commentstreams-comment-delete": "$1 {{GENDER:$2|deleted}} comment $3",
+ "logentry-commentstreams-comment-moderator-delete": "$1 {{GENDER:$2|(moderator) deleted}} comment $3",
+ "logentry-commentstreams-reply-delete": "$1 {{GENDER:$2|deleted}} reply $3",
+ "logentry-commentstreams-reply-moderator-delete": "$1 {{GENDER:$2|(moderator) deleted}} reply $3",
+ "commentstreamsallcomments": "All Comments",
+ "commentstreams-allcomments-label-page": "Comment Page",
+ "commentstreams-allcomments-label-associatedpage": "Associated Page",
+ "commentstreams-allcomments-label-commenttitle": "Comment Title",
+ "commentstreams-allcomments-label-wikitext": "Comment",
+ "commentstreams-allcomments-label-author": "Author",
+ "commentstreams-allcomments-label-created": "Created",
+ "commentstreams-allcomments-label-lasteditor": "Last Editor",
+ "commentstreams-allcomments-label-lastedited": "Last Edited",
+ "commentstreams-allcomments-button-next": "Next",
+ "commentstreams-allcomments-button-previous": "Previous"
+}
diff --git a/CommentStreams/i18n/qqq.json b/CommentStreams/i18n/qqq.json
new file mode 100644
index 00000000..d824cf43
--- /dev/null
+++ b/CommentStreams/i18n/qqq.json
@@ -0,0 +1,149 @@
+{
+ "@metadata": {
+ "authors": [
+ "Cindy Cicalese",
+ "Jason Ji"
+ ]
+ },
+ "commentstreams-desc": "{{desc|name=CommentStreams|url=http://gestalt.mitre.org/gestaltd/index.php/CommentStreams}}",
+ "commentstreams-error-prohibitedaction": "Error message.",
+ "apihelp-csQueryComment-description": "{{doc-apihelp-description|csQueryComment}}",
+ "apihelp-csQueryComment-summary": "{{doc-apihelp-summary|csQueryComment}}",
+ "apihelp-csQueryComment-param-pageid": "{{doc-apihelp-param|csQueryComment|pageid}}",
+ "apihelp-csQueryComment-param-title": "{{doc-apihelp-param|csQueryComment|title}}",
+ "apihelp-csQueryComment-pageid-example": "{{doc-apihelp-example|csQueryComment}}",
+ "apihelp-csQueryComment-title-example": "{{doc-apihelp-example|csQueryComment}}",
+ "apihelp-csDeleteComment-description": "{{doc-apihelp-description|csDeleteComment}}",
+ "apihelp-csDeleteComment-summary": "{{doc-apihelp-summary|csDeleteComment}}",
+ "apihelp-csDeleteComment-param-pageid": "{{doc-apihelp-param|csDeleteComment|pageid}}",
+ "apihelp-csDeleteComment-param-title": "{{doc-apihelp-param|csDeleteComment|title}}",
+ "apihelp-csDeleteComment-pageid-example": "{{doc-apihelp-example|csDeleteComment}}",
+ "apihelp-csDeleteComment-title-example": "{{doc-apihelp-example|csDeleteComment}}",
+ "apihelp-csPostComment-description": "{{doc-apihelp-description|csPostComment}}",
+ "apihelp-csPostComment-summary": "{{doc-apihelp-summary|csPostComment}}",
+ "apihelp-csPostComment-param-commenttitle": "{{doc-apihelp-param|csPostComment|commenttitle}}",
+ "apihelp-csPostComment-param-wikitext": "{{doc-apihelp-param|csPostComment|wikitext}}",
+ "apihelp-csPostComment-param-associatedid": "{{doc-apihelp-param|csPostComment|associatedid}}",
+ "apihelp-csPostComment-param-parentid": "{{doc-apihelp-param|csPostComment|parentid}}",
+ "apihelp-csEditComment-description": "{{doc-apihelp-description|csEditComment}}",
+ "apihelp-csEditComment-summary": "{{doc-apihelp-summary|csEditComment}}",
+ "apihelp-csEditComment-param-pageid": "{{doc-apihelp-param|csEditComment|pageid}}",
+ "apihelp-csEditComment-param-title": "{{doc-apihelp-param|csEditComment|title}}",
+ "apihelp-csEditComment-param-commenttitle": "{{doc-apihelp-param|csEditComment|commenttitle}}",
+ "apihelp-csEditComment-param-wikitext": "{{doc-apihelp-param|csEditComment|wikitext}}",
+ "apihelp-csVote-description": "{{doc-apihelp-description|csVote}}",
+ "apihelp-csVote-summary": "{{doc-apihelp-summary|csVote}}",
+ "apihelp-csVote-param-pageid": "{{doc-apihelp-param|csVote|pageid}}",
+ "apihelp-csVote-param-title": "{{doc-apihelp-param|csVote|title}}",
+ "apihelp-csVote-param-vote": "{{doc-apihelp-param|csVote|vote}}",
+ "apihelp-csVote-pageid-example": "{{doc-apihelp-example|csVote}}",
+ "apihelp-csVote-title-example": "{{doc-apihelp-example|csVote}}",
+ "apihelp-csWatch-description": "{{doc-apihelp-description|csWatch}}",
+ "apihelp-csWatch-summary": "{{doc-apihelp-summary|csWatch}}",
+ "apihelp-csWatch-param-pageid": "{{doc-apihelp-param|csWatch|pageid}}",
+ "apihelp-csWatch-param-title": "{{doc-apihelp-param|csWatch|title}}",
+ "apihelp-csWatch-pageid-example": "{{doc-apihelp-example|csWatch}}",
+ "apihelp-csWatch-title-example": "{{doc-apihelp-example|csWatch}}",
+ "apihelp-csUnwatch-description": "{{doc-apihelp-description|csUnwatch}}",
+ "apihelp-csUnwatch-summary": "{{doc-apihelp-summary|csUnwatch}}",
+ "apihelp-csUnwatch-param-pageid": "{{doc-apihelp-param|csUnwatch|pageid}}",
+ "apihelp-csUnwatch-param-title": "{{doc-apihelp-param|csUnwatch|title}}",
+ "apihelp-csUnwatch-pageid-example": "{{doc-apihelp-example|csUnwatch}}",
+ "apihelp-csUnwatch-title-example": "{{doc-apihelp-example|csUnwatch}}",
+ "commentstreams-api-error-notloggedin": "Error message.",
+ "commentstreams-api-error-commentnotfound": "Error message.",
+ "commentstreams-api-error-notacomment": "Error message.",
+ "commentstreams-api-error-missingcommenttitle": "Error message.",
+ "commentstreams-api-error-post-permissions": "Error message.",
+ "commentstreams-api-error-post-parentandtitle": "Error message.",
+ "commentstreams-api-error-post-parentpagedoesnotexist": "Error message.",
+ "commentstreams-api-error-post-associatedpageidmismatch": "Error message.",
+ "commentstreams-api-error-post-associatedpagedoesnotexist": "Error message.",
+ "commentstreams-api-error-post": "Error message.",
+ "commentstreams-api-error-edit-notloggedin": "Error message.",
+ "commentstreams-api-error-edit-permissions": "Error message.",
+ "commentstreams-api-error-edit": "Error message.",
+ "commentstreams-api-error-delete-notloggedin": "Error message.",
+ "commentstreams-api-error-delete-permissions": "Error message.",
+ "commentstreams-api-error-delete-haschildren": "Error message.",
+ "commentstreams-api-error-delete": "Error message.",
+ "commentstreams-api-error-vote-notloggedin": "Error message.",
+ "commentstreams-api-error-vote-novoteonreply": "Error message.",
+ "commentstreams-api-error-vote": "Error message.",
+ "commentstreams-api-error-watch-notloggedin": "Error message.",
+ "commentstreams-api-error-watch-nowatchreply": "Error message.",
+ "commentstreams-api-error-watch": "Error message.",
+ "commentstreams-api-error-unwatch-notloggedin": "Error message.",
+ "commentstreams-api-error-unwatch-nounwatchreply": "Error message.",
+ "commentstreams-api-error-unwatch": "Error message.",
+ "commentstreams-validation-error-nocommenttitle": "Error message.",
+ "commentstreams-validation-error-nocommenttext": "Error message.",
+ "commentstreams-buttontooltip-add": "User interface button tooltip.",
+ "commentstreams-buttontooltip-reply": "User interface button tooltip.",
+ "commentstreams-buttontooltip-edit": "User interface button tooltip.",
+ "commentstreams-buttontooltip-moderator-edit": "User interface button tooltip.",
+ "commentstreams-buttontooltip-delete": "User interface button tooltip.",
+ "commentstreams-buttontooltip-moderator-delete": "User interface button tooltip.",
+ "commentstreams-buttontooltip-permalink": "User interface button tooltip.",
+ "commentstreams-buttontooltip-collapse": "User interface button tooltip.",
+ "commentstreams-buttontooltip-expand": "User interface button tooltip.",
+ "commentstreams-buttontooltip-upvote": "User interface button tooltip.",
+ "commentstreams-buttontooltip-downvote": "User interface button tooltip.",
+ "commentstreams-buttontooltip-watch": "User interface button tooltip.",
+ "commentstreams-buttontooltip-unwatch": "User interface button tooltip.",
+ "commentstreams-buttontooltip-submit": "User interface button text.",
+ "commentstreams-buttontooltip-cancel": "User interface button text.",
+ "commentstreams-dialog-delete-message": "User interface dialog message.",
+ "commentstreams-dialog-buttontext-ok": "User interface dialog button text.",
+ "commentstreams-dialog-buttontext-yes": "User interface dialog button text.",
+ "commentstreams-dialog-buttontext-no": "User interface dialog button text.",
+ "commentstreams-urldialog-instructions": "User interface dialog button text.",
+ "commentstreams-datetext-postedon": "User interface date text",
+ "commentstreams-datetext-lasteditedon": "User interface date text",
+ "commentstreams-datetext-moderated": "User interface date text",
+ "commentstreams-title-field-placeholder": "Text field placeholder text",
+ "commentstreams-body-field-placeholder": "Text field placeholder text",
+ "echo-category-title-commentstreams-notification-category": "Name of category on Prefences/Notifications page for CommentStreams notifications",
+ "notification-header-commentstreams-comment-on-watched-page": "Flyout-specific format for displaying notification header of a new comment on a watched page.\n\nParameters:\n* $1 - the formatted username of the person who commented.\n* $2 - the comment title.\n* $3 - the display name of the page being commented on.\n* $4 - the username of the person who commented (for gender).\n* $5 - the username of the viewing user (for gender).",
+ "notification-header-commentstreams-reply-on-watched-page": "Flyout-specific format for displaying notification header of a new reply on a watched page.\n\nParameters:\n* $1 - the formatted username of the person who commented.\n* $2 - the comment title.\n* $3 - the display name of the page being commented on.\n* $4 - the username of the person who commented (for gender).\n* $5 - the username of the viewing user (for gender).",
+ "notification-header-commentstreams-reply-to-watched-comment": "Flyout-specific format for displaying notification header of a reply to a comment by the user.\n\nParameters:\n* $1 - the formatted username of the person who commented.\n* $2 - the comment title.\n* $3 - the display name of the page being commented on.\n* $4 - the username of the person who commented (for gender).\n* $5 - the username of the viewing user (for gender).",
+ "notification-subject-commentstreams-comment-on-watched-page": "Echo email subject.",
+ "notification-body-commentstreams-comment-on-watched-page": "Echo email body.\n\nParameters:\n*$1 - the username of the user authoring the new comment.\n* $2 - the display name of the comment author.\n* $3 - the title of the comment.\n* $4 - the display title of the page being commented on.\n* $5 - a link to that page.\n* $6 - the wikitext of the new comment.",
+ "notification-subject-commentstreams-reply-on-watched-page": "Echo email subject.",
+ "notification-body-commentstreams-reply-on-watched-page": "Echo email body.\n\nParameters:\n*$1 - the username of the user authoring the new comment.\n* $2 - the display name of the comment author.\n* $3 - the title of the comment.\n* $4 - the display title of the page being commented on.\n* $5 - a link to that page.\n* $6 - the wikitext of the new comment.",
+ "notification-subject-commentstreams-reply-to-watched-comment": "Echo email subject.",
+ "notification-body-commentstreams-reply-to-watched-comment": "Echo email body.\n\nParameters:\n*$1 - the username of the user authoring the new comment.\n* $2 - the display name of the comment author.\n* $3 - the title of the comment.\n* $4 - the display title of the page being commented on.\n* $5 - a link to that page.\n* $6 - the wikitext of the new comment.",
+ "notification-link-label-commentstreams-comment-on-watched-page": "Label",
+ "notification-link-label-commentstreams-reply-on-watched-page": "Label",
+ "notification-link-label-commentstreams-reply-to-watched-comment": "Label",
+ "group-csmoderator": "{{doc-group|csmoderator|group}}",
+ "group-csmoderator-member": "{{doc-group|csmoderator|member}}",
+ "grouppage-csmoderator": "{{doc-group|csmoderator|page}}",
+ "right-cs-moderator-edit": "{{doc-right|csedit}}",
+ "action-cs-moderator-edit": "{{doc-action|csedit}}",
+ "right-cs-moderator-delete": "{{doc-right|csdelete}}",
+ "action-cs-moderator-delete": "{{doc-action|csdelete}}",
+ "log-name-commentstreams": "The Special:Log log name that appears in the drop-down on the Special:Log page",
+ "log-description-commentstreams": "The Special:Log description that appears on the Special:Log page when you filter logs on this specific log name",
+ "logentry-commentstreams-comment-create": "The template of the log entry message",
+ "logentry-commentstreams-reply-create": "The template of the log entry message",
+ "logentry-commentstreams-comment-edit": "The template of the log entry message",
+ "logentry-commentstreams-comment-moderator-edit": "The template of the log entry message",
+ "logentry-commentstreams-reply-edit": "The template of the log entry message",
+ "logentry-commentstreams-reply-moderator-edit": "The template of the log entry message",
+ "logentry-commentstreams-comment-delete": "The template of the log entry message",
+ "logentry-commentstreams-comment-moderator-delete": "The template of the log entry message",
+ "logentry-commentstreams-reply-delete": "The template of the log entry message",
+ "logentry-commentstreams-reply-moderator-delete": "The template of the log entry message",
+ "commentstreamsallcomments": "Special page title",
+ "commentstreams-allcomments-label-page": "Table column label",
+ "commentstreams-allcomments-label-associatedpage": "Table column label",
+ "commentstreams-allcomments-label-commenttitle": "Table column label",
+ "commentstreams-allcomments-label-wikitext": "Table column label",
+ "commentstreams-allcomments-label-author": "Table column label",
+ "commentstreams-allcomments-label-created": "Table column label",
+ "commentstreams-allcomments-label-lasteditor": "Table column label",
+ "commentstreams-allcomments-label-lastedited": "Table column label",
+ "commentstreams-allcomments-button-next": "Button label",
+ "commentstreams-allcomments-button-previous": "Button label"
+}
diff --git a/CommentStreams/images/CREDITS b/CommentStreams/images/CREDITS
new file mode 100644
index 00000000..ec86b3e0
--- /dev/null
+++ b/CommentStreams/images/CREDITS
@@ -0,0 +1,5 @@
+The icons in this directory are derived from Farm-Fresh Web Icons
+(http://www.fatcow.com/free-icons), which are licensed under a Creative
+Commons Attribution 3.0 License. They were downloaded from Wikimedia Commons
+(https://commons.wikimedia.org/wiki/Farm-Fresh_web_icons) and modified to suit
+the purposes of this MediaWiki extension.
diff --git a/CommentStreams/images/cancel.png b/CommentStreams/images/cancel.png
new file mode 100644
index 00000000..29e9ee6a
--- /dev/null
+++ b/CommentStreams/images/cancel.png
Binary files differ
diff --git a/CommentStreams/images/collapse.png b/CommentStreams/images/collapse.png
new file mode 100644
index 00000000..33047bb8
--- /dev/null
+++ b/CommentStreams/images/collapse.png
Binary files differ
diff --git a/CommentStreams/images/comment_add.png b/CommentStreams/images/comment_add.png
new file mode 100644
index 00000000..3682cd22
--- /dev/null
+++ b/CommentStreams/images/comment_add.png
Binary files differ
diff --git a/CommentStreams/images/comment_delete.png b/CommentStreams/images/comment_delete.png
new file mode 100644
index 00000000..ae8a6abb
--- /dev/null
+++ b/CommentStreams/images/comment_delete.png
Binary files differ
diff --git a/CommentStreams/images/comment_edit.png b/CommentStreams/images/comment_edit.png
new file mode 100644
index 00000000..ca50070c
--- /dev/null
+++ b/CommentStreams/images/comment_edit.png
Binary files differ
diff --git a/CommentStreams/images/comment_moderator_delete.png b/CommentStreams/images/comment_moderator_delete.png
new file mode 100644
index 00000000..4a51aa83
--- /dev/null
+++ b/CommentStreams/images/comment_moderator_delete.png
Binary files differ
diff --git a/CommentStreams/images/comment_moderator_edit.png b/CommentStreams/images/comment_moderator_edit.png
new file mode 100644
index 00000000..9176f1e9
--- /dev/null
+++ b/CommentStreams/images/comment_moderator_edit.png
Binary files differ
diff --git a/CommentStreams/images/comment_reply.png b/CommentStreams/images/comment_reply.png
new file mode 100644
index 00000000..05676052
--- /dev/null
+++ b/CommentStreams/images/comment_reply.png
Binary files differ
diff --git a/CommentStreams/images/downvote-disabled.png b/CommentStreams/images/downvote-disabled.png
new file mode 100644
index 00000000..dc10396b
--- /dev/null
+++ b/CommentStreams/images/downvote-disabled.png
Binary files differ
diff --git a/CommentStreams/images/downvote-enabled.png b/CommentStreams/images/downvote-enabled.png
new file mode 100644
index 00000000..057cf47a
--- /dev/null
+++ b/CommentStreams/images/downvote-enabled.png
Binary files differ
diff --git a/CommentStreams/images/expand.png b/CommentStreams/images/expand.png
new file mode 100644
index 00000000..993ab02a
--- /dev/null
+++ b/CommentStreams/images/expand.png
Binary files differ
diff --git a/CommentStreams/images/link.png b/CommentStreams/images/link.png
new file mode 100644
index 00000000..0e10aedd
--- /dev/null
+++ b/CommentStreams/images/link.png
Binary files differ
diff --git a/CommentStreams/images/notwatching.png b/CommentStreams/images/notwatching.png
new file mode 100644
index 00000000..a14af7ee
--- /dev/null
+++ b/CommentStreams/images/notwatching.png
Binary files differ
diff --git a/CommentStreams/images/submit.png b/CommentStreams/images/submit.png
new file mode 100644
index 00000000..8ea8d8d4
--- /dev/null
+++ b/CommentStreams/images/submit.png
Binary files differ
diff --git a/CommentStreams/images/upvote-disabled.png b/CommentStreams/images/upvote-disabled.png
new file mode 100644
index 00000000..3816ef6a
--- /dev/null
+++ b/CommentStreams/images/upvote-disabled.png
Binary files differ
diff --git a/CommentStreams/images/upvote-enabled.png b/CommentStreams/images/upvote-enabled.png
new file mode 100644
index 00000000..15f5bfea
--- /dev/null
+++ b/CommentStreams/images/upvote-enabled.png
Binary files differ
diff --git a/CommentStreams/images/watching.png b/CommentStreams/images/watching.png
new file mode 100644
index 00000000..cf642b47
--- /dev/null
+++ b/CommentStreams/images/watching.png
Binary files differ
diff --git a/CommentStreams/includes/ApiCSBase.php b/CommentStreams/includes/ApiCSBase.php
new file mode 100644
index 00000000..78ac2e27
--- /dev/null
+++ b/CommentStreams/includes/ApiCSBase.php
@@ -0,0 +1,127 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+abstract class ApiCSBase extends ApiBase {
+
+ private $edit;
+ protected $comment;
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ * @param boolean $edit whether this API module will be editing the database
+ */
+ public function __construct( $main, $action, $edit = false ) {
+ parent::__construct( $main, $action );
+ $this->edit = $edit;
+ }
+
+ /**
+ * execute the API request
+ */
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $wikipage = $this->getTitleOrPageId( $params,
+ $this->edit ? 'frommasterdb' : 'fromdb' );
+ $this->comment = Comment::newFromWikiPage( $wikipage );
+ if ( is_null( $this->comment ) ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-notacomment' );
+ }
+ $result = $this->executeBody();
+ if ( !is_null( $result ) ) {
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
+ }
+ }
+
+ /**
+ * the real body of the execute function
+ */
+ protected abstract function executeBody();
+
+ /**
+ * @return array allowed parameters
+ */
+ public function getAllowedParams() {
+ return [
+ 'pageid' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_REQUIRED => false
+ ],
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => false
+ ]
+ ];
+ }
+
+ /**
+ * @return array examples of the use of this API module
+ */
+ public function getExamplesMessages() {
+ return [
+ 'action=' . $this->getModuleName() . '&pageid=3' =>
+ 'apihelp-' . $this->getModuleName() . '-pageid-example',
+ 'action=' . $this->getModuleName() . '&title=CommentStreams:3' =>
+ 'apihelp-' . $this->getModuleName() . '-title-example'
+ ];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF token
+ */
+ public function needsToken() {
+ if ( $this->edit ) {
+ return 'csrf';
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * log action
+ * @param string $action the name of the action to be logged
+ */
+ protected function logAction( $action, $title = null ) {
+ $logEntry = new ManualLogEntry( 'commentstreams', $action );
+ $logEntry->setPerformer( $this->getUser() );
+ if ( $title ) {
+ $logEntry->setTarget( $title );
+ } else {
+ $logEntry->setTarget( $this->comment->getWikiPage()->getTitle() );
+ }
+ $logid = $logEntry->insert();
+ }
+
+ /**
+ * die with a custom usage message
+ * @param string $message_name the name of the custom message
+ */
+ protected function dieCustomUsageMessage( $message_name ) {
+ $error_message = wfMessage( $message_name );
+ $this->dieUsageMsg(
+ [
+ ApiMessage::create( $error_message )
+ ]
+ );
+ }
+}
diff --git a/CommentStreams/includes/ApiCSDeleteComment.php b/CommentStreams/includes/ApiCSDeleteComment.php
new file mode 100644
index 00000000..460038cd
--- /dev/null
+++ b/CommentStreams/includes/ApiCSDeleteComment.php
@@ -0,0 +1,114 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSDeleteComment extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, true );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ if ( $this->getUser()->isAnon() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-delete-notloggedin' );
+ }
+
+ if ( $this->getUser()->getId() ===
+ $this->comment->getWikiPage()->getOldestRevision()->getUser() &&
+ $this->comment->getNumReplies() === 0 ) {
+ $action = 'edit'; // need edit but not delete to delete a comment
+ } else {
+ $action = 'cs-moderator-delete';
+ }
+
+ if ( !$this->comment->getWikiPage()->getTitle()->userCan( $action,
+ $this->getUser() ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-delete-permissions' );
+ }
+
+ $childCount = $this->comment->getNumReplies();
+ if ( $childCount > 0 ) {
+ if ( $GLOBALS['wgCommentStreamsModeratorFastDelete'] ) {
+ $result = $this->recursiveDelete( $this->comment );
+ } else {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-delete-haschildren' );
+ }
+ } else {
+ $result = $this->comment->delete();
+ if ( $action === 'edit' ) {
+ if ( is_null( $this->comment->getParentId() ) ) {
+ $this->logAction( 'comment-delete' );
+ } else {
+ $this->logAction( 'reply-delete' );
+ }
+ } else {
+ if ( is_null( $this->comment->getParentId() ) ) {
+ $this->logAction( 'comment-moderator-delete' );
+ } else {
+ $this->logAction( 'reply-moderator-delete' );
+ }
+ }
+ }
+
+ if ( !$result ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-delete' );
+ }
+
+ return null;
+ }
+
+ /**
+ * recursively delete comment and replies
+ *
+ * @param Comment $comment the comment to recursively delete
+ */
+ private function recursiveDelete( $comment ) {
+ $replies = Comment::getReplies( $comment->getId() );
+ foreach ( $replies as $reply ) {
+ $result = $this->recursiveDelete( $reply );
+ if ( !$result ) {
+ return $result;
+ }
+ }
+ $result = $comment->delete();
+ $title = $comment->getWikiPage()->getTitle();
+ if ( is_null( $comment->getParentId() ) ) {
+ $this->logAction( 'comment-moderator-delete', $title );
+ } else {
+ $this->logAction( 'reply-moderator-delete', $title );
+ }
+ return $result;
+ }
+}
diff --git a/CommentStreams/includes/ApiCSEditComment.php b/CommentStreams/includes/ApiCSEditComment.php
new file mode 100644
index 00000000..0423fdb8
--- /dev/null
+++ b/CommentStreams/includes/ApiCSEditComment.php
@@ -0,0 +1,129 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSEditComment extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, true );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ if ( $this->getUser()->isAnon() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-edit-notloggedin' );
+ }
+
+ if ( $this->getUser()->getId() ===
+ $this->comment->getWikiPage()->getOldestRevision()->getUser() ) {
+ $action = 'edit';
+ } else {
+ $action = 'cs-moderator-edit';
+ }
+ if ( !$this->comment->getWikiPage()->getTitle()->userCan( $action,
+ $this->getUser() ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-edit-permissions' );
+ }
+
+ $comment_title = $this->getMain()->getVal( 'commenttitle' );
+ $wikitext = $this->getMain()->getVal( 'wikitext' );
+
+ if ( is_null( $this->comment->getParentId() ) && is_null( $comment_title ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-missingcommenttitle' );
+ }
+
+ $result = $this->comment->update( $comment_title, $wikitext, $this->getUser() );
+ if ( !$result ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-edit' );
+ }
+
+ if ( $action === 'edit' ) {
+ if ( is_null( $this->comment->getParentId() ) ) {
+ $this->logAction( 'comment-edit' );
+ } else {
+ $this->logAction( 'reply-edit' );
+ }
+ } else {
+ if ( is_null( $this->comment->getParentId() ) ) {
+ $this->logAction( 'comment-moderator-edit' );
+ } else {
+ $this->logAction( 'reply-moderator-edit' );
+ }
+ }
+
+ $json = $this->comment->getJSON();
+
+ if ( is_null( $this->comment->getParentId() ) ) {
+ if ( $GLOBALS['wgCommentStreamsEnableVoting'] ) {
+ $json['vote'] = $this->comment->getVote( $this->getUser() );
+ }
+ $json['watching'] = $this->comment->isWatching( $this->getUser() ) ? 1 : 0;
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $json );
+ }
+
+ /**
+ * @return array allowed paramters
+ */
+ public function getAllowedParams() {
+ return array_merge( parent::getAllowedParams(),
+ [
+ 'commenttitle' =>
+ [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => false
+ ],
+ 'wikitext' =>
+ [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return array examples of the use of this API module
+ */
+ public function getExamplesMessages() {
+ return [];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF toekn
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+}
diff --git a/CommentStreams/includes/ApiCSPostComment.php b/CommentStreams/includes/ApiCSPostComment.php
new file mode 100644
index 00000000..adc1c797
--- /dev/null
+++ b/CommentStreams/includes/ApiCSPostComment.php
@@ -0,0 +1,228 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSPostComment extends ApiBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ }
+
+ /**
+ * execute the API request
+ */
+ public function execute() {
+ if ( !in_array( 'edit', $this->getUser()->getRights() ) ||
+ $this->getUser()->isBlocked() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-post-permissions' );
+ }
+
+ $associatedid = $this->getMain()->getVal( 'associatedid' );
+ $parentid = $this->getMain()->getVal( 'parentid' );
+ $comment_title = $this->getMain()->getVal( 'commenttitle' );
+ $wikitext = $this->getMain()->getVal( 'wikitext' );
+
+ if ( is_null( $parentid ) && is_null( $comment_title ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-missingcommenttitle' );
+ }
+
+ if ( !is_null( $parentid ) && !is_null( $comment_title ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-post-parentandtitle' );
+ }
+
+ if ( !is_null( $parentid ) ) {
+ $parent_page = WikiPage::newFromId( $parentid );
+ if ( is_null( $parent_page ) || !$parent_page->getTitle()->exists() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-post-parentpagedoesnotexist' );
+ }
+ $parent_comment = Comment::newFromWikiPage( $parent_page );
+ if ( $parent_comment->getAssociatedId() !== (integer)$associatedid ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-post-associatedpageidmismatch' );
+ }
+ }
+
+ $associated_page = WikiPage::newFromId( $associatedid );
+ if ( is_null( $associated_page ) ||
+ !$associated_page->getTitle()->exists() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-post-associatedpagedoesnotexist' );
+ }
+
+ $comment = Comment::newFromValues( $associatedid, $parentid,
+ $comment_title, $wikitext, $this->getUser() );
+ if ( !$comment ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-post' );
+ }
+
+ $title = $comment->getWikiPage()->getTitle();
+ if ( is_null( $comment->getParentId() ) ) {
+ $this->logAction( 'comment-create', $title );
+ } else {
+ $this->logAction( 'reply-create', $title );
+ }
+
+ $json = $comment->getJSON();
+ if ( class_exists( 'EchoEvent' ) && is_null( $comment->getParentId() ) ) {
+ $json['watching'] = 1;
+ }
+ $this->getResult()->addValue( null, $this->getModuleName(), $json );
+
+ $this->sendNotifications( $comment, $associated_page );
+ }
+
+ /**
+ * @return array allowed paramters
+ */
+ public function getAllowedParams() {
+ return [
+ 'commenttitle' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => false
+ ],
+ 'wikitext' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true
+ ],
+ 'associatedid' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_REQUIRED => true
+ ],
+ 'parentid' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_REQUIRED => false
+ ]
+ ];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF token
+ */
+ public function needstoken() {
+ return 'csrf';
+ }
+
+ /**
+ * Send Echo notifications if Echo is installed.
+ *
+ * @param Comment $comment the comment to send notifications for
+ * @param WikiPage $associated_page the associated page for the comment
+ * @return not used
+ */
+ private function sendNotifications( $comment, $associated_page ) {
+ if ( !class_exists( 'EchoEvent' ) ) {
+ return;
+ }
+
+ $parent_id = $comment->getParentId();
+ if ( is_null( $parent_id ) ) {
+ $comment_title = $comment->getCommentTitle();
+ } else {
+ $parent_page = WikiPage::newFromId( $parent_id );
+ if ( is_null( $parent_page ) ) {
+ return;
+ }
+ $parent_comment = Comment::newFromWikiPage( $parent_page );
+ if ( is_null( $parent_comment ) ) {
+ return;
+ } else {
+ $comment_title = $parent_comment->getCommentTitle();
+ }
+ }
+
+ $associated_page_display_title =
+ $associated_page->getTitle()->getPrefixedText();
+ if ( class_exists( 'PageProps' ) ) {
+ $associated_title = $associated_page->getTitle();
+ $values = PageProps::getInstance()->getProperties( $associated_title,
+ 'displaytitle' );
+ if ( array_key_exists( $associated_title->getArticleID(), $values ) ) {
+ $associated_page_display_title =
+ $values[$associated_title->getArticleID()];
+ }
+ }
+
+ $extra = [
+ 'comment_id' => $comment->getId(),
+ 'parent_id' => $comment->getParentId(),
+ 'comment_author_username' => $comment->getUsername(),
+ 'comment_author_display_name' => $comment->getUserDisplayNameUnlinked(),
+ 'comment_title' => $comment_title,
+ 'associated_page_display_title' => $associated_page_display_title,
+ 'comment_wikitext' => $comment->getWikitext()
+ ];
+
+ if ( !is_null( $parent_id ) ) {
+ EchoEvent::create( [
+ 'type' => 'commentstreams-reply-on-watched-page',
+ 'title' => $associated_page->getTitle(),
+ 'extra' => $extra,
+ 'agent' => $this->getUser()
+ ] );
+ EchoEvent::create( [
+ 'type' => 'commentstreams-reply-to-watched-comment',
+ 'title' => $associated_page->getTitle(),
+ 'extra' => $extra,
+ 'agent' => $this->getUser()
+ ] );
+ } else {
+ EchoEvent::create( [
+ 'type' => 'commentstreams-comment-on-watched-page',
+ 'title' => $associated_page->getTitle(),
+ 'extra' => $extra,
+ 'agent' => $this->getUser()
+ ] );
+ }
+ }
+
+ /**
+ * log action
+ * @param string $action the name of the action to be logged
+ */
+ protected function logAction( $action, $title ) {
+ $logEntry = new ManualLogEntry( 'commentstreams', $action );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $title );
+ $logid = $logEntry->insert();
+ }
+
+ /**
+ * die with a custom usage message
+ * @param string $message_name the name of the custom message
+ */
+ private function dieCustomUsageMessage( $message_name ) {
+ $error_message = wfMessage( $message_name );
+ $this->dieUsageMsg(
+ [
+ ApiMessage::create( $error_message )
+ ]
+ );
+ }
+}
diff --git a/CommentStreams/includes/ApiCSQueryComment.php b/CommentStreams/includes/ApiCSQueryComment.php
new file mode 100644
index 00000000..d0d290fb
--- /dev/null
+++ b/CommentStreams/includes/ApiCSQueryComment.php
@@ -0,0 +1,42 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSQueryComment extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ return $this->comment->getJSON();
+ }
+}
diff --git a/CommentStreams/includes/ApiCSUnwatch.php b/CommentStreams/includes/ApiCSUnwatch.php
new file mode 100644
index 00000000..85d746e3
--- /dev/null
+++ b/CommentStreams/includes/ApiCSUnwatch.php
@@ -0,0 +1,76 @@
+<?php
+/*
+ * Copyright (c) 2017 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSUnwatch extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, true );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ if ( $this->getUser()->isAnon() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-unwatch-notloggedin' );
+ }
+
+ if ( !is_null( $this->comment->getParentId() ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-unwatch-nounwatchonreply' );
+ }
+
+ $result = $this->comment->unwatch( $this->getUser() );
+ if ( !$result ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-unwatch' );
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), '' );
+ }
+
+ /**
+ * @return array examples of the use of this API module
+ */
+ public function getExamplesMessages() {
+ return [
+ 'action=' . $this->getModuleName() . '&pageid=3' =>
+ 'apihelp-' . $this->getModuleName() . '-pageid-example',
+ 'action=' . $this->getModuleName() . '&title=CommentStreams:3' =>
+ 'apihelp-' . $this->getModuleName() . '-title-example'
+ ];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF toekn
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+}
diff --git a/CommentStreams/includes/ApiCSVote.php b/CommentStreams/includes/ApiCSVote.php
new file mode 100644
index 00000000..e93021f1
--- /dev/null
+++ b/CommentStreams/includes/ApiCSVote.php
@@ -0,0 +1,93 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSVote extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, true );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ if ( $this->getUser()->isAnon() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-vote-notloggedin' );
+ }
+
+ $vote = $this->getMain()->getVal( 'vote' );
+
+ if ( !is_null( $this->comment->getParentId() ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-vote-novoteonreply' );
+ }
+
+ $result = $this->comment->vote( $vote, $this->getUser() );
+ if ( !$result ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-vote' );
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), '' );
+ }
+
+ /**
+ * @return array allowed paramters
+ */
+ public function getAllowedParams() {
+ return array_merge( parent::getAllowedParams(),
+ [
+ 'vote' =>
+ [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_REQUIRED => true
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return array examples of the use of this API module
+ */
+ public function getExamplesMessages() {
+ return [
+ 'action=' . $this->getModuleName() . '&pageid=3&vote=1' =>
+ 'apihelp-' . $this->getModuleName() . '-pageid-example',
+ 'action=' . $this->getModuleName() . '&title=CommentStreams:3&vote=-1' =>
+ 'apihelp-' . $this->getModuleName() . '-title-example'
+ ];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF toekn
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+}
diff --git a/CommentStreams/includes/ApiCSWatch.php b/CommentStreams/includes/ApiCSWatch.php
new file mode 100644
index 00000000..a1a67072
--- /dev/null
+++ b/CommentStreams/includes/ApiCSWatch.php
@@ -0,0 +1,76 @@
+<?php
+/*
+ * Copyright (c) 2017 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class ApiCSWatch extends ApiCSBase {
+
+ /**
+ * @param ApiMain $main main module
+ * @param string $action name of this module
+ */
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, true );
+ }
+
+ /**
+ * the real body of the execute function
+ *
+ * @return result of API request
+ */
+ protected function executeBody() {
+ if ( $this->getUser()->isAnon() ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-watch-notloggedin' );
+ }
+
+ if ( !is_null( $this->comment->getParentId() ) ) {
+ $this->dieCustomUsageMessage(
+ 'commentstreams-api-error-watch-nowatchonreply' );
+ }
+
+ $result = $this->comment->watch( $this->getUser() );
+ if ( !$result ) {
+ $this->dieCustomUsageMessage( 'commentstreams-api-error-watch' );
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), '' );
+ }
+
+ /**
+ * @return array examples of the use of this API module
+ */
+ public function getExamplesMessages() {
+ return [
+ 'action=' . $this->getModuleName() . '&pageid=3' =>
+ 'apihelp-' . $this->getModuleName() . '-pageid-example',
+ 'action=' . $this->getModuleName() . '&title=CommentStreams:3' =>
+ 'apihelp-' . $this->getModuleName() . '-title-example'
+ ];
+ }
+
+ /**
+ * @return string indicates that this API module requires a CSRF toekn
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+}
diff --git a/CommentStreams/includes/Comment.php b/CommentStreams/includes/Comment.php
new file mode 100644
index 00000000..84bf186f
--- /dev/null
+++ b/CommentStreams/includes/Comment.php
@@ -0,0 +1,958 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class Comment {
+
+ // wiki page object for this comment wiki page
+ private $wikipage = null;
+
+ // data for this comment has been loaded from the database
+ private $loaded = false;
+
+ // int page ID for the wikipage this comment is on
+ private $assoc_page_id;
+
+ // int page ID for the wikipage this comment is in reply to or null
+ private $parent_page_id;
+
+ // string title of comment
+ private $comment_title;
+
+ // string wikitext of comment
+ private $wikitext = null;
+
+ // string HTML of comment
+ private $html = null;
+
+ // User user object for the author of this comment
+ private $user = null;
+
+ // Avatar for author of this comment
+ private $avatar = null;
+
+ // MWTimestamp the earliest revision date for this comment
+ private $creation_timestamp = null;
+
+ // MWTimestamp the latest revision date for this comment
+ private $modification_timestamp = null;
+
+ // number of replies to this comment
+ private $num_replies = null;
+
+ // number of up votes for this comment
+ private $num_up_votes = null;
+
+ // number of dow votes for this comment
+ private $num_down_votes = null;
+
+ /**
+ * create a new Comment object from existing wiki page
+ *
+ * @param WikiPage $wikipage WikiPage object corresponding to comment page
+ * @return Comment|null the newly created comment or null if there was an
+ * error
+ */
+ public static function newFromWikiPage( $wikipage ) {
+ if ( !is_null( $wikipage ) &&
+ $wikipage->getTitle()->getNamespace() === NS_COMMENTSTREAMS ) {
+ $comment = new Comment( $wikipage );
+ if ( $wikipage->exists() ) {
+ $comment->loadFromDatabase();
+ }
+ return $comment;
+ }
+ return null;
+ }
+
+ /**
+ * create a new Comment object from values and save to database
+ * NOTE: since only head comments can contain a comment title, either
+ * $comment_title or $parent_page_id must be non null, but not both
+ *
+ * @param int $assoc_page_id page ID for the wikipage this comment is on
+ * @param int $parent_page_id page ID for the wikipage this comment is in
+ * reply to or null
+ * @param string $comment_title string title of comment
+ * @param string $wikitext the wikitext to add
+ * @param User $user the user
+ * @return Comment|null new comment object or null if there was a problem
+ * creating it
+ */
+ public static function newFromValues( $assoc_page_id, $parent_page_id,
+ $comment_title, $wikitext, $user ) {
+ if ( is_null( $comment_title ) && is_null( $parent_page_id ) ) {
+ return null;
+ }
+ if ( !is_null( $comment_title ) && !is_null( $parent_page_id ) ) {
+ return null;
+ }
+ $annotated_wikitext = self::addAnnotations( $wikitext, $comment_title,
+ $assoc_page_id );
+ $content = new WikitextContent( $annotated_wikitext );
+ $success = false;
+ while ( !$success ) {
+ $index = wfRandomString();
+ $title = Title::newFromText( (string)$index, NS_COMMENTSTREAMS );
+ if ( !$title->isDeletedQuick() && !$title->exists() ) {
+ $wikipage = new WikiPage( $title );
+ $status = $wikipage->doEditContent( $content, '',
+ EDIT_NEW | EDIT_SUPPRESS_RC , false, $user, null );
+ if ( !$status->isOK() && !$status->isGood() ) {
+ if ( $status->getMessage()->getKey() == 'edit-already-exists' ) {
+ $index = wfRandomString();
+ } else {
+ return null;
+ }
+ } else {
+ $success = true;
+ }
+ } else {
+ $index = wfRandomString();
+ }
+ }
+ $comment = new Comment( $wikipage );
+ $comment->wikitext = $wikitext;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->insert(
+ 'cs_comment_data',
+ [
+ 'page_id' => $wikipage->getId(),
+ 'assoc_page_id' => $assoc_page_id,
+ 'parent_page_id' => $parent_page_id,
+ 'comment_title' => $comment_title
+ ],
+ __METHOD__
+ );
+ if ( !$result ) {
+ return null;
+ }
+ $comment->loadFromValues( $assoc_page_id, $parent_page_id, $comment_title );
+
+ if ( is_null( $parent_page_id ) ) {
+ $comment->watch( $user );
+ } else {
+ self::watchComment( $parent_page_id, $user );
+ }
+
+ if ( defined( 'SMW_VERSION' ) ) {
+ $job = new SMWUpdateJob( $title );
+ JobQueueGroup::singleton()->push( $job );
+ }
+
+ return $comment;
+ }
+
+ /**
+ * constructor
+ *
+ * @param WikiPage $wikipage WikiPage object corresponding to comment page
+ */
+ private function __construct( $wikipage ) {
+ $this->wikipage = $wikipage;
+ }
+
+ /**
+ * load comment data from database
+ */
+ private function loadFromDatabase() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->selectRow(
+ 'cs_comment_data',
+ [ 'assoc_page_id', 'parent_page_id', 'comment_title' ],
+ [ 'page_id' => $this->getId() ],
+ __METHOD__
+ );
+ if ( $result ) {
+ $this->assoc_page_id = (integer)$result->assoc_page_id;
+ $this->parent_page_id = $result->parent_page_id;
+ if ( !is_null( $this->parent_page_id ) ) {
+ $this->parent_page_id = (integer)$this->parent_page_id;
+ }
+ $this->comment_title = $result->comment_title;
+ $this->loaded = true;
+ }
+ }
+
+ /**
+ * load comment data from values
+ *
+ * @param int $assoc_page_id page ID for the wikipage this comment is on
+ * @param int $parent_page_id page ID for the wikipage this comment is in
+ * reply to or null
+ * @param string $comment_title string title of comment
+ */
+ private function loadFromValues( $assoc_page_id, $parent_page_id,
+ $comment_title ) {
+ $this->assoc_page_id = (integer)$assoc_page_id;
+ $this->parent_page_id = $parent_page_id;
+ if ( !is_null( $this->parent_page_id ) ) {
+ $this->parent_page_id = (integer)$this->parent_page_id;
+ }
+ $this->comment_title = $comment_title;
+ $this->loaded = true;
+ }
+
+ /**
+ * @return int page ID of the comment's wikipage
+ */
+ public function getId() {
+ return $this->wikipage->getId();
+ }
+
+ /**
+ * @return WikiPage wiki page object associate with this comment page
+ */
+ public function getWikiPage() {
+ return $this->wikipage;
+ }
+
+ /**
+ * @return int page ID for the wikipage this comment is on
+ */
+ public function getAssociatedId() {
+ if ( $this->loaded === false ) {
+ $this->loadFromDatabase();
+ }
+ return $this->assoc_page_id;
+ }
+
+ /**
+ * @return int|null page ID for the wikipage this comment is in reply to or
+ * null if this comment is a discussion, not a reply
+ */
+ public function getParentId() {
+ if ( $this->loaded === false ) {
+ $this->loadFromDatabase();
+ }
+ return $this->parent_page_id;
+ }
+
+ /**
+ * @return string the title of the comment
+ */
+ public function getCommentTitle() {
+ if ( $this->loaded === false ) {
+ $this->loadFromDatabase();
+ }
+ return $this->comment_title;
+ }
+
+ /**
+ * @return string wikitext of the comment
+ */
+ public function getWikiText() {
+ if ( is_null( $this->wikitext ) ) {
+ $wikitext = ContentHandler::getContentText( $this->wikipage->getContent(
+ Revision::RAW ) );
+ $wikitext = $this->removeAnnotations( $wikitext );
+ $this->wikitext = $wikitext;
+ }
+ return $this->wikitext;
+ }
+
+ /**
+ * @return string parsed HTML of the comment
+ */
+ public function getHTML() {
+ if ( is_null( $this->html ) ) {
+ $this->getWikiText();
+ if ( !is_null( $this->wikitext ) ) {
+ $parser = new Parser;
+ $this->html = $parser->parse( $this->wikitext,
+ $this->wikipage->getTitle(), new ParserOptions )->getText();
+ }
+ }
+ return $this->html;
+ }
+
+ /**
+ * @return User the author of this comment
+ */
+ public function getUser() {
+ if ( is_null( $this->user ) ) {
+ $user_id = $this->wikipage->getOldestRevision()->getUser();
+ $this->user = User::newFromId( $user_id );
+ }
+ return $this->user;
+ }
+
+ /**
+ * @return boolean true if the last edit to this comment was not done by the
+ * original author
+ */
+ public function isLastEditModerated() {
+ $author = $this->wikipage->getOldestRevision()->getUser();
+ $lastEditor = $this->wikipage->getRevision()->getUser();
+ return $author !== $lastEditor;
+ }
+
+ /**
+ * @return string username of the author of this comment
+ */
+ public function getUsername() {
+ return $this->getUser()->getName();
+ }
+
+ /**
+ * @return string display name of the author of this comment linked to
+ * the user's user page if it exists
+ */
+ public function getUserDisplayName() {
+ return self::getDisplayNameFromUser( $this->getUser() );
+ }
+
+ /**
+ * @return string display name of the author of this comment
+ */
+ public function getUserDisplayNameUnlinked() {
+ return self::getDisplayNameFromUser( $this->getUser(), false );
+ }
+
+ /**
+ * @return string the URL of the avatar of the author of this comment
+ */
+ public function getAvatar() {
+ if ( is_null( $this->avatar ) ) {
+ if ( class_exists( 'wAvatar' ) ) { // from Extension:SocialProfile
+ $avatar = new wAvatar( $this->getUser()->getId(), 'l' );
+ $this->avatar = $GLOBALS['wgUploadPath'] . '/avatars/' .
+ $avatar->getAvatarImage();
+ } else {
+ $this->avatar = self::getAvatarFromUser( $this->getUser() );
+ }
+ }
+ return $this->avatar;
+ }
+
+ /**
+ * @return MWTimestamp the earliest revision date for this
+ */
+ public function getCreationTimestamp() {
+ if ( is_null( $this->creation_timestamp ) ) {
+ $this->creation_timestamp = MWTimestamp::getLocalInstance(
+ $this->wikipage->getTitle()->getEarliestRevTime() );
+ }
+ return $this->creation_timestamp;
+ }
+
+ /**
+ * @return MWTimestamp the earliest revision date for this
+ */
+ public function getCreationDate() {
+ if ( !is_null( $this->getCreationTimestamp() ) ) {
+ return $this->creation_timestamp->format( "M j \a\\t g:i a" );
+ }
+ return "";
+ }
+
+ /**
+ * @return MWTimestamp the latest revision date for this
+ */
+ public function getModificationTimestamp() {
+ if ( is_null( $this->modification_timestamp ) ) {
+ $title = $this->wikipage->getTitle();
+ if ( $title->getFirstRevision()->getId() === $title->getLatestRevID() ) {
+ return null;
+ }
+ $timestamp = Revision::getTimestampFromId( $title,
+ $title->getLatestRevID() );
+ $this->modification_timestamp = MWTimestamp::getLocalInstance(
+ $timestamp );
+ }
+ return $this->modification_timestamp;
+ }
+
+ /**
+ * @return MWTimestamp the earliest revision date for this
+ */
+ public function getModificationDate() {
+ if ( !is_null( $this->getModificationTimestamp() ) ) {
+ return $this->modification_timestamp->format( "M j \a\\t g:i a" );
+ }
+ return null;
+ }
+
+ /**
+ * @return int number of replies
+ */
+ public function getNumReplies() {
+ if ( is_null( $this->num_replies ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->num_replies = $dbr->selectRowCount(
+ 'cs_comment_data',
+ '*',
+ [ 'parent_page_id' => $this->getId() ],
+ __METHOD__
+ );
+ }
+ return $this->num_replies;
+ }
+
+ /**
+ * @return array get comment data in array suitable for JSON
+ */
+ public function getJSON() {
+ $json = [
+ 'commenttitle' => $this->getCommentTitle(),
+ 'username' => $this->getUsername(),
+ 'userdisplayname' => $this->getUserDisplayName(),
+ 'avatar' => $this->getAvatar(),
+ 'created' => $this->getCreationDate(),
+ 'created_timestamp' => $this->getCreationTimestamp()->format( "U" ),
+ 'modified' => $this->getModificationDate(),
+ 'moderated' => $this->isLastEditModerated() ? "moderated" : null,
+ 'wikitext' => $this->getWikiText(),
+ 'html' => $this->getHTML(),
+ 'pageid' => $this->getId(),
+ 'associatedid' => $this->getAssociatedId(),
+ 'parentid' => $this->getParentId(),
+ 'numreplies' => $this->getNumReplies(),
+ ];
+ if ( $GLOBALS['wgCommentStreamsEnableVoting'] ) {
+ $json['numupvotes'] = $this->getNumUpVotes();
+ $json['numdownvotes'] = $this->getNumDownVotes();
+ }
+ return $json;
+ }
+
+ /**
+ * get vote for user
+ *
+ * @param User $user the author of the edit
+ * @return +1 for up vote, -1 for down vote, 0 for no vote
+ */
+ public function getVote( $user ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->selectRow(
+ 'cs_votes',
+ [ 'vote' ],
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ if ( $result ) {
+ $vote = (integer)$result->vote;
+ if ( $vote > 0 ) {
+ return 1;
+ }
+ if ( $vote < 0 ) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @return int number of up votes
+ */
+ public function getNumUpVotes() {
+ if ( is_null( $this->num_up_votes ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->num_up_votes = $dbr->selectRowCount(
+ 'cs_votes',
+ '*',
+ [
+ 'page_id' => $this->getId(),
+ 'vote' => 1
+ ],
+ __METHOD__
+ );
+ }
+ return $this->num_up_votes;
+ }
+
+ /**
+ * @return int number of down votes
+ */
+ public function getNumDownVotes() {
+ if ( is_null( $this->num_down_votes ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->num_down_votes = $dbr->selectRowCount(
+ 'cs_votes',
+ '*',
+ [
+ 'page_id' => $this->getId(),
+ 'vote' => -1
+ ],
+ __METHOD__
+ );
+ }
+ return $this->num_down_votes;
+ }
+
+ /**
+ * record a vote
+ *
+ * @param vote 1 for up vote, -1 for down vote, 0 for no vote
+ * @param User $user the user voting on the comment
+ * @return database status code
+ */
+ public function vote( $vote, $user ) {
+ if ( $vote !== "-1" && $vote !== "0" && $vote !== "1" ) {
+ return false;
+ }
+ $vote = (integer)$vote;
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->selectRow(
+ 'cs_votes',
+ [ 'vote' ],
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ if ( $result ) {
+ if ( $vote === (integer)$result->vote ) {
+ return true;
+ }
+ if ( $vote === 1 || $vote === -1 ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->update(
+ 'cs_votes',
+ [ 'vote' => $vote ],
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ } else {
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->delete(
+ 'cs_votes',
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ }
+ } else {
+ if ( $vote === 0 ) {
+ return true;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->insert(
+ 'cs_votes',
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId(),
+ 'vote' => $vote
+ ],
+ __METHOD__
+ );
+ }
+ return $result;
+ }
+
+ /**
+ * watch a comment (get page ID from this comment)
+ *
+ * @param User $user the user watching the comment
+ * @return database true for OK, false for error
+ */
+ public function watch( $user ) {
+ return self::watchComment( $this->getID(), $user );
+ }
+
+ /**
+ * watch a comment (get page ID from parameter)
+ *
+ * @param $pageid the page ID of the comment to watch
+ * @param User $user the user watching the comment
+ * @return database true for OK, false for error
+ */
+ private static function watchComment( $pageid, $user ) {
+ if ( self::isWatchingComment( $pageid, $user ) ) {
+ return true;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->insert(
+ 'cs_watchlist',
+ [
+ 'page_id' => $pageid,
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ return $result;
+ }
+
+ /**
+ * unwatch a comment
+ *
+ * @param User $user the user unwatching the comment
+ * @return database true for OK, false for error
+ */
+ public function unwatch( $user ) {
+ if ( !$this->isWatching( $user ) ) {
+ return true;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->delete (
+ 'cs_watchlist',
+ [
+ 'page_id' => $this->getId(),
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ return $result;
+ }
+
+ /**
+ * Check if a particular user is watching this comment
+ *
+ * @param User $user the user watching the comment
+ * @return database true for OK, false for error
+ */
+ public function isWatching( $user ) {
+ return self::isWatchingComment( $this->getId(), $user );
+ }
+
+ /**
+ * Check if a particular user is watching a comment
+ *
+ * @param $pageid the page ID of the comment to check
+ * @param User $user the user watching the comment
+ * @return database true for OK, false for error
+ */
+ private static function isWatchingComment( $pageid, $user ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->selectRow(
+ 'cs_watchlist',
+ [ 'page_id' ],
+ [
+ 'page_id' => $pageid,
+ 'user_id' => $user->getId()
+ ],
+ __METHOD__
+ );
+ if ( $result ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get an array of watchers for this comment
+ *
+ * @return array of user IDs
+ */
+ public function getWatchers() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->select(
+ 'cs_watchlist',
+ [ 'user_id' ],
+ [ 'page_id' => $this->getId() ],
+ __METHOD__
+ );
+ $users = [];
+ foreach ( $result as $row ) {
+ $user_id = $row->user_id;
+ $user = User::newFromId( $user_id );
+ $users[$user_id] = $user;
+ }
+ return $users;
+ }
+
+ /**
+ * update comment in database
+ * NOTE: since only head comments can contain a comment title,
+ * $comment_title may only be non null if this comment has a null parent id
+ * and vice versa
+ *
+ * @param string $comment_title the new title for the comment
+ * @param string $wikitext the wikitext to add
+ * @param User $user the author of the edit
+ * @return boolean true if successful
+ */
+ public function update( $comment_title, $wikitext, $user ) {
+ if ( is_null( $comment_title ) && is_null( $this->getParentId() ) ) {
+ return false;
+ }
+ if ( !is_null( $comment_title ) && !is_null( $this->getParentId() ) ) {
+ return false;
+ }
+ $annotated_wikitext =
+ self::addAnnotations( $wikitext, $comment_title,
+ $this->getAssociatedId() );
+ $content = new WikitextContent( $annotated_wikitext );
+ $status = $this->wikipage->doEditContent( $content, '',
+ EDIT_UPDATE | EDIT_SUPPRESS_RC , false, $user, null );
+ if ( !$status->isOK() && !$status->isGood() ) {
+ return false;
+ }
+ $this->wikitext = $wikitext;
+ $this->modification_timestamp = null;
+ $this->wikipage = WikiPage::newFromID( $this->wikipage->getId() );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->update(
+ 'cs_comment_data',
+ [ 'comment_title' => $comment_title ],
+ [ 'page_id' => $this->getId() ],
+ __METHOD__
+ );
+ if ( !$result ) {
+ return false;
+ }
+ $this->comment_title = $comment_title;
+
+ return true;
+ }
+
+ /**
+ * delete comment from database
+ *
+ * @return boolean true if successful
+ */
+ public function delete() {
+ $pageid = $this->getId();
+
+ $status = $this->getWikiPage()->doDeleteArticleReal( 'comment deleted',
+ true, 0 );
+ if ( !$status->isOK() && !$status->isGood() ) {
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $result = $dbw->delete(
+ 'cs_comment_data',
+ [ 'page_id' => $pageid ],
+ __METHOD__
+ );
+ return $result;
+ }
+
+ /**
+ * add extra information to wikitext before storage
+ *
+ * @param string $wikitext the wikitext to which to add
+ * @param string $comment_title string title of comment
+ * @param int $assoc_page_id page ID for the wikipage this comment is on
+ * @return string annotated wikitext
+ */
+ public static function addAnnotations( $wikitext, $comment_title,
+ $assoc_page_id ) {
+ if ( !is_null( $comment_title ) ) {
+ $wikitext .= <<<EOT
+{{DISPLAYTITLE:
+$comment_title
+}}
+EOT;
+ }
+ return $wikitext;
+ }
+
+ /**
+ * add extra information to wikitext before storage
+ *
+ * @param string $wikitext the wikitext to which to add
+ * @return string wikitext without annotations
+ */
+ public function removeAnnotations( $wikitext ) {
+ $comment_title = $this->getCommentTitle();
+ if ( !is_null( $comment_title ) ) {
+ $strip = <<<EOT
+{{DISPLAYTITLE:
+$comment_title
+}}
+EOT;
+ $wikitext = str_replace( $strip, '', $wikitext );
+ }
+ return $wikitext;
+ }
+
+ /**
+ * get comments for the given page
+ *
+ * @param int $assoc_page_id ID of page to get comments for
+ * @return array array of comments for the given page
+ */
+ public static function getAssociatedComments( $assoc_page_id ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->select(
+ 'cs_comment_data',
+ [ 'page_id' ],
+ [ 'assoc_page_id' => $assoc_page_id ],
+ __METHOD__
+ );
+ $comments = [];
+ foreach ( $result as $row ) {
+ $page_id = $row->page_id;
+ $wikipage = WikiPage::newFromId( $page_id );
+ $comment = self::newFromWikiPage( $wikipage );
+ if ( !is_null( $comment ) ) {
+ $comments[] = $comment;
+ }
+ }
+ return $comments;
+ }
+
+ /**
+ * get replies for the given comment
+ *
+ * @param int $parent_page_id ID of page to get comments for
+ * @return array array of comments for the given page
+ */
+ public static function getReplies( $parent_page_id ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->select(
+ 'cs_comment_data',
+ [ 'page_id' ],
+ [ 'parent_page_id' => $parent_page_id ],
+ __METHOD__
+ );
+ $comments = [];
+ foreach ( $result as $row ) {
+ $page_id = $row->page_id;
+ $wikipage = WikiPage::newFromId( $page_id );
+ $comment = self::newFromWikiPage( $wikipage );
+ if ( !is_null( $comment ) ) {
+ $comments[] = $comment;
+ }
+ }
+ return $comments;
+ }
+
+ /**
+ * return the text to use to represent the user at the top of a comment
+ *
+ * @param User $user the user
+ * @param boolean $linked whether to link the display name to the user page,
+ * if it exists
+ * @return string display name for user
+ */
+ public static function getDisplayNameFromUser( $user, $linked = true ) {
+ $userpage = $user->getUserPage();
+ $displayname = null;
+ if ( !is_null( $GLOBALS['wgCommentStreamsUserRealNamePropertyName'] ) ) {
+ $displayname = self::getUserProperty( $user,
+ $GLOBALS['wgCommentStreamsUserRealNamePropertyName'] );
+ }
+ if ( is_null( $displayname ) || strlen( $displayname ) == 0 ) {
+ if ( class_exists( 'PageProps' ) ) {
+ $values = PageProps::getInstance()->getProperties( $userpage,
+ 'displaytitle' );
+ if ( array_key_exists( $userpage->getArticleID(), $values ) ) {
+ $displayname = $values[$userpage->getArticleID()];
+ }
+ }
+ }
+ if ( is_null( $displayname ) || strlen( $displayname ) == 0 ) {
+ $displayname = $user->getRealName();
+ }
+ if ( is_null( $displayname ) || strlen( $displayname ) == 0 ) {
+ $displayname = $user->getName();
+ }
+ if ( $linked && $userpage->exists() ) {
+ $displayname = Linker::link( $userpage, $displayname );
+ }
+ return $displayname;
+ }
+
+ /**
+ * return the name of the file page containing the user's avatar
+ *
+ * @param User $user the user
+ * @return string URL of avatar
+ */
+ public static function getAvatarFromUser( $user ) {
+ $avatar = null;
+ if ( !is_null( $GLOBALS['wgCommentStreamsUserAvatarPropertyName'] ) ) {
+ $avatar = self::getUserProperty( $user,
+ $GLOBALS['wgCommentStreamsUserAvatarPropertyName'] );
+ if ( !is_null( $avatar ) ) {
+ if ( gettype( $avatar ) === 'string' ) {
+ $avatar = Title::newFromText( $avatar );
+ if ( is_null( $avatar ) ) {
+ return null;
+ }
+ }
+ if ( !get_class( $avatar ) === 'Title' ) {
+ return null;
+ }
+ if ( $avatar->isKnown() && $avatar->getNamespace() === NS_FILE ) {
+ $file = wfFindFile( $avatar );
+ if ( $file ) {
+ return $file->getFullUrl();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * return the value of a property on a user page
+ *
+ * @param User $user the user
+ * @param string $propertyName the name of the property
+ * @return string|null the value of the property
+ */
+ private static function getUserProperty( $user, $propertyName ) {
+ if ( defined( 'SMW_VERSION' ) ) {
+ $userpage = $user->getUserPage();
+ if ( $userpage->exists() ) {
+ $store = \SMW\StoreFactory::getStore();
+ $subject = SMWDIWikiPage::newFromTitle( $userpage );
+ $data = $store->getSemanticData( $subject );
+ $property = SMWDIProperty::newFromUserLabel( $propertyName );
+ $values = $data->getPropertyValues( $property );
+ if ( count( $values ) > 0 ) {
+ // this property should only have one value so pick the first one
+ $value = $values[0];
+ if ( $value->getDIType() == SMWDataItem::TYPE_STRING
+ || $value->getDIType() == SMWDataItem::TYPE_BLOB ) {
+ return $value->getString();
+ } elseif ( $value->getDIType() == SMWDataItem::TYPE_WIKIPAGE ) {
+ return $value->getTitle();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Used by Echo to locate the users watching a comment being replied to.
+ * @param EchoEvent $event the Echo event
+ * @return array array mapping user id to User object
+ */
+ public static function locateUsersWatchingComment( $event ) {
+ $id = $event->getExtraParam( 'parent_id' );
+ $wikipage = WikiPage::newFromId( $id );
+ if ( !is_null( $wikipage ) ) {
+ $comment = Comment::newFromWikiPage( $wikipage );
+ if ( !is_null( $comment ) ) {
+ return $comment->getWatchers();
+ }
+ }
+ return [];
+ }
+}
diff --git a/CommentStreams/includes/CommentStreams.php b/CommentStreams/includes/CommentStreams.php
new file mode 100644
index 00000000..07d4e4a2
--- /dev/null
+++ b/CommentStreams/includes/CommentStreams.php
@@ -0,0 +1,282 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class CommentStreams {
+
+ // CommentStreams singleton instance
+ private static $instance = null;
+
+ /**
+ * create a CommentStreams singleton instance
+ *
+ * @return CommentStreams a singleton CommentStreams instance
+ */
+ public static function singleton() {
+ if ( is_null( self::$instance ) ) {
+ self::$instance = new CommentStreams();
+ }
+ return self::$instance;
+ }
+
+ // no CommentStreams flag
+ private $noCommentStreams = false;
+
+ /**
+ * disables the display of comments on the current page
+ */
+ public function disableCommentsOnPage() {
+ $this->noCommentStreams = true;
+ }
+
+ // initially collapse CommentStreams flag
+ private $initiallyCollapseCommentStreams = false;
+
+ /**
+ * makes the comments appear initially collapsed when the current page
+ * is viewed
+ */
+ public function initiallyCollapseCommentsOnPage() {
+ $this->initiallyCollapseCommentStreams = true;
+ }
+
+ /**
+ * initializes the display of comments
+ *
+ * @param OutputPage $output OutputPage object
+ */
+ public function init( $output ) {
+ if ( $this->checkDisplayComments( $output ) ) {
+ $comments = $this->getComments( $output );
+ $this->initJS( $output, $comments );
+ }
+ }
+
+ /**
+ * checks to see if comments should be displayed on this page
+ *
+ * @param OutputPage $output the OutputPage object
+ * @return boolean true if comments should be displayed on this page
+ */
+ private function checkDisplayComments( $output ) {
+ // don't display comments on this page if they are explicitly disabled
+ if ( $this->noCommentStreams ) {
+ return false;
+ }
+
+ // don't display comments on any page action other than view action
+ if ( Action::getActionName( $output->getContext() ) !== "view" ) {
+ return false;
+ }
+
+ // if $wgCommentStreamsAllowedNamespaces is not set, display comments
+ // in all content namespaces
+ $csAllowedNamespaces = $GLOBALS['wgCommentStreamsAllowedNamespaces'];
+ if ( is_null( $csAllowedNamespaces ) ) {
+ $csAllowedNamespaces = $GLOBALS['wgContentNamespaces'];
+ } elseif ( !is_array( $csAllowedNamespaces ) ) {
+ $csAllowedNamespaces = [ $csAllowedNamespaces ];
+ }
+
+ // don't display comments in a talk namespace unless:
+ // 1) $wgCommentStreamsEnableTalk is true, OR
+ // 2) the namespace is a talk namespace for a namespace in the array of
+ // allowed namespaces
+ $title = $output->getTitle();
+ $namespace = $title->getNamespace();
+ if ( $title->isTalkPage() ) {
+ $subject_namespace = MWNamespace::getSubject( $namespace );
+ if ( !$GLOBALS['wgCommentStreamsEnableTalk'] &&
+ !in_array( $subject_namespace, $csAllowedNamespaces ) ) {
+ return false;
+ }
+ } elseif ( !in_array( $namespace, $csAllowedNamespaces ) ) {
+ return false;
+ }
+
+ // don't display comments in CommentStreams namespace
+ if ( $namespace === NS_COMMENTSTREAMS ) {
+ return false;
+ }
+
+ // don't display comments on pages that do not exist
+ if ( !$title->exists() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * retrieve all comments for the current page
+ *
+ * @param OutputPage $output the OutputPage object for the current page
+ * @return Comment[] array of comments
+ */
+ private function getComments( $output ) {
+ $commentData = [];
+ $pageId = $output->getTitle()->getArticleID();
+ $allComments = Comment::getAssociatedComments( $pageId );
+ $parentComments = $this->getDiscussions( $allComments,
+ $GLOBALS['wgCommentStreamsNewestStreamsOnTop'],
+ $GLOBALS['wgCommentStreamsEnableVoting'] );
+ foreach ( $parentComments as $parentComment ) {
+ $parentJSON = $parentComment->getJSON();
+ if ( $GLOBALS['wgCommentStreamsEnableVoting'] ) {
+ $parentJSON['vote'] = $parentComment->getVote( $output->getUser() );
+ }
+ if ( class_exists( 'EchoEvent' ) ) {
+ $parentJSON['watching'] = $parentComment->isWatching( $output->getUser() );
+ }
+ $childComments = $this->getReplies( $allComments,
+ $parentComment->getId() );
+ foreach ( $childComments as $childComment ) {
+ $childJSON = $childComment->getJSON();
+ $parentJSON['children'][] = $childJSON;
+ }
+ $commentData[] = $parentJSON;
+ }
+ return $commentData;
+ }
+
+ /**
+ * initialize JavaScript
+ *
+ * @param OutputPage $output the OutputPage object
+ * @param Comment[] $comments array of comments on the current page
+ */
+ private function initJS( $output, $comments ) {
+ // determine if comments should be initially collapsed or expanded
+ // if the namespace is a talk namespace, use state of its subject namespace
+ $title = $output->getTitle();
+ $namespace = $title->getNamespace();
+ if ( $title->isTalkPage() ) {
+ $namespace = MWNamespace::getSubject( $namespace );
+ }
+
+ if ( $this->initiallyCollapseCommentStreams ) {
+ $initiallyCollapsed = true;
+ } else {
+ $initiallyCollapsed = in_array( $namespace,
+ $GLOBALS['wgCommentStreamsInitiallyCollapsedNamespaces'] );
+ }
+
+ $commentStreamsParams = [
+ 'moderatorEdit' => in_array( 'cs-moderator-edit',
+ $output->getUser()->getRights() ),
+ 'moderatorDelete' => in_array( 'cs-moderator-delete',
+ $output->getUser()->getRights() ),
+ 'moderatorFastDelete' =>
+ $GLOBALS['wgCommentStreamsModeratorFastDelete'] ? 1 : 0,
+ 'userDisplayName' =>
+ Comment::getDisplayNameFromUser( $output->getUser() ),
+ 'userAvatar' =>
+ Comment::getAvatarFromUser( $output->getUser() ),
+ 'newestStreamsOnTop' =>
+ $GLOBALS['wgCommentStreamsNewestStreamsOnTop'] ? 1 : 0,
+ 'initiallyCollapsed' => $initiallyCollapsed,
+ 'enableVoting' =>
+ $GLOBALS['wgCommentStreamsEnableVoting'] ? 1 : 0,
+ 'enableWatchlist' =>
+ class_exists( 'EchoEvent' ) ? 1 : 0,
+ 'comments' => $comments
+ ];
+ $output->addJsConfigVars( 'CommentStreams', $commentStreamsParams );
+ $output->addModules( 'ext.CommentStreams' );
+ }
+
+ /**
+ * return all discussions (top level comments) in an array of comments
+ *
+ * @param array $allComments an array of all comments on a page
+ * @param boolean $newestOnTop true if array should be sorted from newest to
+ * @return array an array of all discussions
+ * oldest
+ */
+ private function getDiscussions( $allComments, $newestOnTop, $enableVoting ) {
+ $array = array_filter(
+ $allComments, function ( $comment ) {
+ return is_null( $comment->getParentId() );
+ }
+ );
+ usort( $array, function ( $comment1, $comment2 )
+ use ( $newestOnTop, $enableVoting ) {
+ $date1 = $comment1->getCreationTimestamp()->timestamp;
+ $date2 = $comment2->getCreationTimestamp()->timestamp;
+ if ( $enableVoting ) {
+ $upvotes1 = $comment1->getNumUpVotes();
+ $downvotes1 = $comment1->getNumDownVotes();
+ $votediff1 = $upvotes1 - $downvotes1;
+ $upvotes2 = $comment2->getNumUpVotes();
+ $downvotes2 = $comment2->getNumDownVotes();
+ $votediff2 = $upvotes2 - $downvotes2;
+ if ( $votediff1 === $votediff2 ) {
+ if ( $upvotes1 === $upvotes2 ) {
+ if ( $newestOnTop ) {
+ return $date1 > $date2 ? -1 : 1;
+ } else {
+ return $date1 < $date2 ? -1 : 1;
+ }
+ } else {
+ return $upvotes1 > $upvotes2 ? -1 : 1;
+ }
+ } else {
+ return $votediff1 > $votediff2 ? -1 : 1;
+ }
+ } else {
+ if ( $newestOnTop ) {
+ return $date1 > $date2 ? -1 : 1;
+ } else {
+ return $date1 < $date2 ? -1 : 1;
+ }
+ }
+ }
+ );
+ return $array;
+ }
+
+ /**
+ * return all replies for a given discussion in an array of comments
+ *
+ * @param array $allComments an array of all comments on a page
+ * @param int $parentId the page ID of the discussion to get replies for
+ * @return array an array of replies for the given discussion
+ */
+ private function getReplies( $allComments, $parentId ) {
+ $array = array_filter(
+ $allComments, function ( $comment ) use ( $parentId ) {
+ if ( $comment->getParentId() === $parentId ) {
+ return true;
+ }
+ return false;
+ }
+ );
+ usort(
+ $array, function ( $comment1, $comment2 ) {
+ $date1 = $comment1->getCreationTimestamp()->timestamp;
+ $date2 = $comment2->getCreationTimestamp()->timestamp;
+ return $date1 < $date2 ? -1 : 1;
+ }
+ );
+ return $array;
+ }
+}
diff --git a/CommentStreams/includes/CommentStreamsAllComments.alias.php b/CommentStreams/includes/CommentStreamsAllComments.alias.php
new file mode 100644
index 00000000..76c5d1f4
--- /dev/null
+++ b/CommentStreams/includes/CommentStreamsAllComments.alias.php
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * Copyright (c) 2017 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+$specialPageAliases = [];
+
+/** English */
+$specialPageAliases['en'] = [
+ 'CommentStreamsAllComments' => [ 'AllComments' ]
+];
diff --git a/CommentStreams/includes/CommentStreamsAllComments.php b/CommentStreams/includes/CommentStreamsAllComments.php
new file mode 100644
index 00000000..9631842d
--- /dev/null
+++ b/CommentStreams/includes/CommentStreamsAllComments.php
@@ -0,0 +1,177 @@
+<?php
+
+/*
+ * Copyright (c) 2017 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class CommentStreamsAllComments extends SpecialPage {
+
+ function __construct() {
+ parent::__construct( 'CommentStreamsAllComments' );
+ }
+
+ function execute( $par ) {
+ $request = $this->getRequest();
+ $this->setHeaders();
+ $this->getOutput()->addModuleStyles( 'ext.CommentStreamsAllComments' );
+
+ $offset = $request->getText( 'offset', 0 );
+ $limit = 20;
+ $pages = self::getCommentPages( $limit + 1, $offset );
+
+ if ( !$pages->valid() ) {
+ $offset = 0;
+ $pages = self::getCommentPages( $limit + 1, $offset );
+ if ( !$pages->valid() ) {
+ $this->displayMessage(
+ wfMessage( 'commentstreams-allcomments-nocommentsfound' )
+ );
+ return;
+ }
+ }
+
+ $wikitext = '{| class="wikitable csall-wikitable"' . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-page' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-associatedpage' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-commenttitle' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-wikitext' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-author' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-lasteditor' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-created' ) . PHP_EOL;
+ $wikitext .=
+ '!' . wfMessage( 'commentstreams-allcomments-label-lastedited' ) . PHP_EOL;
+
+ $index = 0;
+ $more = false;
+ foreach ( $pages as $page ) {
+ if ( $index < $limit ) {
+ $wikipage = WikiPage::newFromId( $page->page_id );
+ $comment = Comment::newFromWikiPage( $wikipage );
+ $pagename = $comment->getWikiPage()->getTitle()->getPrefixedText() ;
+ $associatedpageid = $comment->getAssociatedId();
+ $associatedpagename =
+ WikiPage::newFromId( $associatedpageid )->getTitle()->getPrefixedText();
+ $author = $comment->getUser()->getName();
+ $lasteditor = User::newFromId( $wikipage->getRevision()->getUser() )->getName();
+ if ( $lasteditor === $author ) {
+ $lasteditor = '';
+ }
+ $wikitext .= '|-' . PHP_EOL;
+ $wikitext .= '|[[' . $pagename . ']]' . PHP_EOL;
+ $wikitext .= '|[[' . $associatedpagename . ']]' . PHP_EOL;
+ $wikitext .= '|' . $comment->getCommentTitle() . PHP_EOL;
+ $wikitext .= '|' . $comment->getWikiText() . PHP_EOL;
+ $wikitext .= '|' . $author . PHP_EOL;
+ $wikitext .= '|' . $lasteditor . PHP_EOL;
+ $wikitext .= '|' . $comment->getCreationDate() . PHP_EOL;
+ $wikitext .= '|' . $comment->getModificationDate() . PHP_EOL;
+ $index ++;
+ } else {
+ $more = true;
+ }
+ }
+
+ $wikitext .= '|}' . PHP_EOL;
+ $this->getOutput()->addWikiText( $wikitext );
+
+ if ( $offset > 0 || $more ) {
+ $this->addTableNavigation( $offset, $more, $limit, 'offset' );
+ }
+ }
+
+ private function displayMessage( $message ) {
+ $html = Html::openElement( 'p', [
+ 'class' => 'csall-message'
+ ] )
+ . $message
+ . Html::closeElement( 'p' );
+ $this->getOutput()->addHtml( $html );
+ }
+
+ private function addTableNavigation( $offset, $more, $limit, $paramname ) {
+
+ $title = Title::newFromText( 'Special:' . __CLASS__ );
+ $url = $title->getFullURL();
+
+ $html = Html::openElement( 'table', [
+ 'class' => 'csall-navigationtable'
+ ] )
+ . Html::openElement( 'tr' )
+ . Html::openElement( 'td' );
+
+ if ( $offset > 0 ) {
+ $prevurl = $url . '?' . $paramname . '=' . ( $offset - $limit );
+ $html .= Html::openElement( 'a', [
+ 'href' => $prevurl,
+ 'class' => 'csall-button'
+ ] )
+ . wfMessage( 'commentstreams-allcomments-button-previous' )
+ . Html::closeElement( 'a' );
+ }
+
+ $html .= Html::closeElement( 'td' )
+ . Html::openElement( 'td', [
+ 'style' => 'text-align:right;'
+ ] );
+
+ if ( $more ) {
+ $nexturl = $url . '?' . $paramname . '=' . ( $offset + $limit );
+ $html .= Html::openElement( 'a', [
+ 'href' => $nexturl,
+ 'class' => 'csall-button'
+ ] )
+ . wfMessage( 'commentstreams-allcomments-button-next' )
+ . Html::closeElement( 'a' );
+ }
+
+ $html .= Html::closeElement( 'td' )
+ . Html::closeElement( 'tr' )
+ . Html::closeElement( 'table' );
+ $this->getOutput()->addHtml( $html );
+ }
+
+ private static function getCommentPages( $limit, $offset ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $pages = $dbr->select(
+ 'page',
+ [
+ 'page_id'
+ ],
+ [
+ 'page_namespace' => $GLOBALS['wgCommentStreamsNamespaceIndex']
+ ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'page_latest DESC' ,
+ 'LIMIT' => $limit,
+ 'OFFSET' => $offset
+ ]
+ );
+ return $pages;
+ }
+}
diff --git a/CommentStreams/includes/CommentStreamsHooks.php b/CommentStreams/includes/CommentStreamsHooks.php
new file mode 100644
index 00000000..ab21cbd9
--- /dev/null
+++ b/CommentStreams/includes/CommentStreamsHooks.php
@@ -0,0 +1,436 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class CommentStreamsHooks {
+
+ /**
+ * Implements LoadExtensionSchemaUpdates hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates
+ * Updates database schema.
+ *
+ * @param DatabaseUpdater $updater database updater
+ * @return bool continue checking hooks
+ */
+ public static function addCommentTableToDatabase( DatabaseUpdater $updater ) {
+ $dir = $GLOBALS['wgExtensionDirectory'] . DIRECTORY_SEPARATOR .
+ 'CommentStreams' . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR;
+ $updater->addExtensionTable( 'cs_comment_data', $dir . 'commentData.sql',
+ true );
+ $updater->addExtensionTable( 'cs_votes', $dir . 'votes.sql', true );
+ $updater->addExtensionTable( 'cs_watchlist', $dir . 'watch.sql', true );
+ return true;
+ }
+
+ /**
+ * Implements CanonicalNamespaces hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/CanonicalNamespaces
+ * Adds CommentStreams namespaces.
+ *
+ * @param array &$namespaces modifiable array of namespace numbers with
+ * corresponding canonical names
+ * @return bool continue checking hooks
+ */
+ public static function addCommentStreamsNamespaces( array &$namespaces ) {
+ $namespaces[NS_COMMENTSTREAMS] = 'CommentStreams';
+ $namespaces[NS_COMMENTSTREAMS_TALK] = 'CommentStreams_Talk';
+ return true;
+ }
+
+ /**
+ * Implement MediaWikiPerformAction hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/MediaWikiPerformAction
+ * Prevents comment pages from being edited or deleted. Displays
+ * comment title and link to associated page when comment is viewed.
+ *
+ * @param OutputPage $output OutputPage object
+ * @param Article $article Article object
+ * @param Title $title Title object
+ * @param User $user User object
+ * @param WebRequest $request WebRequest object
+ * @param MediaWiki $wiki MediaWiki object
+ * @return bool continue checking hooks
+ */
+ public static function onMediaWikiPerformAction( OutputPage $output,
+ Article $article, Title $title, User $user, WebRequest $request,
+ MediaWiki $wiki ) {
+ if ( $title->getNamespace() !== NS_COMMENTSTREAMS ) {
+ return true;
+ }
+ $action = $wiki->getAction();
+ if ( $action === 'info' || $action === 'history' ) {
+ return true;
+ }
+
+ if ( $action !== 'view' ) {
+ $message =
+ wfMessage( 'commentstreams-error-prohibitedaction', $action )->text();
+ $output->addHTML( '<p class="error">' . $message . '</p>' );
+ }
+ $wikipage = new WikiPage( $title );
+ $comment = Comment::newFromWikiPage( $wikipage );
+ if ( !is_null( $comment ) ) {
+ $commentTitle = $comment->getCommentTitle();
+ if ( !is_null( $commentTitle ) ) {
+ $output->setPageTitle( $commentTitle );
+ }
+ $associatedTitle = Title::newFromId( $comment->getAssociatedId() );
+ if ( !is_null( $associatedTitle ) ) {
+ $values = [];
+ if ( class_exists( 'PageProps' ) ) {
+ $values = PageProps::getInstance()->getProperties( $associatedTitle,
+ 'displaytitle' );
+ }
+ if ( array_key_exists( $comment->getAssociatedId(), $values ) ) {
+ $displaytitle = $values[$comment->getAssociatedId()];
+ } else {
+ $displaytitle = $associatedTitle->getPrefixedText();
+ }
+ $link = Linker::link( $associatedTitle, '< ' . $displaytitle );
+ $output->setSubtitle( $link );
+ $output->addWikitext( $comment->getHTML() );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Implement MovePageIsValidMove hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/MovePageIsValidMove
+ * Prevents comment pages from being moved.
+ *
+ * @param Title $oldTitle Title object of the current (old) location
+ * @param Title $newTitle Title object of the new location
+ * @param Status $status Status object to pass error messages to
+ * @return bool continue checking hooks
+ */
+ public static function onMovePageIsValidMove( Title $oldTitle,
+ Title $newTitle, Status $status ) {
+ if ( $oldTitle->getNamespace() === NS_COMMENTSTREAMS ||
+ $newTitle->getNamespace() === NS_COMMENTSTREAMS ) {
+ $status->fatal( wfMessage( 'commentstreams-error-prohibitedaction',
+ 'move' ) );
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Implements userCan hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/userCan
+ * Ensures that only the original author can edit a comment
+ *
+ * @param Title &$title the title object in question
+ * @param User &$user the user performing the action
+ * @param string $action the action being performed
+ * @param boolean &$result true means the user is allowed, false means the
+ * user is not allowed, untouched means this hook has no opinion
+ * @return bool continue checking hooks
+ */
+ public static function userCan( Title &$title, User &$user, $action,
+ &$result ) {
+ if ( $title->getNamespace() !== NS_COMMENTSTREAMS ) {
+ return true;
+ }
+
+ $wikipage = new WikiPage( $title );
+
+ if ( !$wikipage->exists() ) {
+ return true;
+ }
+
+ if ( $user->isBlocked() ) {
+ $result = false;
+ return false;
+ }
+
+ if ( $action === 'edit' ) {
+ if ( $user->getId() === $wikipage->getOldestRevision()->getUser() ) {
+ $result = true;
+ } else {
+ $result = false;
+ }
+ return false;
+ }
+
+ if ( $action === 'cs-moderator-edit' ) {
+ if ( in_array( 'cs-moderator-edit', $user->getRights() ) ) {
+ $result = true;
+ } else {
+ $result = false;
+ }
+ return false;
+ }
+
+ if ( $action === 'cs-moderator-delete' ) {
+ if ( in_array( 'cs-moderator-delete', $user->getRights() ) ) {
+ $result = true;
+ } else {
+ $result = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Implements ParserFirstCallInit hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
+ * Adds no-comment-streams and comment-streams-initially-collapsed magic
+ * words.
+ *
+ * @param Parser $parser the parser
+ * @return bool continue checking hooks
+ */
+ public static function onParserSetup( Parser $parser ) {
+ $parser->setHook( 'no-comment-streams',
+ 'CommentStreamsHooks::hideCommentStreams' );
+ $parser->setHook( 'comment-streams-initially-collapsed',
+ 'CommentStreamsHooks::initiallyCollapseCommentStreams' );
+ return true;
+ }
+
+ /**
+ * Implements tag function, <no-comment-streams/>, which disables
+ * CommentStreams on a page.
+ *
+ * @param string $input input between the tags (ignored)
+ * @param array $args tag arguments
+ * @param Parser $parser the parser
+ * @param PPFrame $frame the parent frame
+ * @return string to replace tag with
+ */
+ public static function hideCommentStreams( $input, array $args,
+ Parser $parser, PPFrame $frame ) {
+ $parser->disableCache();
+ $cs = CommentStreams::singleton();
+ $cs->disableCommentsOnPage();
+ return "";
+ }
+
+ /**
+ * Implements tag function, <comment-streams-initially-collapsed/>, which
+ * makes CommentStreams on a page start as collapsed when the page is viewed.
+ *
+ * @param string $input input between the tags (ignored)
+ * @param array $args tag arguments
+ * @param Parser $parser the parser
+ * @param PPFrame $frame the parent frame
+ * @return string to replace tag with
+ */
+ public static function initiallyCollapseCommentStreams( $input, array $args,
+ Parser $parser, PPFrame $frame ) {
+ $parser->disableCache();
+ $cs = CommentStreams::singleton();
+ $cs->initiallyCollapseCommentsOnPage();
+ return "";
+ }
+
+ /**
+ * Implements BeforePageDisplay hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
+ * Updates database schema.
+ *
+ * @param OutputPage &$output OutputPage object
+ * @param Skin &$skin Skin object that will be used to generate the page
+ * @return bool continue checking hooks
+ */
+ public static function addCommentsAndInitializeJS( OutputPage &$output,
+ Skin &$skin ) {
+ $cs = CommentStreams::singleton();
+ $cs->init( $output );
+ return true;
+ }
+
+ /**
+ * Implements ShowSearchHitTitle hook.
+ * See https://www.mediawiki.org/wiki/Manual:Hooks/ShowSearchHitTitle
+ * Modifies search results pointing to comment pages to point to the
+ * associated content page instead.
+ *
+ * @param Title &$title title to link to
+ * @param string &$text text to use for the link
+ * @param SearchResult $result the search result
+ * @param array $terms the search terms entered
+ * @param SpecialSearch $page the SpecialSearch object
+ * @return bool continue checking hooks
+ */
+ public static function showSearchHitTitle( Title &$title, &$text,
+ SearchResult $result, array $terms, SpecialSearch $page ) {
+ $comment = Comment::newFromWikiPage( WikiPage::factory( $title ) );
+ if ( !is_null( $comment ) ) {
+ $title = Title::newFromId( $comment->getAssociatedId() );
+ }
+ return true;
+ }
+
+ /**
+ * Implements extension registration callback.
+ * See https://www.mediawiki.org/wiki/Manual:Extension_registration#Customizing_registration
+ * Defines CommentStreams namespace constants.
+ *
+ */
+ public static function onRegistration() {
+ define( 'NS_COMMENTSTREAMS', $GLOBALS['wgCommentStreamsNamespaceIndex'] );
+ define( 'NS_COMMENTSTREAMS_TALK',
+ $GLOBALS['wgCommentStreamsNamespaceIndex'] + 1 );
+ $GLOBALS['wgNamespacesToBeSearchedDefault'][NS_COMMENTSTREAMS] = true;
+ $GLOBALS['smwgNamespacesWithSemanticLinks'][NS_COMMENTSTREAMS] = true;
+ if ( !isset( $GLOBALS['wgGroupPermissions']['csmoderator']
+ ['cs-moderator-delete'] ) ) {
+ $GLOBALS['wgGroupPermissions']['csmoderator']['cs-moderator-delete'] =
+ true;
+ }
+ if ( !isset( $GLOBALS['wgGroupPermissions']['csmoderator']
+ ['cs-moderator-edit'] ) ) {
+ $GLOBALS['wgGroupPermissions']['csmoderator']['cs-moderator-edit'] =
+ false;
+ }
+ $GLOBALS['wgAvailableRights'][] = 'cs-moderator-edit';
+ $GLOBALS['wgAvailableRights'][] = 'cs-moderator-delete';
+ $GLOBALS['wgLogTypes'][] = 'commentstreams';
+ $GLOBALS['wgLogActionsHandlers']['commentstreams/*'] = 'LogFormatter';
+ }
+
+ /**
+ * Initialize extra Semantic MediaWiki properties.
+ * This won't get called unless Semantic MediaWiki is installed.
+ */
+ public static function initProperties() {
+ $pr = SMW\PropertyRegistry::getInstance();
+ $pr->registerProperty( '___CS_ASSOCPG', '_wpg', 'Comment on' );
+ $pr->registerProperty( '___CS_REPLYTO', '_wpg', 'Reply to' );
+ $pr->registerProperty( '___CS_TITLE', '_txt', 'Comment title of' );
+ $pr->registerProperty( '___CS_UPVOTES', '_num', 'Comment up votes' );
+ $pr->registerProperty( '___CS_DOWNVOTES', '_num', 'Comment down votes' );
+ $pr->registerProperty( '___CS_VOTEDIFF', '_num', 'Comment vote diff' );
+ }
+
+ /**
+ * Implements Semantic MediaWiki SMWStore::updateDataBefore callback.
+ * This won't get called unless Semantic MediaWiki is installed.
+ * If the comment has not been added to the database yet, which is indicated
+ * by a null associated page id, this function will return early, but it
+ * will be invoked again by an update job.
+ *
+ * @param SMW\Store $store semantic data store
+ * @param SMW\SemanticData $semanticData semantic data for page
+ * @return boolean true to continue
+ */
+ public static function updateData( $store, $semanticData ) {
+ $subject = $semanticData->getSubject();
+ if ( !is_null( $subject ) && !is_null( $subject->getTitle() ) &&
+ $subject->getTitle()->getNamespace() === NS_COMMENTSTREAMS ) {
+ $page_id = $subject->getTitle()->getArticleID( Title::GAID_FOR_UPDATE );
+ $wikipage = WikiPage::newFromId( $page_id );
+ $comment = Comment::newFromWikiPage( $wikipage );
+
+ if ( is_null( $comment ) ) {
+ return true;
+ }
+
+ $assoc_page_id = $comment->getAssociatedId();
+ if ( !is_null( $assoc_page_id ) ) {
+ $assoc_wikipage = WikiPage::newFromId( $assoc_page_id );
+ if ( !is_null( $assoc_wikipage ) ) {
+ $propertyDI = new SMW\DIProperty( '___CS_ASSOCPG' );
+ $dataItem =
+ SMW\DIWikiPage::newFromTitle( $assoc_wikipage->getTitle() );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ }
+ }
+
+ $parent_page_id = $comment->getParentId();
+ if ( !is_null( $parent_page_id ) ) {
+ $parent_wikipage = WikiPage::newFromId( $parent_page_id );
+ if ( !is_null( $parent_wikipage ) ) {
+ $propertyDI = new SMW\DIProperty( '___CS_REPLYTO' );
+ $dataItem =
+ SMW\DIWikiPage::newFromTitle( $parent_wikipage->getTitle() );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ }
+ }
+
+ $commentTitle = $comment->getCommentTitle();
+ if ( !is_null( $commentTitle ) ) {
+ $propertyDI = new SMW\DIProperty( '___CS_TITLE' );
+ $dataItem = new SMWDIBlob( $comment->getCommentTitle() );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ }
+
+ if ( $GLOBALS['wgCommentStreamsEnableVoting'] === true ) {
+ $upvotes = $comment->getNumUpVotes();
+ $propertyDI = new SMW\DIProperty( '___CS_UPVOTES' );
+ $dataItem = new SMWDINumber( $upvotes );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ $downvotes = $comment->getNumDownVotes();
+ $propertyDI = new SMW\DIProperty( '___CS_DOWNVOTES' );
+ $dataItem = new SMWDINumber( $downvotes );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ $votediff = $upvotes - $downvotes;
+ $propertyDI = new SMW\DIProperty( '___CS_VOTEDIFF' );
+ $dataItem = new SMWDINumber( $votediff );
+ $semanticData->addPropertyObjectValue( $propertyDI, $dataItem );
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param array &$notifications notifications
+ * @param array &$notificationCategories notification categories
+ * @param array &$icons notification icons
+ */
+ public static function onBeforeCreateEchoEvent( &$notifications,
+ &$notificationCategories, &$icons ) {
+
+ $notificationCategories['commentstreams-notification-category'] = [
+ 'priority' => 3
+ ];
+
+ $notifications['commentstreams-comment-on-watched-page'] = [
+ 'category' => 'commentstreams-notification-category',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'presentation-model' => EchoCSPresentationModel::class,
+ 'user-locators' => [ 'EchoUserLocator::locateUsersWatchingTitle' ]
+ ];
+
+ $notifications['commentstreams-reply-on-watched-page'] = [
+ 'category' => 'commentstreams-notification-category',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'presentation-model' => EchoCSPresentationModel::class,
+ 'user-locators' => [ 'EchoUserLocator::locateUsersWatchingTitle' ],
+ 'user-filters' => [ 'Comment::locateUsersWatchingComment' ]
+ ];
+
+ $notifications['commentstreams-reply-to-watched-comment'] = [
+ 'category' => 'commentstreams-notification-category',
+ 'group' => 'positive',
+ 'section' => 'alert',
+ 'presentation-model' => EchoCSPresentationModel::class,
+ 'user-locators' => [ 'Comment::locateUsersWatchingComment' ]
+ ];
+ }
+}
diff --git a/CommentStreams/includes/EchoCSPresentationModel.php b/CommentStreams/includes/EchoCSPresentationModel.php
new file mode 100644
index 00000000..c95657cb
--- /dev/null
+++ b/CommentStreams/includes/EchoCSPresentationModel.php
@@ -0,0 +1,93 @@
+<?php
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+class EchoCSPresentationModel extends EchoEventPresentationModel {
+
+ /**
+ * @return string The symbolic icon name as defined in $wgEchoNotificationIcons
+ */
+ public function getIconType() {
+ return 'chat';
+ }
+
+ /**
+ * Array of primary link details, with possibly-relative URL & label.
+ *
+ * @return array|bool Array of link data, or false for no link:
+ * ['url' => (string) url, 'label' => (string) link text (non-escaped)]
+ */
+ public function getPrimaryLink() {
+ $id = $this->event->getExtraParam( 'comment_id' );
+ return [
+ 'url' => $this->event->getTitle()->getFullURL() . '#cs-comment-' . $id,
+ 'label' => $this->msg( "notification-link-label-{$this->type}" )
+ ];
+ }
+
+ /**
+ * Get a message object and add the performer's name as
+ * a parameter. It is expected that subclasses will override
+ * this.
+ *
+ * @return Message
+ */
+ public function getHeaderMessage() {
+ $msg = wfMessage( "notification-header-{$this->type}" );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_author_display_name' ) );
+ $msg->params( $this->event->getExtraParam( 'comment_title' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'associated_page_display_title' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_author_username' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_wikitext' ) );
+ $msg->params( $this->getViewingUserForGender() );
+ return $msg;
+ }
+
+ public function getBodyMessage() {
+ $msg = wfMessage( "notification-body-{$this->type}" );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_author_display_name' ) );
+ $msg->params( $this->event->getExtraParam( 'comment_title' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'associated_page_display_title' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_author_username' ) );
+ $msg->params( $this->event->getExtraParam(
+ 'comment_wikitext' ) );
+ $msg->params( $this->getViewingUserForGender() );
+ return $msg;
+ }
+
+ /**
+ * If this function returns false, no other methods will be called
+ * on the object.
+ *
+ * @return bool
+ */
+ public function canRender() {
+ return !is_null( $this->event->getTitle() );
+ }
+}
diff --git a/CommentStreams/package.json b/CommentStreams/package.json
new file mode 100644
index 00000000..bcf5b133
--- /dev/null
+++ b/CommentStreams/package.json
@@ -0,0 +1,11 @@
+{
+ "private": true,
+ "scripts": {
+ "test": "grunt test"
+ },
+ "devDependencies": {
+ "grunt": "1.0.1",
+ "grunt-banana-checker": "0.5.0",
+ "grunt-jsonlint": "1.1.0"
+ }
+}
diff --git a/CommentStreams/resources/CommentStreams.css b/CommentStreams/resources/CommentStreams.css
new file mode 100644
index 00000000..818ef1c4
--- /dev/null
+++ b/CommentStreams/resources/CommentStreams.css
@@ -0,0 +1,191 @@
+.cs-hidden {
+ display: none;
+}
+
+#cs-comments {
+ margin-top: 10px;
+ font-family: sans-serif;
+}
+
+.cs-stream {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ overflow: hidden;
+}
+
+.cs-comment {
+ position: relative;
+}
+
+.cs-reply-comment {
+ margin-top: 10px;
+ margin-left: 30px;
+}
+
+.cs-head-comment > .cs-comment-header {
+ border-top: 1px solid #00a7d8;
+}
+
+.cs-target-comment .cs-comment-header {
+ border: 2px solid green;
+}
+
+.cs-expanded .cs-comment-header {
+ background-color: #e4f1ff;
+}
+
+.cs-collapsed .cs-comment-header {
+ background-color: #eeeeee;
+ margin-bottom: 10px;
+}
+
+.cs-reply-comment > .cs-comment-header {
+ background-color: #f5faff;
+}
+
+.cs-comment-header {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.cs-comment-header-left {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.cs-comment-header-center {
+ display: inline-block;
+ vertical-align: middle;
+ padding-left: 5px;
+}
+
+.cs-comment-header-right {
+ display: inline-block;
+ float: right;
+}
+
+.cs-avatar {
+ height: 48px;
+ padding: 5px;
+}
+
+.cs-comment-title {
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.cs-comment-author {
+ padding-right: 5px;
+ font-size: 12px;
+}
+
+.cs-comment-author a {
+ color: #00a7d8;
+ font-weight: bold;
+}
+
+.cs-comment-body {
+ margin-bottom: 5px;
+ font-size: 14px;
+}
+
+.cs-comment-details {
+ color: #555555;
+ opacity: 0.8;
+ font-size: 12px;
+ padding-right: 5px;
+ padding-left: 5px;
+}
+
+button:hover {
+ background-color: #8eddf5;
+}
+
+.cs-button {
+ background-color: transparent;
+ border: none;
+ font-family: sans-serif;
+ font-weight: bold;
+ text-decoration: none;
+ padding-left: 5px;
+ padding-right: 5px;
+ box-shadow: none;
+}
+
+.cs-button:enabled:active {
+ opacity: 0.5;
+}
+
+.cs-button:enabled:hover {
+ color: #3ccefa;
+}
+
+.cs-button:disabled {
+ color: #95a5a6;
+ opacity: 0.2;
+}
+
+.cs-moderator-button {
+ color: #ff0000;
+}
+
+.cs-moderator-button:enabled:hover {
+ color: #ff5555;
+}
+
+.cs-toggle-button {
+ border: none;
+ border-style: none;
+}
+
+.cs-vote-upcount, .cs-vote-downcount {
+ color: #555555;
+ padding-left: 3px;
+}
+
+.cs-link-button {
+ border: none;
+ border-style: none;
+}
+
+#cs-add-button, .cs-reply-button {
+ font-size: 14px;
+ padding-left: 0;
+}
+
+#cs-edit-box {
+ position: relative;
+ background-color: #e4f1ff;
+ padding: 5px 5px 5px 5px;
+ margin-bottom: 7px;
+}
+
+.cs-reply-edit-box {
+ margin-left: 30px;
+}
+
+#cs-title-edit-field {
+ border-radius: 2px;
+ border-width: 1px;
+ border-color: #00a7d8;
+ box-style: content-box;
+ -webkit-box-sizing: content-box;
+ width: 80%;
+ min-width: 400px;
+ padding: 5px 5px 5px 5px;
+ margin-bottom: 5px;
+ font-family: sans-serif;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+#cs-body-edit-field {
+ border-radius: 2px;
+ border-width: 1px;
+ border-color: #00a7d8;
+ width: 100%;
+ padding: 5px 5px 5px 5px;
+ margin-bottom: 5px;
+ font-family: sans-serif;
+ font-size: 14px;
+}
diff --git a/CommentStreams/resources/CommentStreams.js b/CommentStreams/resources/CommentStreams.js
new file mode 100644
index 00000000..bd2fb3d4
--- /dev/null
+++ b/CommentStreams/resources/CommentStreams.js
@@ -0,0 +1,1296 @@
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+var commentstreams_controller = ( function( mw, $ ) {
+ 'use strict';
+
+ return {
+ baseUrl: null,
+ imagepath: null,
+ targetComment: null,
+ isLoggedIn: false,
+ moderatorEdit: false,
+ moderatorDelete: false,
+ moderatorFastDelete: false,
+ userDisplayName: null,
+ newestStreamsOnTop: false,
+ initiallyCollapsed: false,
+ enableVoting: false,
+ enableWatchlist: false,
+ comments: [],
+ spinnerOptions: {
+ lines: 11, // The number of lines to draw
+ length: 8, // The length of each line
+ width: 4, // The line thickness
+ radius: 8, // The radius of the inner circle
+ corners: 1, // Corner roundness (0..1)
+ rotate: 0, // The rotation offset
+ direction: 1, // 1: clockwise, -1: counterclockwise
+ color: '#000', // #rgb or #rrggbb or array of colors
+ speed: 1, // Rounds per second
+ trail: 60, // ƒfterglow percentage
+ shadow: false, // Whether to render a shadow
+ hwaccel: false, // Whether to use hardware acceleration
+ className: 'spinner', // The CSS class to assign to the spinner
+ zIndex: 2e9, // The z-index (defaults to 2000000000)
+ top: '50%', // Top position relative to parent
+ left: '50%' // Left position relative to parent
+ },
+ initialize: function() {
+ var self = this;
+ this.baseUrl = window.location.href.split(/[?#]/)[0];
+ this.imagepath = mw.config.get( 'wgExtensionAssetsPath' ) +
+ '/CommentStreams/images/';
+ if ( window.location.hash ) {
+ var hash = window.location.hash.substring( 1 );
+ var queryIndex = hash.indexOf( '?' );
+ if ( queryIndex !== -1 ) {
+ hash = hash.substring( 0, queryIndex );
+ }
+ this.targetComment = hash;
+ }
+ this.isLoggedIn = mw.config.get( 'wgUserName' ) !== null;
+ var config = mw.config.get( 'CommentStreams' );
+ this.moderatorEdit = config.moderatorEdit;
+ this.moderatorDelete = config.moderatorDelete;
+ this.moderatorFastDelete = this.moderatorDelete ?
+ config.moderatorFastDelete : false;
+ this.userDisplayName = config.userDisplayName;
+ this.newestStreamsOnTop = config.newestStreamsOnTop;
+ this.initiallyCollapsed = config.initiallyCollapsed;
+ this.enableVoting = config.enableVoting;
+ this.enableWatchlist = config.enableWatchlist;
+ this.comments = config.comments;
+ this.setupDivs();
+ this.addInitialComments();
+ if ( this.targetComment ) {
+ this.scrollToAnchor( this.targetComment );
+ }
+ },
+ scrollToAnchor: function( id ){
+ var element = $( '#' + id );
+ if ( element.length ) {
+ $('html,body').animate( {scrollTop: element.offset().top},'slow');
+ }
+ },
+ setupDivs: function() {
+ var self = this;
+
+ var mainDiv = $( '<div>' ).attr( 'id', 'cs-comments' );
+
+ var headerDiv = $( '<div> ').attr( 'id', 'cs-header');
+ mainDiv.append( headerDiv );
+
+ var footerDiv = $( '<div> ').attr( 'id', 'cs-footer');
+ mainDiv.append( footerDiv );
+
+ if ( this.isLoggedIn ) {
+ var addButton = $( '<button>' )
+ .attr( {
+ type: 'button',
+ id: 'cs-add-button'
+ } )
+ .addClass( 'cs-button' );
+ var addimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-add' ),
+ src: this.imagepath + 'comment_add.png'
+ } );
+ addButton.append( addimage );
+
+ if ( this.newestStreamsOnTop ) {
+ headerDiv.append( addButton );
+ } else {
+ footerDiv.append( addButton );
+ }
+
+ addButton.click( function() {
+ self.showNewCommentStreamBox();
+ } );
+ }
+
+ mainDiv.insertAfter( '#catlinks' );
+ },
+ addInitialComments: function() {
+ var self = this;
+ var parentIndex;
+ for ( parentIndex in this.comments ) {
+ var parentComment = this.comments[ parentIndex ];
+ var commenthtml = this.formatComment( parentComment );
+ var location = $( commenthtml )
+ .insertBefore( '#cs-footer' );
+ var childIndex;
+ for ( childIndex in parentComment.children ) {
+ var childComment = parentComment.children[ childIndex ];
+ commenthtml = this.formatComment( childComment );
+ $( commenthtml ).insertBefore(
+ $( location ).find( '.cs-stream-footer' ) );
+ }
+ }
+
+ if ( this.initiallyCollapsed ) {
+ $( '.cs-stream' ).each( function() {
+ self.collapseStream( $( this ), $( this )
+ .find( '.cs-toggle-button' ) );
+ } );
+ }
+ },
+ collapseStream: function( stream, button ) {
+ stream.find( '.cs-reply-comment' ).addClass( 'cs-hidden' );
+ stream.find( '.cs-head-comment .cs-comment-body' ).addClass( 'cs-hidden' );
+ stream.find( '.cs-stream-footer .cs-reply-button' ).addClass( 'cs-hidden' );
+ $( stream ).addClass( 'cs-collapsed' );
+ $( stream ).removeClass( 'cs-expanded' );
+ $( button ).find( 'img' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-expand' ),
+ src: this.imagepath + 'expand.png'
+ } );
+ },
+ expandStream: function( stream, button ) {
+ stream.find( '.cs-reply-comment' ).removeClass( 'cs-hidden' );
+ stream.find( '.cs-head-comment .cs-comment-body' ).removeClass( 'cs-hidden' );
+ stream.find( '.cs-stream-footer .cs-reply-button' ).removeClass( 'cs-hidden' );
+ $( stream ).addClass( 'cs-expanded' );
+ $( stream ).removeClass( 'cs-collapsed' );
+ $( button ).find( 'img' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-collapse' ),
+ src: this.imagepath + 'collapse.png'
+ } );
+ },
+ disableAllButtons: function() {
+ $( '.cs-edit-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-reply-button' ).attr( 'disabled', 'disabled' );
+ $( '#cs-add-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-delete-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-toggle-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-link-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-vote-button' ).attr( 'disabled', 'disabled' );
+ $( '.cs-watch-button' ).attr( 'disabled', 'disabled' );
+ },
+ enableAllButtons: function() {
+ $( '.cs-edit-button' ).attr( 'disabled', false );
+ $( '.cs-reply-button' ).attr( 'disabled', false );
+ $( '#cs-add-button' ).attr( 'disabled', false );
+ $( '.cs-delete-button' ).attr( 'disabled', false );
+ $( '.cs-toggle-button' ).attr( 'disabled', false );
+ $( '.cs-link-button' ).attr( 'disabled', false );
+ $( '.cs-vote-button' ).attr( 'disabled', false );
+ $( '.cs-watch-button' ).attr( 'disabled', false );
+ },
+ formatComment: function( commentData ) {
+ var self = this;
+ var comment = this.formatCommentInner( commentData );
+
+ if ( commentData.parentid === null ) {
+ comment = $( '<div>' )
+ .addClass( 'cs-stream' )
+ .addClass( 'cs-expanded' )
+ .attr( 'data-created-timestamp', commentData.created_timestamp )
+ .append( comment );
+
+ var streamFooter = $( '<div>' )
+ .addClass( 'cs-stream-footer' );
+ comment.append( streamFooter );
+
+ if ( this.isLoggedIn ) {
+ var replyButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-reply-button' )
+ .attr( {
+ type: 'button',
+ 'data-stream-id': commentData.pageid
+ } );
+ var replyimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-reply' ),
+ src: this.imagepath + 'comment_reply.png'
+ } );
+ replyButton.append( replyimage );
+ streamFooter.append( replyButton );
+ replyButton.click( function() {
+ var pageId = $( this ).attr( 'data-stream-id' );
+ self.showNewReplyBox( $( this ), pageId );
+ } );
+ }
+ }
+
+ return comment;
+ },
+ formatCommentInner: function( commentData ) {
+ var self = this;
+ var commentHeader = $( '<div>' )
+ .addClass( 'cs-comment-header' );
+
+ var leftDiv = $( '<div>' )
+ .addClass( 'cs-comment-header-left' );
+ if ( commentData.avatar !== null && commentData.avatar.length > 0 ) {
+ var avatar = $( '<img>' )
+ .addClass( 'cs-avatar' )
+ .attr( 'src', commentData.avatar );
+ leftDiv.append( avatar );
+ }
+ commentHeader.append( leftDiv );
+
+ var centerDiv = $( '<div>' )
+ .addClass( 'cs-comment-header-center' );
+
+ if ( commentData.parentid === null ) {
+ var title = $( '<div>' )
+ .addClass( 'cs-comment-title' )
+ .text( commentData.commenttitle );
+ centerDiv.append( title );
+ }
+
+ var author = $( '<span>' )
+ .addClass( 'cs-comment-author' )
+ .html( commentData.userdisplayname );
+ centerDiv.append( author );
+
+ var created = $( '<span>' )
+ .addClass( 'cs-comment-details' )
+ .text( mw.message( 'commentstreams-datetext-postedon' ) +
+ ' ' + commentData.created );
+ centerDiv.append( this.createDivider() );
+ centerDiv.append( created );
+
+ if ( commentData.modified !== null ) {
+ var text = mw.message( 'commentstreams-datetext-lasteditedon' ) +
+ ' ' + commentData.modified;
+ if ( commentData.moderated ) {
+ text += ' (' + mw.message( 'commentstreams-datetext-moderated' ) +
+ ')';
+ }
+ var modified = $( '<span>' )
+ .addClass( 'cs-comment-details' )
+ .text( text );
+ centerDiv.append( this.createDivider() );
+ centerDiv.append( modified );
+ }
+
+ var divider = this.createDivider();
+ centerDiv.append( divider );
+
+ if ( this.canEdit( commentData ) ) {
+ centerDiv.append( this.createEditButton( commentData.username) );
+ }
+
+ if ( this.canDelete( commentData ) ) {
+ centerDiv.append( this.createDeleteButton( commentData.username) );
+ }
+
+ centerDiv.append( this.createPermalinkButton( commentData.pageid ) );
+
+ commentHeader.append( centerDiv );
+
+ var rightDiv = $( '<div>' )
+ .addClass( 'cs-comment-header-right' );
+
+ if ( commentData.parentid === null && this.enableWatchlist &&
+ !mw.user.isAnon() ) {
+ rightDiv.append( this.createWatchButton( commentData ) );
+ }
+
+ if ( commentData.parentid === null && this.enableVoting ) {
+ rightDiv.append( this.createVotingButtons( commentData ) );
+ }
+
+ if ( commentData.parentid === null ) {
+ var collapseButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-toggle-button' )
+ .attr( 'type', 'button' );
+ var collapseimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-collapse' ),
+ src: this.imagepath + 'collapse.png'
+ } );
+ collapseButton.append( collapseimage );
+ rightDiv.append( collapseButton );
+ collapseButton.click( function() {
+ var stream = $( this ).closest( '.cs-stream' );
+ if ( stream.hasClass( 'cs-expanded' ) ) {
+ self.collapseStream( stream, this );
+ } else {
+ self.expandStream( stream, this );
+ }
+ } );
+ }
+
+ commentHeader.append( rightDiv );
+
+ var commentBody = $( '<div>' )
+ .addClass( 'cs-comment-body' )
+ .html( commentData.html );
+ var commentFooter = $( '<div>' )
+ .addClass( 'cs-comment-footer' );
+
+ var commentClass;
+ if ( commentData.parentid !== null ) {
+ commentClass = 'cs-reply-comment';
+ } else {
+ commentClass = 'cs-head-comment';
+ }
+ var id = 'cs-comment-' + commentData.pageid;
+ var comment = $( '<div>' )
+ .addClass( 'cs-comment' )
+ .addClass( commentClass )
+ .attr( {
+ 'id': id,
+ 'data-id': commentData.pageid
+ } );
+ if ( this.targetComment === id ) {
+ comment
+ .addClass( 'cs-target-comment' );
+ }
+ comment
+ .append( [ commentHeader, commentBody, commentFooter ] );
+
+ return comment;
+ },
+ showUrlDialog: function ( id ) {
+ var instructions =
+ mw.message( 'commentstreams-urldialog-instructions' ).text();
+ var textInput = new OO.ui.TextInputWidget( {
+ value: this.baseUrl + '#' + id
+ } );
+ function UrlDialog( config ) {
+ UrlDialog.super.call( this, config );
+ }
+ OO.inheritClass( UrlDialog, OO.ui.Dialog );
+ UrlDialog.static.name = 'urlDialog';
+ UrlDialog.static.title = 'Simple dialog';
+ UrlDialog.prototype.initialize = function () {
+ UrlDialog.super.prototype.initialize.call( this );
+ this.content =
+ new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ this.content.$element.append( '<p>' + instructions + '</p>' );
+ this.content.$element.append( textInput.$element );
+ this.$body.append( this.content.$element );
+ };
+ UrlDialog.prototype.getBodyHeight = function () {
+ return this.content.$element.outerHeight( true );
+ };
+ var urlDialog = new UrlDialog( {
+ size: 'medium'
+ } );
+ var windowManager = new OO.ui.WindowManager();
+ $( 'body' ).append( windowManager.$element );
+ windowManager.addWindows( [ urlDialog ] );
+ windowManager.openWindow( urlDialog );
+ textInput.select();
+ },
+ createEditButton: function( username ) {
+ var self = this;
+ var editButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-edit-button' )
+ .attr( 'type', 'button' );
+ var editimage = $( '<img>' );
+ if ( mw.user.getName() !== username ) {
+ editimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-moderator-edit' ),
+ src: this.imagepath + 'comment_moderator_edit.png'
+ } );
+ editButton
+ .addClass( 'cs-moderator-button' )
+ } else {
+ editimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-edit' ),
+ src: this.imagepath + 'comment_edit.png'
+ } );
+ }
+ editButton.append( editimage );
+ editButton.click( function() {
+ var comment = $( this ).closest( '.cs-comment' );
+ var pageId = $( comment ).attr( 'data-id' );
+ self.editComment( $( comment ), pageId );
+ } );
+ return editButton;
+ },
+ createDeleteButton: function( username ) {
+ var self = this;
+ var deleteButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-delete-button' )
+ .attr( 'type', 'button' );
+ var deleteimage = $( '<img>' );
+ if ( mw.user.getName() !== username ) {
+ deleteimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-moderator-delete' ),
+ src: this.imagepath + 'comment_moderator_delete.png'
+ } );
+ deleteButton
+ .addClass( 'cs-moderator-button' )
+ } else {
+ deleteimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-delete' ),
+ src: this.imagepath + 'comment_delete.png'
+ } );
+ }
+ deleteButton.append( deleteimage );
+ deleteButton.click( function() {
+ var comment = $( this ).closest( '.cs-comment' );
+ var pageId = $( comment ).attr( 'data-id' );
+ self.deleteComment( $( comment ), pageId );
+ } );
+ return deleteButton;
+ },
+ createPermalinkButton( pageid ) {
+ var self = this;
+ var id = 'cs-comment-' + pageid;
+ var permalinkButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-link-button' )
+ .click( function() {
+ $( '.cs-target-comment' )
+ .removeClass( 'cs-target-comment' );
+ self.scrollToAnchor( id )
+ var comment = $( this ).closest( '.cs-comment' );
+ comment
+ .addClass( 'cs-target-comment' );
+ self.showUrlDialog( id );
+ window.location.hash = '#' + id;
+ } );
+ var permalinkimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-permalink' ),
+ src: this.imagepath + 'link.png'
+ } );
+ permalinkButton.append( permalinkimage );
+ return permalinkButton;
+ },
+ createWatchButton( commentData ) {
+ var self = this;
+ var watchButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-watch-button' )
+ .click( function() {
+ self.watch( $( this ), commentData.pageid );
+ } );
+ var watchimage = $( '<img>' )
+ .addClass( 'cs-watch-image' );
+ if ( commentData.watching ) {
+ watchimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-unwatch' ),
+ src: this.imagepath + 'watching.png'
+ } )
+ .addClass( 'cs-watch-watching' );
+ } else {
+ watchimage
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-watch' ),
+ src: this.imagepath + 'notwatching.png'
+ } )
+ }
+ watchButton.append( watchimage );
+ return watchButton;
+ },
+ createVotingButtons( commentData ) {
+ var self = this;
+
+ var upButton;
+ if ( mw.user.isAnon() ) {
+ upButton = $( '<span>' )
+ .addClass( 'cs-button' );
+ } else {
+ upButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-vote-button' )
+ .click( function() {
+ self.vote( $( this ), commentData.pageid, true,
+ commentData.created_timestamp );
+ } );
+ }
+ var upimage = $( '<img>' )
+ .attr( 'title', mw.message( 'commentstreams-buttontooltip-upvote' ) )
+ .addClass( 'cs-vote-upimage' );
+ if ( commentData.vote > 0 ) {
+ upimage.attr( 'src', this.imagepath + 'upvote-enabled.png' );
+ upimage.addClass( 'cs-vote-enabled' );
+ } else {
+ upimage.attr( 'src', this.imagepath + 'upvote-disabled.png' );
+ }
+ var upcountspan = $( '<span>' )
+ .addClass( 'cs-vote-upcount' )
+ .text( commentData.numupvotes );
+ upButton.append( upimage );
+ upButton.append( upcountspan );
+
+ var downButton;
+ if ( mw.user.isAnon() ) {
+ downButton = $( '<span>' )
+ .addClass( 'cs-button' );
+ } else {
+ downButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-vote-button' )
+ .click( function() {
+ self.vote( $( this ), commentData.pageid, false,
+ commentData.created_timestamp );
+ } );
+ }
+ var downimage = $( '<img>' )
+ .attr( 'title', mw.message( 'commentstreams-buttontooltip-downvote' ) )
+ .addClass( 'cs-vote-downimage' );
+ if ( commentData.vote < 0 ) {
+ downimage.attr( 'src', this.imagepath + 'downvote-enabled.png' );
+ downimage.addClass( 'cs-vote-enabled' );
+ } else {
+ downimage.attr( 'src', this.imagepath + 'downvote-disabled.png' );
+ }
+ var downcountspan = $( '<span>' )
+ .addClass( 'cs-vote-downcount' )
+ .text( commentData.numdownvotes );
+ downButton.append( downimage );
+ downButton.append( downcountspan );
+
+ var votingSpan = $( '<span>' )
+ .addClass( 'cs-voting-span' );
+ votingSpan.append( upButton );
+ votingSpan.append( downButton );
+ return votingSpan;
+ },
+ vote: function( button, pageid, up, created_timestamp ) {
+
+ var self = this;
+ var votespan = button.closest( '.cs-voting-span' );
+ var upcountspan = votespan.find( '.cs-vote-upcount' );
+ var upcount = parseInt(upcountspan.text());
+ var upimage = votespan.find( '.cs-vote-upimage' );
+ var downcountspan = votespan.find( '.cs-vote-downcount' );
+ var downcount = parseInt(downcountspan.text());
+ var downimage = votespan.find( '.cs-vote-downimage' );
+
+ var newvote;
+ var oldvote;
+ if ( up ) {
+ if ( upimage.hasClass( 'cs-vote-enabled' ) ) {
+ newvote = 0;
+ oldvote = 1;
+ } else {
+ newvote = 1;
+ if ( downimage.hasClass( 'cs-vote-enabled' ) ) {
+ oldvote = -1;
+ } else {
+ oldvote = 0;
+ }
+ }
+ } else {
+ if ( downimage.hasClass( 'cs-vote-enabled' ) ) {
+ newvote = 0;
+ oldvote = -1;
+ } else {
+ newvote = -1;
+ if ( upimage.hasClass( 'cs-vote-enabled' ) ) {
+ oldvote = 1;
+ } else {
+ oldvote = 0;
+ }
+ }
+ }
+
+ var comment = button.closest( '.cs-comment' );
+ this.disableAllButtons();
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( comment.attr( 'id' ) ) );
+ CommentStreamsQuerier.vote( pageid, newvote, function( result ) {
+ $( '.spinner' ).remove();
+ if ( result.error === undefined ) {
+ if ( up ) {
+ if ( upimage.hasClass( 'cs-vote-enabled' ) ) {
+ upimage.attr( 'src', self.imagepath + 'upvote-disabled.png' );
+ upimage.removeClass( 'cs-vote-enabled' );
+ upcount = upcount - 1;
+ upcountspan.text( upcount );
+ } else {
+ upimage.attr( 'src', self.imagepath + 'upvote-enabled.png' );
+ upimage.addClass( 'cs-vote-enabled' );
+ upcount = upcount + 1;
+ upcountspan.text( upcount );
+ if ( downimage.hasClass( 'cs-vote-enabled' ) ) {
+ downimage.attr( 'src', self.imagepath + 'downvote-disabled.png' );
+ downimage.removeClass( 'cs-vote-enabled' );
+ downcount = downcount - 1;
+ downcountspan.text( downcount );
+ }
+ }
+ } else {
+ if ( downimage.hasClass( 'cs-vote-enabled' ) ) {
+ downimage.attr( 'src', self.imagepath + 'downvote-disabled.png' );
+ downimage.removeClass( 'cs-vote-enabled' );
+ downcount = downcount - 1;
+ downcountspan.text( downcount );
+ } else {
+ downimage.attr( 'src', self.imagepath + 'downvote-enabled.png' );
+ downimage.addClass( 'cs-vote-enabled' );
+ downcount = downcount + 1;
+ downcountspan.text( downcount );
+ if ( upimage.hasClass( 'cs-vote-enabled' ) ) {
+ upimage.attr( 'src', self.imagepath + 'upvote-disabled.png' );
+ upimage.removeClass( 'cs-vote-enabled' );
+ upcount = upcount - 1;
+ upcountspan.text( upcount );
+ }
+ }
+ }
+ var votediff = upcount - downcount;
+ var stream = comment.closest( '.cs-stream' );
+ self.adjustCommentOrder( stream, votediff, upcount,
+ created_timestamp );
+ } else {
+ self.reportError( result.error );
+ self.enableAllButtons();
+ }
+ } );
+ },
+ watch: function( button, pageid ) {
+ var self = this;
+ var image = button.find( '.cs-watch-image');
+ var watchaction = !image.hasClass( 'cs-watch-watching' );
+ var comment = button.closest( '.cs-comment' );
+ this.disableAllButtons();
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( comment.attr( 'id' ) ) );
+ CommentStreamsQuerier.watch( pageid, watchaction, function( result ) {
+ $( '.spinner' ).remove();
+ if ( result.error === undefined ) {
+ if ( watchaction ) {
+ image
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-unwatch' ),
+ src: self.imagepath + 'watching.png'
+ } )
+ .addClass( 'cs-watch-watching' );
+ } else {
+ image
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-watch' ),
+ src: self.imagepath + 'notwatching.png'
+ } )
+ .removeClass( 'cs-watch-watching' );
+ }
+ } else {
+ self.reportError( result.error );
+ }
+ self.enableAllButtons();
+ } );
+ },
+ adjustCommentOrder: function( stream, votediff, upcount,
+ created_timestamp ) {
+ var nextSiblings = stream.nextAll( '.cs-stream' );
+ var first = true;
+ var index;
+ for ( index = 0; index < nextSiblings.length; index++ ) {
+ var sibling = nextSiblings[index];
+ var nextupcountspan =
+ $( sibling ).find( '.cs-vote-upcount' );
+ var nextupcount = parseInt(nextupcountspan.text());
+ var nextdowncountspan =
+ $( sibling ).find( '.cs-vote-downcount' );
+ var nextdowncount = parseInt(nextdowncountspan.text());
+ var nextvotediff = nextupcount - nextdowncount;
+ if ( nextvotediff > votediff ) {
+ // keeping looking
+ } else if ( nextvotediff === votediff ) {
+ if ( nextupcount > upcount ) {
+ // keeping looking
+ } else if ( nextupcount === upcount ) {
+ var nextcreated_timestamp =
+ $( sibling ).attr( 'data-created-timestamp' );
+ if ( this.newestStreamsOnTop ) {
+ if ( nextcreated_timestamp > created_timestamp ) {
+ // keeping looking
+ } else if ( first ) {
+ // check previous siblings
+ break;
+ } else {
+ this.moveComment( stream, true, $( sibling ) );
+ return;
+ }
+ } else if ( nextcreated_timestamp < created_timestamp ) {
+ // keep looking
+ } else if ( first ) {
+ // check previous siblings
+ break;
+ } else {
+ this.moveComment( stream, true, $( sibling ) );
+ return;
+ }
+ } else if ( first ) {
+ // check previous siblings
+ break;
+ } else {
+ this.moveComment( stream, true, $( sibling ) );
+ return;
+ }
+ } else if ( first ) {
+ // check previous siblings
+ break;
+ } else {
+ this.moveComment( stream, true, $( sibling ) );
+ return;
+ }
+ first = false;
+ }
+ if ( !first ) {
+ this.moveComment( stream, false,
+ $( nextSiblings[nextSiblings.length - 1] ) );
+ return;
+ }
+ var prevSiblings = stream.prevAll( '.cs-stream' );
+ first = true;
+ for ( index = 0; index < prevSiblings.length; index++ ) {
+ var sibling = prevSiblings[index];
+ var prevupcountspan =
+ $( sibling ).find( '.cs-vote-upcount' );
+ var prevupcount = parseInt(prevupcountspan.text());
+ var prevdowncountspan =
+ $( sibling ).find( '.cs-vote-downcount' );
+ var prevdowncount = parseInt(prevdowncountspan.text());
+ var prevvotediff = prevupcount - prevdowncount;
+ if ( prevvotediff < votediff ) {
+ // keeping looking
+ } else if ( prevvotediff === votediff ) {
+ if ( prevupcount < upcount ) {
+ // keeping looking
+ } else if ( prevupcount === upcount ) {
+ var prevcreated_timestamp =
+ $( sibling ).attr( 'data-created-timestamp' );
+ if ( this.newestStreamsOnTop ) {
+ if ( prevcreated_timestamp < created_timestamp ) {
+ // keeping looking
+ } else if ( first ) {
+ // done
+ break;
+ } else {
+ this.moveComment( stream, false, $( sibling ) );
+ return;
+ }
+ } else if ( prevcreated_timestamp > created_timestamp ) {
+ // keeping looking
+ } else if ( first ) {
+ // done
+ break;
+ } else {
+ this.moveComment( stream, false, $( sibling ) );
+ return;
+ }
+ } else if ( first ) {
+ // done
+ break;
+ } else {
+ this.moveComment( stream, false, $( sibling ) );
+ return;
+ }
+ } else if ( first ) {
+ // done
+ break;
+ } else {
+ this.moveComment( stream, false, $( sibling ) );
+ return;
+ }
+ first = false;
+ }
+ if ( !first ) {
+ this.moveComment( stream, true,
+ $( prevSiblings[prevSiblings.length - 1] ) );
+ return;
+ }
+ // otherwise, the comment was in the correct place already
+ this.enableAllButtons();
+ },
+ moveComment: function( stream, before, location ) {
+ var self = this;
+ stream.slideUp( 1000, function() {
+ stream.detach();
+ stream.hide();
+ if ( before ) {
+ stream.insertBefore( location );
+ } else {
+ stream.insertAfter( location );
+ }
+ stream.slideDown( 1000, function() {
+ self.enableAllButtons();
+ var id = $ (this ).find( '.cs-head-comment:first' ).attr( 'id' );
+ self.scrollToAnchor( id );
+ } );
+ } );
+ },
+ createDivider: function() {
+ return $( '<span>' )
+ .addClass( 'cs-comment-details' )
+ .text('|');
+ },
+ formatEditBox: function( is_stream ) {
+ var commentBox = $( '<div>' )
+ .addClass( 'cs-edit-box' )
+ .attr( 'id', 'cs-edit-box' );
+
+ if ( is_stream ) {
+ var titleField = $( '<input>' )
+ .attr( {
+ 'id': 'cs-title-edit-field',
+ 'type': 'text',
+ 'placeholder': mw.message( 'commentstreams-title-field-placeholder' )
+ } );
+ commentBox.append( titleField );
+ } else {
+ commentBox.addClass( 'cs-reply-edit-box' );
+ }
+
+ var bodyField = $( '<textarea>' )
+ .attr( {
+ 'id': 'cs-body-edit-field',
+ 'rows': 10,
+ 'placeholder': mw.message( 'commentstreams-body-field-placeholder' )
+ } );
+ commentBox.append( bodyField );
+
+ var submitButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-submit-button' )
+ .attr( {
+ 'id': 'cs-submit-button',
+ 'type': 'button'
+ } );
+ var submitimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-submit' ),
+ src: this.imagepath + 'submit.png'
+ } );
+ submitButton.append( submitimage );
+
+ commentBox.append( submitButton );
+
+ var cancelButton = $( '<button>' )
+ .addClass( 'cs-button' )
+ .addClass( 'cs-cancel-button' )
+ .attr( {
+ 'id': 'cs-cancel-button',
+ 'type': 'button'
+ } );
+ var cancelimage = $( '<img>' )
+ .attr( {
+ title: mw.message( 'commentstreams-buttontooltip-cancel' ),
+ src: this.imagepath + 'cancel.png'
+ } );
+ cancelButton.append( cancelimage );
+
+ commentBox.append( cancelButton );
+
+ return commentBox;
+ },
+ showNewCommentStreamBox: function() {
+ var self = this;
+ var editBox = this.formatEditBox( true );
+ if ( this.newestStreamsOnTop ) {
+ $( '#cs-header' ).append( editBox );
+ $( '#cs-edit-box' )
+ .hide()
+ .slideDown();
+ } else {
+ $( '#cs-footer' ).prepend( editBox );
+ $( '#cs-edit-box' )
+ .hide()
+ .slideDown();
+ }
+ $( '#cs-submit-button' ).click( function() {
+ self.postComment( null );
+ } );
+ $( '#cs-cancel-button' ).click( function() {
+ self.hideEditBox( true );
+ } );
+ this.disableAllButtons();
+ var titleField = $( '#cs-title-edit-field' );
+ if ( titleField !== null ) {
+ titleField.focus();
+ }
+ },
+ showNewReplyBox: function( element, topCommentId ) {
+ var self = this;
+ var editBox = this.formatEditBox( false );
+ $( editBox )
+ .insertBefore( element.closest( '.cs-stream-footer' ) )
+ .hide()
+ .slideDown();
+
+ $( '#cs-submit-button' ).click( function() {
+ self.postComment( topCommentId );
+ } );
+ $( '#cs-cancel-button' ).click( function() {
+ self.hideEditBox( true );
+ } );
+ this.disableAllButtons();
+ var editField = $( '#cs-body-edit-field' );
+ if ( editField !== null ) {
+ editField.focus();
+ }
+ },
+ hideEditBox: function( animated ) {
+ var self = this;
+ if ( animated ) {
+ $( '#cs-edit-box' ).slideUp( 'normal', function() {
+ $( '#cs-edit-box' ).remove();
+ } );
+ } else {
+ $( '#cs-edit-box' ).remove();
+ }
+ this.enableAllButtons();
+ },
+ postComment: function( parentPageId ) {
+ var self = this;
+
+ var commentTitle;
+ if ( parentPageId === null ) {
+ var titleField = $( '#cs-title-edit-field' );
+ if ( titleField !== null ) {
+ commentTitle = titleField .val();
+ if ( commentTitle === null || commentTitle.trim() === "" ) {
+ this.reportError( 'commentstreams-validation-error-nocommenttitle' );
+ return;
+ }
+ }
+ } else {
+ commentTitle = null;
+ }
+
+ var commentText = $( '#cs-body-edit-field' ).val();
+ if ( commentText === null || commentText.trim() === "" ) {
+ this.reportError( 'commentstreams-validation-error-nocommenttext' );
+ return;
+ }
+
+ $( '#cs-submit-button' ).attr( 'disabled', 'disabled' );
+ $( '#cs-cancel-button' ).attr( 'disabled', 'disabled' );
+
+ $( '#cs-edit-box' ).fadeTo( 100, 0.2, function() {
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( 'cs-edit-box' ) );
+
+ var associatedPageId = mw.config.get( 'wgArticleId' );
+ CommentStreamsQuerier.postComment( commentTitle, commentText,
+ associatedPageId, parentPageId, function( result ) {
+ $( '.spinner' ).remove();
+ if ( result.error === undefined ) {
+ var comment = self.formatComment( result );
+ if ( parentPageId ) {
+ if ( !self.moderatorFastDelete ) {
+ var deleteSpan = $( '#cs-edit-box' )
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .find( '.cs-comment-header' )
+ .find( '.cs-delete-button' );
+ deleteSpan.remove();
+ }
+ var location = $( '#cs-edit-box' )
+ .closest( '.cs-stream' )
+ .find( '.cs-stream-footer' );
+ self.hideEditBox( false );
+ comment.insertBefore( $( location ) )
+ .hide()
+ .slideDown();
+ } else {
+ self.hideEditBox( false );
+ if ( self.newestStreamsOnTop ) {
+ comment.insertAfter( '#cs-header' )
+ .hide()
+ .slideDown();
+ } else {
+ comment.insertBefore( '#cs-footer' )
+ .hide()
+ .slideDown();
+ }
+ self.adjustCommentOrder( comment, 0, 0,
+ result.created_timestamp );
+ }
+ } else {
+ self.reportError( result.error );
+ $( '#cs-edit-box').fadeTo( 0.2, 100, function() {
+ $( '#cs-submit-button' ).attr( 'disabled', false );
+ $( '#cs-cancel-button' ).attr( 'disabled', false );
+ } );
+ }
+ } );
+ } );
+ },
+ deleteComment: function( element, pageId ) {
+ var self = this;
+ var message_text =
+ mw.message( 'commentstreams-dialog-delete-message' ).text();
+ var yes_text =
+ mw.message( 'commentstreams-dialog-buttontext-yes' ).text();
+ var no_text =
+ mw.message( 'commentstreams-dialog-buttontext-no' ).text();
+ var dialog = new OO.ui.MessageDialog();
+ var window_manager = new OO.ui.WindowManager();
+ $( '#cs-comments' ).append( window_manager.$element );
+ window_manager.addWindows( [ dialog ] );
+ window_manager.openWindow( dialog, {
+ message: message_text,
+ actions: [
+ { label: yes_text, action: 'yes' },
+ { label: no_text, flags: 'primary' }
+ ]
+ } ).then( function ( opened ) {
+ opened.then( function ( closing, data ) {
+ if ( data && data.action ) {
+ if ( data.action === 'yes' ) {
+ self.realDeleteComment( element, pageId );
+ }
+ }
+ } );
+ } );
+ },
+ realDeleteComment: function( element, pageId ) {
+ var self = this;
+ this.disableAllButtons();
+ element.fadeTo( 100, 0.2, function() {
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( element.attr( 'id' ) ) );
+ CommentStreamsQuerier.deleteComment( pageId, function( result ) {
+ $( '.spinner' ).remove();
+ if ( result.error === undefined ||
+ result.error === 'commentstreams-api-error-commentnotfound' ) {
+ if ( element.hasClass( 'cs-head-comment' ) ) {
+ element.closest( '.cs-stream' )
+ .slideUp( 'normal', function() {
+ element.closest( '.cs-stream' ).remove();
+ self.enableAllButtons();
+ } );
+ } else {
+ var parentId = element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .attr( 'data-id' );
+ CommentStreamsQuerier.queryComment( parentId, function( result ) {
+ if ( result.error === undefined && self.canDelete( result ) &&
+ !self.moderatorFastDelete ) {
+ self.createDeleteButton( result.username )
+ .insertAfter ( element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .find( '.cs-comment-header' )
+ .find( '.cs-edit-button' ) );
+ }
+ element.slideUp( 'normal', function() {
+ element.remove();
+ self.enableAllButtons();
+ } );
+ } );
+ }
+ } else {
+ self.reportError( result.error );
+ element.fadeTo( 0.2, 100, function() {
+ self.enableAllButtons();
+ } );
+ }
+ } );
+ } );
+ },
+ editComment: function( element, pageId ) {
+ var self = this;
+ this.disableAllButtons();
+ element.fadeTo( 100, 0.2, function() {
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( element.attr( 'id' ) ) );
+ CommentStreamsQuerier.queryComment( pageId, function( result ) {
+ $( '.spinner' ).remove();
+
+ if ( result.error === undefined ) {
+ var is_stream = element.hasClass( 'cs-head-comment' );
+ var commentBox = self.formatEditBox( is_stream );
+ commentBox.insertAfter( element );
+ element.hide();
+ commentBox.slideDown();
+
+ var editField = $( '#cs-body-edit-field' );
+ editField.val( result.wikitext );
+ if ( is_stream ) {
+ var titleField = $( '#cs-title-edit-field' );
+ titleField.val( result.commenttitle );
+ titleField.focus();
+ } else {
+ editField.focus();
+ }
+
+ $( '#cs-cancel-button' ).click( function() {
+ commentBox.slideUp( 'normal', function() {
+ element.fadeTo( 0.2, 100, function() {
+ commentBox.remove();
+ self.enableAllButtons();
+ } );
+ } );
+ } );
+
+ $( '#cs-submit-button' ).click( function() {
+ if ( element.hasClass( 'cs-head-comment' ) ) {
+ var commentTitle = $( '#cs-title-edit-field' ).val();
+ if ( commentTitle === null || commentTitle.trim() === "" ) {
+ self.reportError(
+ 'commentstreams-validation-error-nocommenttitle' );
+ return;
+ }
+ }
+
+ var commentText = $( '#cs-body-edit-field' ).val();
+ if ( commentText === null || commentText.trim() === "" ) {
+ self.reportError(
+ 'commentstreams-validation-error-nocommenttext' );
+ return;
+ }
+
+ $( '#cs-submit-button' ).attr( 'disabled', 'disabled' );
+ $( '#cs-cancel-button' ).attr( 'disabled', 'disabled' );
+
+ commentBox.fadeTo( 100, 0.2, function() {
+ new Spinner( self.spinnerOptions )
+ .spin( document.getElementById( 'cs-edit-box' ) );
+
+ CommentStreamsQuerier.editComment( commentTitle, commentText,
+ pageId, function( result ) {
+ $( '.spinner' ).remove();
+ if ( result.error === undefined ) {
+ var comment = self.formatCommentInner( result );
+ if ( element.closest( '.cs-stream' ).hasClass( 'cs-collapsed' ) ) {
+ comment.find( '.cs-comment-body' ).addClass( 'cs-hidden' );
+ }
+ commentBox.slideUp( 'normal', function() {
+ comment.insertAfter( commentBox );
+ commentBox.remove();
+ element.remove();
+ self.enableAllButtons();
+ } );
+ } else if ( result.error === 'commentstreams-api-error-commentnotfound' ) {
+ self.reportError( result.error );
+ var parentId = element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .attr( 'data-id' );
+ CommentStreamsQuerier.queryComment( parentId, function( result ) {
+ if ( result.error === undefined &&
+ self.canDelete( result ) &&
+ !self.moderatorFastDelete ) {
+ self.createDeleteButton( result.username )
+ .insertAfter ( element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .find( '.cs-comment-header' )
+ .find( '.cs-edit-button' ) );
+ }
+ commentBox.slideUp( 'normal', function() {
+ commentBox.remove();
+ element.remove();
+ self.enableAllButtons();
+ } );
+ } );
+ } else {
+ self.reportError( result.error );
+ commentBox.fadeTo( 0.2, 100, function() {
+ $( '#cs-submit-button' ).attr( 'disabled', false );
+ $( '#cs-cancel-button' ).attr( 'disabled', false );
+ } );
+ }
+ } );
+ } );
+ } );
+ } else if ( result.error === 'commentstreams-api-error-commentnotfound' ) {
+ self.reportError( result.error );
+ var parentId = element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .attr( 'data-id' );
+ CommentStreamsQuerier.queryComment( parentId, function( result ) {
+ if ( result.error === undefined &&
+ self.canDelete( result ) &&
+ !self.moderatorFastDelete ) {
+ self.createDeleteButton( result.username )
+ .insertAfter ( element
+ .closest( '.cs-stream' )
+ .find( '.cs-head-comment' )
+ .find( '.cs-comment-header' )
+ .find( '.cs-edit-button' ) );
+ }
+ element.remove();
+ self.enableAllButtons();
+ } );
+ } else {
+ self.reportError( result.error );
+ element.fadeTo( 0.2, 100, function() {
+ self.enableAllButtons();
+ } );
+ }
+ } );
+ } );
+ },
+ canEdit: function( comment ) {
+ var username = comment.username;
+ if ( !mw.user.isAnon() && ( mw.user.getName() === username ||
+ this.moderatorEdit ) ) {
+ return true;
+ }
+ return false;
+ },
+ canDelete: function( comment ) {
+ var username = comment.username;
+ if ( !mw.user.isAnon() &&
+ ( mw.user.getName() === username || this.moderatorDelete ) &&
+ ( comment.numreplies === 0 || this.moderatorFastDelete ) ) {
+ return true;
+ }
+ return false;
+ },
+ reportError: function( message ) {
+ var message_text = mw.message( message ).text();
+ var ok_text = mw.message( 'commentstreams-dialog-buttontext-ok' ).text();
+ var dialog = new OO.ui.MessageDialog();
+ var window_manager = new OO.ui.WindowManager();
+ $( '#cs-comments' ).append( window_manager.$element );
+ window_manager.addWindows( [ dialog ] );
+ window_manager.openWindow( dialog, {
+ message: message_text,
+ actions: [ {
+ action: 'accept',
+ label: ok_text,
+ flags: 'primary'
+ } ]
+ } );
+ }
+ };
+}( mediaWiki, jQuery ) );
+
+window.CommentStreamsController = commentstreams_controller;
+
+( function( mw, $ ) {
+ $( document )
+ .ready( function() {
+ if ( mw.config.exists( 'CommentStreams' ) ) {
+ window.CommentStreamsController.initialize();
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/CommentStreams/resources/CommentStreamsAllComments.css b/CommentStreams/resources/CommentStreamsAllComments.css
new file mode 100644
index 00000000..e39ea4bc
--- /dev/null
+++ b/CommentStreams/resources/CommentStreamsAllComments.css
@@ -0,0 +1,30 @@
+.csall-message {
+ background-color: #ddd;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+.csall-wikitable {
+ width: 100%;
+}
+
+.csall-wikitable th {
+ text-align: center;
+}
+
+.csall-navigationtable {
+ width: 100%;
+}
+
+.csall-button {
+ font-size: 1em;
+ padding:5px 15px;
+ margin:5px 5px;
+ background:#ccc;
+ border:0 none;
+ cursor:pointer;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
+}
diff --git a/CommentStreams/resources/CommentStreamsQuerier.js b/CommentStreams/resources/CommentStreamsQuerier.js
new file mode 100644
index 00000000..b124656a
--- /dev/null
+++ b/CommentStreams/resources/CommentStreamsQuerier.js
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2016 The MITRE Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+var commentstreams_querier = ( function( mw ) {
+ return {
+ queryComment: function( pageid, reply ) {
+ var self = this;
+ var api = new mw.Api();
+ api.get( {
+ action: 'csQueryComment',
+ pageid: pageid
+ } )
+ .done( function( data ) {
+ reply( data.csQueryComment );
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ deleteComment: function( pageid, reply ) {
+ var self = this;
+ var api = new mw.Api();
+ api.post( {
+ action: 'csDeleteComment',
+ pageid: pageid,
+ token: mw.user.tokens.get( 'editToken' )
+ } )
+ .done( function( data ) {
+ reply( data );
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ postComment: function( commenttitle, wikitext, associatedid, parentid,
+ reply ) {
+ var self = this;
+ var api = new mw.Api();
+ var data = {
+ action: 'csPostComment',
+ wikitext: wikitext,
+ associatedid: associatedid,
+ token: mw.user.tokens.get( 'editToken' )
+ };
+ if ( commenttitle !== null ) {
+ data.commenttitle = commenttitle;
+ }
+ if ( parentid !== null ) {
+ data.parentid = parentid;
+ }
+ api.post(
+ data
+ )
+ .done( function( data ) {
+ reply( data.csPostComment );
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ editComment: function( commenttitle, wikitext, pageid, reply ) {
+ var self = this;
+ var api = new mw.Api();
+ api.post( {
+ action: 'csEditComment',
+ pageid: pageid,
+ commenttitle: commenttitle,
+ wikitext: wikitext,
+ token: mw.user.tokens.get( 'editToken' )
+ } )
+ .done( function( data ) {
+ reply( data.csEditComment );
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ vote: function( pageid, vote, reply ) {
+ var self = this;
+ var api = new mw.Api();
+ api.post( {
+ action: 'csVote',
+ pageid: pageid,
+ vote: vote,
+ token: mw.user.tokens.get( 'editToken' )
+ } )
+ .done( function( data ) {
+ reply( data.csVote );
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ watch: function( pageid, action, reply ) {
+ var self = this;
+ var api = new mw.Api();
+ api.post( {
+ action: action ? 'csWatch' : 'csUnwatch',
+ pageid: pageid,
+ token: mw.user.tokens.get( 'editToken' )
+ } )
+ .done( function( data ) {
+ if ( action ) {
+ reply( data.csWatch );
+ } else {
+ reply( data.csUnwatch );
+ }
+ } )
+ .fail( function( data ) {
+ self.reportError( data, reply );
+ } );
+ },
+ reportError: function( data, reply ) {
+ if ( data === 'nosuchpageid' ) {
+ reply( {
+ 'error': 'commentstreams-api-error-commentnotfound'
+ } );
+ } else if ( data === 'badtoken' ) {
+ reply( {
+ 'error': 'commentstreams-api-error-notloggedin'
+ } );
+ } else {
+ reply( {
+ 'error': data
+ } );
+ }
+ }
+ };
+}( mediaWiki ) );
+
+window.CommentStreamsQuerier = commentstreams_querier;
diff --git a/CommentStreams/resources/spin.min.js b/CommentStreams/resources/spin.min.js
new file mode 100644
index 00000000..ebfbb1a2
--- /dev/null
+++ b/CommentStreams/resources/spin.min.js
@@ -0,0 +1,2 @@
+//fgnass.github.com/spin.js#v2.0.1
+!function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d<k.length;d++)if(c=k[d]+b,void 0!==e[c])return c;return void 0!==e[b]?b:void 0}function e(a,b){for(var c in b)a.style[d(a,c)||c]=b[c];return a}function f(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)void 0===a[d]&&(a[d]=c[d])}return a}function g(a,b){return"string"==typeof a?a:a[b%a.length]}function h(a){this.opts=f(a||{},h.defaults,n)}function i(){function c(b,c){return a("<"+b+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}}var j,k=["webkit","Moz","ms","O"],l={},m=function(){var c=a("style",{type:"text/css"});return b(document.getElementsByTagName("head")[0],c),c.sheet||c.styleSheet}(),n={lines:12,length:7,width:5,radius:10,rotate:0,corners:1,color:"#000",direction:1,speed:1,trail:100,opacity:.25,fps:20,zIndex:2e9,className:"spinner",top:"50%",left:"50%",position:"absolute"};h.defaults={},f(h.prototype,{spin:function(b){this.stop();{var c=this,d=c.opts,f=c.el=e(a(0,{className:d.className}),{position:d.position,width:0,zIndex:d.zIndex});d.radius+d.length+d.width}if(e(f,{left:d.left,top:d.top}),b&&b.insertBefore(f,b.firstChild||null),f.setAttribute("role","progressbar"),c.lines(f,c.opts),!j){var g,h=0,i=(d.lines-1)*(1-d.direction)/2,k=d.fps,l=k/d.speed,m=(1-d.opacity)/(l*d.trail/100),n=l/d.lines;!function o(){h++;for(var a=0;a<d.lines;a++)g=Math.max(1-(h+(d.lines-a)*n)%l*m,d.opacity),c.opacity(f,a*d.direction+i,g,d);c.timeout=c.el&&setTimeout(o,~~(1e3/k))}()}return c},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=void 0),this},lines:function(d,f){function h(b,c){return e(a(),{position:"absolute",width:f.length+f.width+"px",height:f.width+"px",background:b,boxShadow:c,transformOrigin:"left",transform:"rotate("+~~(360/f.lines*k+f.rotate)+"deg) translate("+f.radius+"px,0)",borderRadius:(f.corners*f.width>>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k<f.lines;k++)i=e(a(),{position:"absolute",top:1+~(f.width/2)+"px",transform:f.hwaccel?"translate3d(0,0,0)":"",opacity:f.opacity,animation:j&&c(f.opacity,f.trail,l+k*f.direction,f.lines)+" "+1/f.speed+"s linear infinite"}),f.shadow&&b(i,e(h("#000","0 0 4px #000"),{top:"2px"})),b(d,b(i,h(g(f.color,k),"0 0 1px rgba(0,0,0,.1)")));return d},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}});var o=e(a("group"),{behavior:"url(#default#VML)"});return!d(o,"transform")&&o.adj?i():j=d(o,"animation"),h}); \ No newline at end of file
diff --git a/CommentStreams/sql/commentData.sql b/CommentStreams/sql/commentData.sql
new file mode 100644
index 00000000..00b8fead
--- /dev/null
+++ b/CommentStreams/sql/commentData.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS cs_comment_data
+(
+page_id int(10) unsigned,
+assoc_page_id int(10) unsigned,
+parent_page_id int(10) unsigned,
+comment_title varbinary(255),
+PRIMARY KEY (page_id),
+FOREIGN KEY (assoc_page_id) REFERENCES page(page_id)
+); \ No newline at end of file
diff --git a/CommentStreams/sql/votes.sql b/CommentStreams/sql/votes.sql
new file mode 100644
index 00000000..4e232eb2
--- /dev/null
+++ b/CommentStreams/sql/votes.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS cs_votes
+(
+page_id int(10) unsigned NOT NULL,
+user_id int(10) unsigned NOT NULL,
+vote tinyint NOT NULL,
+INDEX (page_id, user_id)
+);
diff --git a/CommentStreams/sql/watch.sql b/CommentStreams/sql/watch.sql
new file mode 100644
index 00000000..4e3dbf61
--- /dev/null
+++ b/CommentStreams/sql/watch.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS cs_watchlist
+(
+page_id int(10) unsigned NOT NULL,
+user_id int(10) unsigned NOT NULL,
+INDEX (page_id, user_id)
+);
diff --git a/CommentStreams/version b/CommentStreams/version
new file mode 100644
index 00000000..7342fdcf
--- /dev/null
+++ b/CommentStreams/version
@@ -0,0 +1,4 @@
+CommentStreams: REL1_30
+2017-09-21T22:04:40
+
+08aec40