MediaWiki:Gadget-dashboard.AddToFlickrBlacklist.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Script providing the logic 
 * and the user interface
 * for adding users to both flickr blacklists
 * with just one click
 *
 * Adds an UI to [[Template:Dashboard/Widgets/Add blacklist user]].
 * Also a dashboard widget.
 *
 * @rev 1 (2013-02-06)
 * @author [[User:Rillke]], 2013
 * <nowiki>
 */
// List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/
// Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
/*global jQuery:false, mediaWiki:false, TextCleaner:false, importScript:false*/

// Set jsHint-options. You should not set forin or undef to false if your script does not validate.
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true, smarttabs:true*/

(function($, mw) {
	"use strict";

	var fbl;

	function firstItem(o) {
		for (var i in o) {
			if (o.hasOwnProperty(i)) {
				return o[i];
			}
		}
	}

	function firstItemName(o) {
		for (var i in o) {
			if (o.hasOwnProperty(i)) {
				return i;
			}
		}
	}

	fbl = {
		version: '0.0.0.5',
		flickrAPIUrl: 'https://api.flickr.com/services/rest/',
		flickrAPIKey: 'secret',
		flickrDoAPICall: function(params, cb, errCb) {
			$.support.cors = true;
			params.format = 'json';
			params.api_key = fbl.flickrAPIKey;
			if (fbl.flickrAPIKey === 'secret') throw new Error("You *must* set the API key before calling the FlickrAPI!");

			var $jqXHR = $.ajax({
				url: fbl.flickrAPIUrl,
				cache: false,
				type: 'GET',
				crossDomain: true,
				jsonp: 'jsoncallback',
				dataType: 'jsonp',
				data: params,
				success: function(r, textStatus, jqXHR) {
					if (r.stat === 'fail') {
						return fbl.secureCall(errCb, $jqXHR, 'FlickrAPI returned error: ' + r.message, r.code);
					} else if (r.stat !== 'ok') {
						return fbl.secureCall(errCb, $jqXHR, 'FlickrAPI issue: ' + r.stat);
					}
					fbl.secureCall(cb, r, textStatus, jqXHR);
				},
				error: function(jqXHR, textStatus, errorThrown) {
					fbl.secureCall(errCb, jqXHR, textStatus, errorThrown);
				}
			});
		},
		flickrGetIDByURL: function(URL, cb, errCb) {
			// sample response: test({"user":{"id":"57124063@N03", "username":{"_content":"Valerii9116"}}, "stat":"ok"})
			fbl.flickrDoAPICall({
				method: 'flickr.urls.lookupUser',
				url: URL
			}, cb, errCb);
		},
		flickrGetInfoById: function(ID, cb, errCb) {
			// sample response: 
			// test({"person":{"id":"62465723@N04", "nsid":"62465723@N04", "ispro":0, "iconserver":"0", "iconfarm":0, "path_alias":null, 
			// "username":{"_content":"pdamasceno"}, "realname":{"_content":""}, "location":{"_content":""}, "description":{"_content":""}, 
			// "photosurl":{"_content":"http:\/\/www.flickr.com\/photos\/62465723@N04\/"}, "profileurl":{"_content":"http:\/\/www.flickr.com\/people\/62465723@N04\/"}, 
			// "mobileurl":{"_content":"http:\/\/m.flickr.com\/photostream.gne?id=62433584"}, "photos":{"firstdatetaken":{"_content":"2012-09-30 14:02:20"}, 
			// "firstdate":{"_content":"1349039021"}, "count":{"_content":1}}}, "stat":"ok"})
			fbl.flickrDoAPICall({
				method: 'flickr.people.getInfo',
				user_id: ID
			}, cb, errCb);
		},
		fetchFlickrAPIKey: function(cb, errCb) {
			if (fbl.flickrAPIKey !== 'secret') return cb();
			$.get(mw.util.getUrl('Special:UploadWizard'), function(r) {
				var m = r.match(/[\"\']?flickrApiKey[\"\']?\s*:\s*[\"\']([0-9a-f]{10,50})[\"\']/);
				if (!m) return errCb("Flickr API Key could not be retrieved!");
				fbl.flickrAPIKey = m[1];
				cb();
			});
		},
		flickrIsURL: function(url) {
			return (/flickr\.com\//).test(url);
		},
		flickrContainsId: function(txt) {
			var m = txt.match(/\d{1,11}@\D\d{2}/);
			if (m) {
				return m[0];
			} else {
				return false;
			}
		},
		flickrIsValidId: function(txt) {
			return (/^\d{1,11}@\D\d{2}$/).test(txt);
		},
		mwChangePage: function(title, modifier, summary, done, failed) {
			mw.libs.commons.api.query({
				action: 'query',
				prop: 'info|revisions',
				rvprop: 'content|timestamp',
				intoken: 'edit',
				titles: title
			}, {
				method: 'GET',
				cache: false,
				cb: function(r) {
					var pg = firstItem(r.query.pages),
						rv = pg.revisions[0],
						t = modifier(rv['*']);

					mw.libs.commons.api.editPage({
						cb: done,
						errCb: function() {
							fbl.secureCall(failed);
							throw new Error("Editing " + title + " failed!");
						},
						editType: 'text',
						title: title,
						text: t,
						summary: summary,
						nocreate: true,
						minor: true,
						starttimestamp: pg.starttimestamp,
						timestamp: rv.timestamp,
						watchlist: 'nochange'
					});
				},
				// r-result, query, text
				errCb: function(t, r, q) {
					throw new Error("Server error while retrieving contents of " + title + "!");
				}
			});
		},
		botPageTitle: 'User:FlickreviewR/bad-authors',
		addAuthorToBotPage: function(id, cb, errCb) {
			if (!fbl.flickrIsValidId(id)) throw new Error("Not a valid ID!");
			var _changeContent = function(t_in) {
				if (!t_in) throw new Error("Unable to retrieve contents of the bot list!");
				var m = t_in.match(/\d{1,11}@\D\d{2}/g);

				if ($.inArray(id, m) >= 0) {
					return cb('already-in-list');
				}

				m.push(id);
				m.sort();
				var t_out = '#Add NSIDs of bad authors below this line. See [[Commons:Questionable Flickr images]] for more information.\n\n' +
					m.join('\n');

				return t_out;
			};
			fbl.secureCall('mwChangePage', fbl.botPageTitle, _changeContent, 'Adding ' + id, cb, errCb);
		},
		userListTitle: 'Commons:Questionable Flickr images/Users',
		qfiFromInfo: function(info) {
			info.path_alias = info.path_alias || '';
			// Running Lupo's text cleaner over the reason preventing invalidation of the whole page
			// by typos/missing closing brackets etc.
			if (window.TextCleaner) info.reason = TextCleaner.sanitizeWikiText(info.reason, true);

			return '{{qfi|' + info.username.replace(/\|/g, '&#124;') + '|' + (info.path_alias || '') + '|' + info.nsid + '|4=' + info.reason + '}}';
		},
		addAuthorToUserList: function(info, cb) {
			var _changeContent = function(t_in) {
				var m = t_in.split('\n'),
					posToInsert = {
						t: '',
						i: 0
					},
					confirmed = 0,
					userNameL = info.username.toLowerCase();

				var toInsert = fbl.qfiFromInfo(info);


				// We assume that the list is already sorted
				// "confirmed" for achieving a minimum error tolerance
				$.each(m, function(i, line) {
					var nextLine = m[i + 1];
					// Skip irrelevant lines
					if (-1 === line.indexOf('{{qfi|') && nextLine !== '|}') {
						confirmed = 0;
						return;
					}
					var currentName = line.match(/\{\{qfi\|([^\|]*)\|/)[1];
					if (!currentName) return;
					currentName = currentName.toLowerCase();

					if (currentName < userNameL) {
						posToInsert.t = currentName;
						posToInsert.i = i;
						confirmed = 0;
					} else {
						confirmed++;
						if (2 === confirmed) return false;
					}
				});
				// Insert the new line directly after the last line that was
				// alphabetically "lower" than the name to insert
				m.splice(posToInsert.i + 1, 0, toInsert);
				return m.join('\n');
			};
			fbl.secureCall('mwChangePage', fbl.userListTitle, _changeContent, 'Adding ' + info.username + ' (' + info.nsid + ') because ' + info.reason, cb);
		},
		/**
		 *  TextCleaner is piece of solid work made by [[User:Lupo]]!
		 **/
		textCleanerLocation: 'MediaWiki:TextCleaner.js',
		textCleanerRequested: false,
		submit: function($ui, e) {
			// UI helper functions
			var _blink = function($el) {
				$el.addClass('ui-state-error');
				setTimeout(function() {
					$el.removeClass('ui-state-error');
				}, 1200);
			};
			var _finish = function(buttonclass) {
				$ui.$flickrXWrap.remove();
				$ui.$reasonWrap.remove();
				$ui.$submit.remove();
				$ui.$ok = $('<button type="button" role="button" id="fblSubmit">').text("Ok").attr({
					'style': 'min-height:2em; width:97%; text-align:center; font-weight: bold; font-size: 1.3em',
					'class': (buttonclass || 'ui-button-green') + ' ui-button-large'
				}).button().appendTo($ui);
				$ui.$ok.click(function() {
					var $p = $ui.parent();
					$ui.remove();
					$p.append(fbl.$getUI());
				});
			};
			var _setStatus = function(txt) {
				$ui.$status.text(txt);
			};

			// First validate input
			var flickrX = $.trim($ui.$flickrX.val()),
				isURL = fbl.flickrIsURL(flickrX),
				isNSID = fbl.flickrIsValidId(flickrX),
				reason = $ui.$reason.val();

			if (!isURL && !isNSID) {
				_blink($ui.$flickrXWrap);
				$ui.$flickrX
					.attr('title', "Please specify a valid URL pointing to a Flickr profile, a flickr file or gallery or enter a valid NSID like 12345678@N94")
					.tipsy({
						gravity: $.fn.tipsy.autoWE,
						trigger: 'focus'
					}).focus();
				return;
			}
			if (reason.length < 8) {
				_blink($ui.$reasonWrap);
				$ui.$reason.attr('title', "A reason with at least 8 letter is mandatory!")
					.tipsy({
						gravity: $.fn.tipsy.autoWE,
						trigger: 'focus'
					}).focus();
				return;
			}

			// Then, disable the submit
			// and query info we need
			$ui.$submit.button({
				'disabled': true
			});
			var gatheredInfo = {
				reason: reason
			};

			var _flickrUserInfo = function() {
				_setStatus("Asking FlickrAPI for user info.");
				fbl.secureCall('flickrGetInfoById', gatheredInfo.nsid, function(r) {
					var p = r.person;
					$.extend(gatheredInfo, {
						username: p.username._content,
						path_alias: p.path_alias,
						ispro: p.ispro,
						realname: p.realname ? p.realname._content : ''
					});
					_setStatus("Got user info.");
					_doEdits();
				}, function($xhr, text) {
					_setStatus(text);
					_finish('ui-button-red');
				});
			};

			_setStatus("Fetching API key.");
			fbl.secureCall('fetchFlickrAPIKey', function() {
				if (isURL) {
					_setStatus("Asking FlickrAPI for NSID of the user.");
					fbl.secureCall('flickrGetIDByURL', flickrX, function(r) {
						gatheredInfo.nsid = r.user.id;
						_setStatus("Got user ID.");
						_flickrUserInfo();
					}, function($xhr, text) {
						_setStatus(text);
						_finish('ui-button-red');
					});
				} else {
					gatheredInfo.nsid = flickrX;
					_flickrUserInfo();
				}
			}, function(err) {
				_setStatus(err);
				_finish('ui-button-red');
			});

			// Finally do the edits, 
			// if everything worked
			var _doEdits = function() {
				_setStatus("Doing edits. User name: " + gatheredInfo.username + " Path alias: " +
					gatheredInfo.path_alias + " Real name: " + gatheredInfo.realname + " ID: " + gatheredInfo.nsid);

				fbl.secureCall('addAuthorToBotPage', gatheredInfo.nsid, function(r) {
					if ('already-in-list' === r) {
						_setStatus("Flickr user ID already in bot list. Aborting.");
						return _finish('ui-button-blue');
					}
					_setStatus("Added ID to bot blacklist. Now processing full list.");
					fbl.secureCall('addAuthorToUserList', gatheredInfo, function() {
						_setStatus("Done.");
						_finish();
					});
				}, function() {
					_setStatus("Editing the bot list failed. Here are all required information: " +
						fbl.qfiFromInfo(gatheredInfo));
					_finish('ui-button-red');
				});
			};
		},

		/**
		 ** Method to catch errors and report where they occurred
		 **/
		secureCall: function(fn) {
			var o = fbl;
			try {
				o.currentTask = arguments[0];
				if ($.isFunction(fn)) {
					if (fn.name) o.log(fn);
					return fn.apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
				} else if ('string' === typeof fn) {
					o.log(fn);
					return o[fn].apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
				} else {
					o.log('This is not a function!');
				}
			} catch (ex) {
				o.log('failure at ' + fn);
				o.fail(ex);
			}
		},
		log: function(key, val) {
			if (window.console && $.isFunction(window.console.log)) window.console.log('FBL> ' + key, val /*, this*/ );
		},
		fail: function(err) {
			// Handle error or report or whatever
			// TODO build an error-report script
			fbl.log(err);
		},


		/**
		 *  Implement Commons Dashboard Widget interface
		 *  @dashboardwidgetinterface
		 **/
		$getUI: function() {
			if (fbl.textCleanerRequested) {
				importScript(fbl.textCleanerLocation);
				fbl.textCleanerRequested = true;
			}
			// Creating root node for our widget
			var $ui = $('<div>').attr({
				'class': 'dashbord-item flickr-blacklist-adder'
			});
			if (mw.user.isAnon()) {
				$('<div>').text("{{Dashboard/Widgets/Add blacklist user}}: In order to use this widget, please log in.").css('font-weight', 'bold').appendTo($ui);
				return $ui;
			}

			$ui.$flickrXWrap = $('<div>').attr({
				'title': "Insert Flickr URL or FlickrID"
			}).appendTo($ui);
			$ui.$flickrXL = $('<label>', {
				'text': "Insert Flickr URL or Flickr user ID (nsid):"
			}).attr({
				'for': 'fblFlickrX'
			}).appendTo($ui.$flickrXWrap);
			$ui.$flickrX = $('<input>').attr({
				'id': 'fblFlickrX',
				'style': 'width:97%',
				'placeholder': 'Flickr URL or Flickr user ID'
			}).appendTo($ui.$flickrXWrap);
			$ui.$reasonWrap = $('<div>').attr({
				'title': "Why should the user be blacklisted?"
			}).appendTo($ui);
			$ui.$reasonL = $('<label>', {
				'text': "Reason for blacklisting:"
			}).attr({
				'for': 'fblReasonFlickr'
			}).appendTo($ui.$reasonWrap);
			$ui.$reason = $('<input>').attr({
				'id': 'fblReasonFlickr',
				'style': 'width:97%',
				'placeholder': 'Reason'
			}).appendTo($ui.$reasonWrap);
			$ui.$spacer = $('<div>').attr({
				'style': 'height:1em; width:97%'
			}).appendTo($ui);
			$ui.$submit = $('<button type="button" role="button" id="fblSubmit">').text("Add user to blacklists").attr({
				'style': 'min-height:2em; width:97%; text-align:center; font-weight: bold; font-size: 1.3em',
				'class': 'ui-button-orange ui-button-large'
			}).button().appendTo($ui);
			$ui.$status = $('<div>').appendTo($ui);

			$ui.$submit.click($.proxy(fbl.submit, fbl, $ui));
			return $ui;
		},
		createUI: function() {
			var $container = $('#fblContainer').text('');
			if ($container.length) $container.append(fbl.$getUI());
		}
	};

	// Expose globally
	window.flickrBlacklist = fbl;


	var launch = function($c) {
		if ($('#fblContainer', $c).length) mw.loader.using(['ext.gadget.libAPI', 'jquery.ui', 'ext.gadget.tipsyDeprecated', 'mediawiki.user'], fbl.createUI);
	};
	mw.hook( 'wikipage.content' ).add( launch );

}(jQuery, mediaWiki));

// </nowiki>