MediaWiki:EnhancedStash.js

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.
/**
 * Enhances [[Special:UploadStash]] adding a button that
 * when clicked will add thumbnails and imageinfo. On top of that,
 * adds buttons to publish completed uploads.
 *
 * @author Rainer Rillke
 * <nowiki>
 */

/*
 * Copyright (C) 2013 Rainer Rillke and others
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

/*global mediaWiki:false, jQuery: false*/

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

	if (mw.config.get('wgCanonicalSpecialPageName') !== 'UploadStash') return;
	
	var us,
		stashMaxServeSize = 1048576,
		id = 'commons-upload-stash-enhance-button',
		$ul = $('#mw-content-text ul').first(),
		rtl = $(document.body).hasClass('rtl'),
		right = rtl ? 'left' : 'right',
		left = rtl ? 'right' : 'left',
		e = rtl ? 'w' : 'e';
	
	us = mw.libs.enhancedStash = {
		fileKeyMap: {},
		fileKeys: [],
		queue: null,
		$exs: $(),
		init: function() {
			this.queue = new mw.libs.JStack(this, null, {
				logPrefix: 'stash',
				app: 'MediaWiki:EnhancedStash.js',
				reportPage: 'MediaWiki talk:EnhancedStash.js',
				stuckTimeout: 2 * 61 * 1000
			});
		},
		loadAndContinue: function(deps, cont, args) {
			var us = this;
			args = Array.prototype.slice.call(args, 0);
			args.unshift(cont);
			mw.loader.using(deps, function() {
				us.queue.secureCall.apply(us.queue, args);
			});
		},
		blockPage: function() {
			this.loadAndContinue(['ext.gadget.jquery.blockUI'], '__blockPage', arguments);
		},
		__blockPage: function() {
			$ul.parent().block({ 
				message: '<img src="//upload.wikimedia.org/wikipedia/commons/1/10/Loading-special.gif" height="15" width="128"/>',
				css: {
					border: 'none',
					background: 'none'
				} 
			});
			us.queue.nextTask();
		},
		unblockPage: function() {
			$ul.parent().unblock();
			us.queue.nextTask();
		},
		parse: function() {
			$('#mw-content-text').find('li').each(function(i, el) {
				var $el = $(el),
					key = $el.find('a').first().text();
					
				us.fileKeys.push(key);
				us.fileKeyMap[key] = {
					$el: $el
				};
			});
			if (!us.fileKeys.length) {
				return us.queue.emptyQueue();
			}
			us.queue.nextTask();
		},
		query: function() {
			this.loadAndContinue(['ext.gadget.libAPI'], '__query', arguments);
		},
		__query: function() {
			var isDone = false;

			setTimeout(function() {
				if (!isDone) {
					us.queue.nextTask();
				}
				isDone = true;
			}, 1 * 60 * 1000);

			mw.libs.commons.api.$autoQuery({
				action: 'query',
				prop: 'stashimageinfo',
				siifilekey: us.fileKeys.join('|'),
				siiprop: 'timestamp|url|size|dimensions|sha1|mime|thumbmime|metadata|extmetadata|bitdepth',
				siiurlwidth: 120,
				siiurlheight: 120
			})
			.progress(function(result, params) {
						$.each(result.query.stashimageinfo, function(i, sii) {
							var key = sii.url.match(/Special\:UploadStash\/file\/(.+)$/)[1];
							us.fileKeyMap[key].sii = sii;
						});
					})
			.done(function(fullResult) {
						if (!isDone) {
							us.queue.nextTask();
						}
						isDone = true;
					});
		},
		getPublishButton: function(key, $img) {
			return 	$('<button>')
				.text('Publish')
				.button({
					icons: {
						primary: 'ui-icon-circle-arrow-n'
					}
				})
				.addClass('ui-button-blue')
				.click(function() {
					us.describe(key, $img);
				});
		},
		render: function() {
			var $detailsCont = $('<div>')
					.css({
						'float': right,
						'width': '49%',
						'box-shadow': '3px 3px 5px 6px #ccc',
						'visibility': 'hidden',
						'background': 'white',
						'border': '1px solid #EEE'
					})
					.insertBefore($ul).first(),
				$details = $('<div>')
					.css({
						overflow: 'auto'
					})
					.appendTo($detailsCont),
				$detailsClose = $('<button type="button"></button>')
					.text('close')
					.button({
						icons: {
							primary: 'ui-icon-circle-close'
						},
						text: false
					})
					.css({
						'position': 'absolute',
						'top': '.1em'
					})
					.attr('title', 'close')
					.prependTo($detailsCont)
					.click(function() {
						$detailsCont.css('visibility', 'hidden');
						us.$exs.button('option', 'disabled', false);
					});
					
			$detailsClose.css(right, '1.8em');
			
			$.each(us.fileKeyMap, function(key, m) {
				m.$el.css({
					'list-style': 'none',
					'display': 'block',
					'height': '120px'
				});

				// Do not process broken uploads
				if ( !m.sii ) {
					if ( m.$el ) {
						us.getPublishButton(key, $('<img>')).appendTo( m.$el );
					}
					return;
				}

				var ___imgLoadError = function () {
					if (m.sii.size < stashMaxServeSize) {
						$(this).attr('src', m.sii.url);
					}
				};
				var $info = $('<div>')
						.css({
							'width': '49%',
							'background': '#ECF4FF',
							'min-height': '115px'
						}),
					$datetime = $('<div>')
						.css({
							color: '#808080',
							'font-size': '.9em'
						})
						// TODO use a formatter!
						.text(m.sii.timestamp.replace('T', ' ').replace('Z', ''))
						.appendTo($info),
					$hwz = $('<div>')
						.css({
							color: '#008000',
							'font-size': '.9em'
						})
						// TODO use a formatter!
						.text(Math.round(m.sii.size / (1024 * 1024)) + ' MiB ' + m.sii.width + ' × ' + m.sii.height + 'px')
						.appendTo($info),
					$imgCont = $('<div>')
						.css('margin-' + right, '1em')
						.css({
							height: 120,
							width: 120,
							'float': left
						}).prependTo($info),
					$img = $('<img>')
						.error(___imgLoadError)
						.attr({
							src: m.sii.thumburl,
							title: key
						})
						.css({
							width: m.sii.thumbwidth,
							height: m.sii.thumbheight
						})
						.appendTo($imgCont),
					$ex = $('<button>')
						.css({
							'height': '100px',
							'width': '30px',
							'float': right
						})
						.button({
							icons: {
								primary: 'ui-icon-circle-triangle-' + e
							},
							text: false
						}).prependTo($info);

					us.getPublishButton(key, $img).appendTo($info);
					
					var t = $ul.offset().top;
					us.$exs = us.$exs.add($ex);
					$detailsCont.css({
						'position': 'fixed',
						'top': t,
						'width': $detailsCont.width(),
						'font-family': 'Courier,monospace',
						'white-space': 'pre-wrap'
					});
					$detailsCont.css(right, '5px');
					$details.css({
						'max-height': $(window).height() - t - 45
					});
					
					$ex.click(function() {
						us.$exs.button('option', 'disabled', false);
						$ex.button('option', 'disabled', true);
						$details.text(JSON.stringify(m.sii, null, '   '));
						$detailsCont.css('visibility', 'visible');
					});

					$info.prepend(m.$el.find('a').first());
					m.$el.empty().append($info);
			});
			us.queue.nextTask();
		},
		run: function() {
			$('#' + id).hide().off();
			us.queue.addTask('blockPage');
			us.queue.addTask('parse');
			us.queue.addTask('query');
			us.queue.addTask('render');
			us.queue.addTask('unblockPage');
			us.queue.nextTask();
		},
		describe: function() {
			us.loadAndContinue(['jquery.ui', 'mediawiki.user'], '__describe', arguments);
		},
		__describe: function(filekey, $img) {
			var d = new Date(),
				y = d.getFullYear(),
				m = (d.getMonth()+1).toString(),
				day = d.getDate();
				
			if (m.length < 2) m = "0"+m;
			if (day.length < 2) day = "0"+day;
			
			var $dlg = $('<div>')
					.prepend($img.clone().css('float', right)),
				$token = $('<input>')
					.val("Edit token")
					.attr('title', 'edit token')
					.attr('readonly', 'readonly')
					.css({
						'max-width': '25em',
						'width': '50%'
					})
					.appendTo($dlg),
				$sW = $('<div>').css('clear', 'both').appendTo($dlg),
				$sL = $('<label for="commons-enh-stash-sum"></label>')
					.text("Upload summary")
					.appendTo($sW),
				$s = $('<input>')
					.attr('placeholder', "Upload summary")
					.attr('id', 'commons-enh-stash-sum')
					.css({
						width: '98%'
					})
					.appendTo($sW),
				$tW = $('<div>').css('clear', 'both').appendTo($dlg),
				$tL = $('<label for="commons-enh-stash-title"></label>')
					.text("Target filename (existing files under that name will be overwritten)")
					.appendTo($tW),
				$t = $('<input>')
					.val(filekey)
					.attr('id', 'commons-enh-stash-title')
					.css({
						width: '98%'
					})
					.appendTo($tW),
				$cW = $('<div>').css('clear', 'both').appendTo($dlg),
				$cL = $('<label for="commons-enh-stash-desc"></label>')
					.text("File description")
					.appendTo($cW),
				$c = $('<textarea>')
					.attr('id', 'commons-enh-stash-desc')
					.css({
						width: '98%',
						height: '20em',
						'font-family': 'Courier,monospace'
					})
					.val("== {{int:filedesc}} ==\n{{Information\n |Description=\n" + 
			"{{en|1=}}\n{{" + mw.user.options.get('language') + "|1=}}\n |Source={{own}}\n |Date=" + d.getFullYear() + "-" + m + "-" + day +
			"\n |Author=[[User:" + mw.user.getName() + "|" + mw.user.getName() + "]]\n |Permission=\n |other_versions=\n}}\n\n== {{int:license-header}} ==\n{{subst:nld}}")
					.appendTo($cW),
				$pb = $('<button type="button"></button>')
					.text('publish')
					.button({
						icons: {
							primary: 'ui-icon-circle-check'
						}
					})
					.addClass('ui-button-large ui-button-green')
					.css({
						'text-transform': 'uppercase',
						'font-weight': 'bold'
					})
					.click(function() {
						$(this).button('option', 'disabled', true);
						var t = $.trim($t.val()),
							s = $.trim($s.val()),
							c = $.trim($c.val());
							
						if (!t) return;
						us.publish(filekey, t, s, c, $dlg, $pb);
					})
					.appendTo($dlg);
				
			var iv = setInterval(function() {
				us.refreshToken($token);
			}, 2*60*1000);
			us.refreshToken($token);
			
			$dlg.dialog({
				title: 'Publish ' + filekey,
				modal: true,
				width: Math.min($(window).width(), 650),
				close: function() {
					clearInterval(iv);
					$(this).remove();
				}
			});
		},
		refreshToken: function() {
			us.loadAndContinue(['ext.gadget.libAPI'], '__refreshToken', arguments);
		},
		__refreshToken: function($target) {
			mw.libs.commons.api.$query({
				'action': 'query',
				'meta': 'tokens',
				'type': 'csrf'
			}).done(function(r) {
				var t = r.query.tokens.csrftoken;
				$target.val(r.query.tokens.csrftoken === '+\\' ? 'WARNING: SESSION EXPIRED! LogIn again!' : t);
			}).fail(function(x) {
				$target.val(x);
			});
		},
		publish: function() {
			us.loadAndContinue(['ext.gadget.libAPI', 'ext.gadget.jquery.blockUI'], '__publish', arguments);
		},
		__publish: function(filekey, target, summary, text, $dlg, $btn) {
			var ext = filekey.match(/\.(\w{2,9})$/),
				extW = target.match(/\.(\w{2,9})$/);
			if (!ext && !extW) return;
			if (!extW) target += '.' + ext[1];
			
			$dlg.block();
			
			mw.libs.commons.api.$query({
				'action': 'query',
				'meta': 'tokens',
				'type': 'csrf'
			}).done(function(r) {
				var t = r.query.tokens.csrftoken;
				target = $.ucFirst(target.replace(/File:/, '').replace(/_/g, ' '));
				$.ajax({
					url: mw.util.wikiScript('api'),
					dataType: 'json',
					data: {
						'format': 'json',
						'action': 'upload',
						'filename': target,
						'comment': (summary || 'Publishing upload from [[Special:UploadStash]]') + ' using [[MediaWiki:EnhancedStash.js]]',
						'text': text,
						'filekey': filekey,
						'ignorewarnings': 1,
						'async': 1,
						'leavemessage': 1,
						'tags': 'EnhancedStash',
						'token': t
					},
					type: 'POST'
					// TODO: Implement in libAPI
				}).done(function(r) {
					$dlg.unblock();
					$btn.button('option', 'disabled', false);
					
					if (!r || !r.upload) {
						throw new Error('Empty or wrong API response. ' + JSON.stringify(r));
					}
					if (r.upload.result === "Success") {
						// TODO: ...
						mw.notify($('<div>').text('Uploaded ' + filekey + ' successfully to ').append( $('<a>', {
							// TODO: ...
							href: mw.util.getUrl('File:' + target),
							target: '_blank'
						}).text(target) ));
						$dlg.dialog('close');
						us.fileKeyMap[filekey].$el.hide();
					}
					if (r.upload.result === "Poll") {
						// TODO: ...
						mw.notify($('<div>').text('THIS MAY TAKE A WHILE. CHECK BACK TOMORROW: Uploaded ' + filekey + ' pending to ').append( $('<a>', {
							// TODO: ...
							href: mw.util.getUrl('File:' + target),
							target: '_blank'
						}).text(target) ));
						$dlg.dialog('close');
						us.fileKeyMap[filekey].$el.hide();
					}
				}).fail(function(x) {
					$btn.button('option', 'disabled', false);
					$dlg.unblock();
					throw new Error(x);
				});
			}).fail(function(x) {
				$btn.button('option', 'disabled', false);
				$dlg.unblock();
				throw new Error(x);
			});
		},
		install: function() {
			var $button = $('<button type="button" id="' + id + '"></button>')
					.text('Further Information')
					.attr('title', 'Displays thumbnails and other information for stashed files and allows you to publish completed uploads.')
					.button({
						icons: { primary: 'ui-icon-image' }
					})
					.css('float', rtl ? 'left' : 'right')
					.click(function() {
						us.run();
					});
					
			if ($('#' + id).length) return;
			
			$button.appendTo('#contentSub');
		}
	};
	mw.loader.using([
		'ext.gadget.JStack',
		'mediawiki.util',
		'jquery.ui'
	], function() {
		us.init();
		us.install();
		if (mw.util.getParamValue('withJS') === 'MediaWiki:EnhancedStash.js') {
			us.run();
		}
	});
	// Prefetch
	mw.loader.load(['ext.gadget.jquery.blockUI', 'ext.gadget.libAPI', 'jquery.ui', 'mediawiki.user']);
		
}(jQuery, mediaWiki));

// </nowiki>