User:Matěj Suchánek/markasduplicate.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * Simplifies marking items as duplicates and requesting articles to merge
 * 
 * You can enable it by adding this line to your common.js (or global.js) file:
 * mw.loader.load( '//www.wikidata.org/w/index.php?title=User:Matěj_Suchánek/markasduplicate.js&action=raw&ctype=text/javascript' );
 * 
 * After enabling, you will see "Mark as duplicate" link in the left sidebar.
 * 
 * IMPORTANT: This is not the place to report for wrong edits in your wiki! Please contact the user directly.
 * 
 * @author [[User:Matěj Suchánek]]
 */
// <nowiki>
( function ( mw, $ ) {

	if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 ) {
		return;
	}

	switch ( mw.config.get( 'wgUserLanguage' ) ) {
		default:
		case 'en':
			mw.messages.set( {
				'mark-as-duplicate': 'Mark as duplicate',
				'mark-as-duplicate-intro-page': 'You can request merging this article and possibly mark the Wikidata item as a duplicte.',
				'mark-as-duplicate-intro-item': 'You can mark this item as a duplicate of another and request merging articles on conflicting wikis.',
				'close': 'Close',
				'targetitem': 'The id of the second item:',
				'targetpage': 'The title of the second page:',
				'no-conflicts': 'No conflicts were found.',
				'watch-pages': 'Watch pages that were requested to merge',
				'mark-item-as-duplicate': 'Mark Wikidata item as duplicate (if connected)',
				'success-marked': 'The item was marked as duplicate.',
				'success-wikis': 'Merge was requested in $1.',
				'success-skipped': '$1 {{PLURAL:$2|was|were}} skipped.',
				'error-sameid': 'The second item\'s id has to be different to the current one\'s.',
				'error-samepage': 'The second page has to be different to the current one.',
				'error-invalidid': 'The given id is not a valid item identifier.',
				'error-notexisting': 'The item with the id $1 does not exist.',
				'error-api': 'There was an error editing the item: $1'
			} );
			break;
		case 'el':
			mw.messages.set( {
				'mark-as-duplicate': 'Σημειώστε ως διπλότυπο',
				'mark-as-duplicate-intro-page': 'You can request merging this article and possibly mark the Wikidata item as a duplicte.',
				'mark-as-duplicate-intro-item': 'Μπορείτε να επισημάνετε αυτό το αντικείμενο ως διπλότυπο ενός άλλου και να ζητήσετε τη συγχώνευση άρθρων σε αντικρουόμενα wiki.',
				'close': 'Κλείσιμο',
				'targetitem': 'Το αναγνωριστικό (id) του δεύτερου αντικειμένου:',
				'targetpage': 'The title of the second page:',
				'no-conflicts': 'Δεν βρέθηκαν συγκρούσεις.',
				'watch-pages': 'Παρακολουθήστε τις σελίδες που ζητήθηκε να συγχωνευθούν',
				'mark-item-as-duplicate': 'Σημειώστε το αντικείμενο Wikidata σαν διπλότυπο (αν είναι συνδεδεμένο)',
				'success-marked': 'Το αντικείμενο έχει σημειωθεί σαν διπλότυπο.',
				'success-wikis': 'Η συγχώνευση ζητήθηκε σε $1.',
				'success-skipped': '$1 παραλείφθηκε.',
				'error-sameid': 'Το δεύτερο αντικείμενο/αναγνωριστικό (id) πρέπει να είναι διαφορετικό από το υφιστάμενο.',
				'error-samepage': 'The second page has to be different to the current one.',
				'error-invalidid': 'Το δοσμένο αναγνωριστικό (id) δεν είναι έγκυρο αναγνωριστικό στοιχείου.',
				'error-notexisting': 'Το αντικείμενο με αναγνωριστικό (id) $1 δεν υπάρχει.',
				'error-api': 'Παρουσιάστηκε σφάλμα κατά την επεξεργασία του αντικειμένου: $1'
			} );
			break;
	}

	const
		INCLUDE_ANONS = 0x1 << 0,
		INCLUDE_BOTS = 0x1 << 1;

	var localApi, wikidataApi, repoApi, wb,
		entity, // current entity
		credit = 'using [[d:User:Matěj Suchánek/markasduplicate.js|markasduplicate.js]]',
		marked,
		baseRevId,
		templateLinks,
		allTemplateLinks,
		targetitem, // target item
		targetentity, // target entity
		dbname = mw.config.get( 'wgDBname' ),
		onWikidata = dbname === 'wikidatawiki';

	function getTemplatePattern( wiki, from, to ) {
		// $1 for template name (eg. "Merge")
		// $2 for the other article name
		switch ( wiki ) {
			case 'cawiki':
				return '{{Duplicat|$2|data={{subst:CURRENTYEAR}}}}';
			case 'cswiki':
				return '{{$1|$2|{{subst:#ifeq:$2|' + from + '|sem|tam}}}}';
			case 'dawiki':
				return '{{$1|$2|dato={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}';
			case 'dewiki':
				return '{{subst:$1|' + from + '|' + to + '}}';
			case 'enwiki':
				return '{{$1|$2|{{subst:DATE}}|discuss=Talk:' + to + '#Merger proposal}}';
			case 'eswiki':
				return '{{$1|t={{sust:CURRENTTIMESTAMP}}|$2}}';
			case 'frwiki':
				return '{{$1|' + from + '|' + to + '}}';
			case 'itwiki':
				return '{{$1|$2|data={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}';
			case 'jawiki':
			case 'svwiki':
				return '{{$1|$2|{{subst:DATE}}}}';
			case 'ptwiki':
				return '{{$1|$2|{{subst:DATA}}}}';
			case 'rowiki':
				return '{{$1|{{subst:PAGENAME}}|$2}}';
			case 'ruwiki':
				return '{{subst:Слить|1=$2|2=section=' + from + ' и ' + to + '}}';
			case 'ukwiki':
				return '{{$1|$2|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}';
			default:
				return '{{$1|$2}}';
		}
	}

	function createSpinner() {
		$( '#mad-result' ).html(
			$.createSpinner( {
				size: 'large',
				type: 'block'
			} )
		);
	}

	function showError( error ) {
		var parameters = Array.prototype.slice.call( arguments, 1 );
		$( '#mad-result' ).html(
			$( '<p>' )
			.attr( 'class', 'error' )
			.html( mw.message( 'error-' + error ).params( parameters ).parse() )
		);
	}

	function onError( error, result ) {
		showError( 'api', result && result.error && result.error.info || error );
		$( '#watch-toggle, #mark-item-toggle' ).attr( 'disabled', false );
	}

	function success( skipped, done ) {
		$( '#mad' ).dialog( 'close' );
		$( '#watch-toggle, #mark-item-toggle' ).attr( 'disabled', false );

		var msg_array = [];
		if ( done.length + skipped.length === 0 ) {
			msg_array.push( mw.msg( 'no-conflicts' ) );
		}
		if ( marked ) {
			msg_array.push( mw.msg( 'success-marked' ) );
		}
		if ( done.length ) {
			msg_array.push( mw.message( 'success-wikis', done.join( mw.msg( 'comma-separator' ) ) ).parse() );
		}
		if ( skipped.length ) {
			msg_array.push( mw.message( 'success-skipped', skipped.join( mw.msg( 'comma-separator' ) ), skipped.length ).parse() );
		}
		mw.notify( $( '<div>' ).html( msg_array.join( '<br>' ) ), {
			autoHide: false,
			title: mw.msg( 'mark-as-duplicate' )
		} );
	}

	function getSignificantContributors( wikiApi, from, to, limit, includeMask ) {
		var query = function ( title ) {
			return wikiApi.get( {
				action: 'query',
				prop: 'revisions',
				titles: title,
				formatversion: 2,
				rvdir: 'newer',
				rvexcludeuser: mw.config.get( 'wgUserName' ),
				rvprop: 'user|flags',
				rvslots: 'main',
				rvlimit: 'max',
			} );
		};
		return $.when( query( from ), query( to ) )
		.then( function ( data1, data2 ) {
			var editsPerUser = {},
				users = [],
				creators = [],
				revisions = Array.prototype.concat(
					data1[0].query.pages[0].revisions,
					data2[0].query.pages[0].revisions
				);
			revisions.forEach( function ( rev ) {
				if ( rev.anon === true && ( includeMask & INCLUDE_ANONS ) !== INCLUDE_ANONS ) {
					return;
				}
				if ( rev.minor === true ) {
					return;
				}
				if ( editsPerUser[rev.user] === undefined ) {
					editsPerUser[rev.user] = 1;
				} else {
					editsPerUser[rev.user]++;
				}
			} );
			creators = [ data1, data2 ].map( function ( obj ) {
				return obj[0].query.pages[0].revisions[0].user;
			} ).filter( function ( u ) {
				return !!editsPerUser[u]; // verify they are not anons or bots if not desired
			} );
			if ( creators.length === 2 && creators[0] === creators[1] ) {
				creators.pop();
			}
			users = Object.keys( editsPerUser )
			.filter( function ( u ) {
				return creators.indexOf( u ) === -1;
			} ).filter( function ( u ) {
				return editsPerUser[u] >= Math.ceil( Math.log10( revisions.length ) );
			} ).sort( function ( a, b ) {
				return editsPerUser[b] - editsPerUser[a];
			} );
			var all_users = Array.prototype.concat( creators, users );
			if ( ( includeMask & INCLUDE_BOTS ) !== INCLUDE_BOTS ) {
				return wikiApi.get( {
					action: 'query',
					list: 'users',
					formatversion: 2,
					usprop: 'groups',
					ususers: all_users,
				} ).then( function ( data ) {
					var bots = {};
					data.query.users.forEach( function ( obj ) {
						if ( obj.groups.indexOf( 'bot' ) !== -1 ) {
							bots[obj.name] = true;
						}
					} );
					return all_users.filter( function ( u ) { return !bots[u]; } );
				} );
			} else {
				return all_users;
			}
		} ).then( function ( users ) {
			return users.slice( 0, limit );
		} );
	}

	function doWikiSpecificStuff( wikiApi, wiki, from, to ) {
		var promise, deferred = $.Deferred(),
			text = ''
				+ 'I propose to merge [[' + from + ']] into [[' + to + ']]. '
				+ 'It seems they duplicate each other. <small>(Generated '
				+ credit + '.)</small> --~~~~';
		switch ( wiki ) {
			case 'dewiki':
				promise = wikiApi.get( {
					action: 'expandtemplates',
					prop: 'wikitext',
					text: 'Wikipedia:Redundanz/{{CURRENTMONTHNAME}} {{CURRENTYEAR}}',
					title: 'Wikipedia:Redundanz',
				} )
				.then( function ( data ) {
					return wikiApi.edit(
						data.expandtemplates.wikitext,
						function ( rev ) {
							if ( new RegExp( '== *' + mw.util.escapeRegExp( '[[' + from + ']] - [[' + to + ']]' ) ).test( rev.content )
								|| new RegExp( '== *' + mw.util.escapeRegExp( '[[' + to + ']] - [[' + from + ']]' ) ).test( rev.content )
							) {
								return $.Deferred().reject( 'nochange' );
							}
							// TODO: {{subst:Redundanz}}
							return {
								notminor: 1,
								section: 'new',
								sectiontitle: '[[' + from + ']] - [[' + to + ']]',
								text: text,
								watchlist: 'nochange',
							};
						}
					);
				} );
				break;
			case 'enwiki':
				promise = wikiApi.edit(
					// xxx: this may take ns information from Wikidata
					mw.Title.newFromText( to ).getTalkPage(),
					function ( rev ) {
						if ( /== *Merger proposal/.test( rev.content ) ) {
							return $.Deferred().reject( 'nochange' );
						}
						return getSignificantContributors( wikiApi, from, to, 5, 0x0 )
						.then( function ( users ) {
							var ping = users.length > 0 ? ( '{{ping|' + users.join( '|' ) + '}} ' ) : '';
							return {
								notminor: 1,
								section: 'new',
								sectiontitle: 'Merger proposal',
								text: ping + text,
								watchlist: 'nochange',
							};
						} );
					}
				);
				break;
			case 'frwiki':
				promise = wikiApi.edit(
					'Wikipédia:Pages à fusionner',
					function ( rev ) {
						if ( new RegExp( '== *' + mw.util.escapeRegExp( '[[' + from + ']] et [[' + to + ']]' ) ).test( rev.content )
							|| new RegExp( '== *' + mw.util.escapeRegExp( '[[' + to + ']] et [[' + from + ']]' ) ).test( rev.content )
						) {
							return $.Deferred().reject( 'nochange' );
						}
						return {
							notminor: 1,
							section: 'new',
							sectiontitle: '[[' + from + ']] et [[' + to + ']]',
							text: text,
							watchlist: 'preferences',
						};
					}
				);
				break;
			case 'ruwiki':
				var ruDeferred = $.Deferred(),
					resolve = function () { ruDeferred.resolve() },
					reject = function () { ruDeferred.reject() };
				promise = ruDeferred.promise();
				wikiApi.get( {
					action: 'expandtemplates',
					prop: 'wikitext',
					text: 'Википедия:К объединению/{{CURRENTDAY}} {{CURRENTMONTHNAMEGEN}} {{CURRENTYEAR}}',
					title: 'Википедия:К объединению',
				} )
				.then( function ( data ) {
					const title = data.expandtemplates.wikitext;
					return wikiApi.edit(
						title,
						function ( rev ) {
							if ( new RegExp( '== *' + mw.util.escapeRegExp( '[[' + from + ']] и [[' + to + ']]' ) ).test( rev.content )
								|| new RegExp( '== *' + mw.util.escapeRegExp( '[[' + to + ']] и [[' + from + ']]' ) ).test( rev.content )
							) {
								return $.Deferred().reject( 'nochange' );
							}
							return {
								notminor: 1,
								section: 'new',
								sectiontitle: '[[' + from + ']] и [[' + to + ']]',
								text: text,
								watchlist: 'preferences',
							};
						}
					)
					.then( resolve, function ( err ) {
						if ( err === 'nocreate-missing' ) {
							wikiApi.create(
								title,
								{ watchlist: 'preferences' },
								'{{КОБ-Навигация}}\n== [[' + from + ']] и [[' + to + ']] ==\n' + text
							)
							.then( resolve, reject );
						} else {
							reject();
						}
					} );
				}, reject );
				break;
			default:
				promise = $.Deferred().resolve().promise();
		}
		promise.then( function () { deferred.resolve(); }, function () { deferred.reject(); } );
		return deferred.promise();
	}

	function prepareRequest( wikiApi, wiki, templatePattern ) {
		var template = templateLinks[wiki].title.slice( templateLinks[wiki].title.indexOf( ':' ) + 1 );
		return function( first, second ) {
			return wikiApi.postWithEditToken( {
				formatversion: 2,
				action: 'edit',
				assert: 'user',
				title: first,
				notminor: 1,
				prependtext: templatePattern
					.replace( /\$1/g, template )
					.replace( /\$2/g, second ) + '\n',
				summary: '{{' + template + '|[[' + second + ']]}}, ' + credit,
				watchlist: $( '#watch-toggle' ).prop( 'checked' ) === true ? 'watch' : 'preferences'
			} );
		};
	}

	function requestMerge( wikiApi, wiki, from, to ) {
		var templatePattern = getTemplatePattern( wiki, from, to ),
			request = prepareRequest( wikiApi, wiki, templatePattern ),
			mergeRequested = $.Deferred(),
			reject = function () { mergeRequested.reject(); };

		wikiApi.get( {
			formatversion: 2,
			action: 'query',
			prop: 'templates',
			redirects: 1,
			titles: [ from, to ],
			tltemplates: allTemplateLinks[wiki]
		} )
		.then( function ( data ) {
			var result = data.query.pages;
			if (
				data.query.redirects !== undefined ||
				result[0].missing !== undefined ||
				result[1].missing !== undefined
			) {
				reject();
				return;
			}

			doWikiSpecificStuff( wikiApi, wiki, from, to ).then( function () {
				var firstP,
					doFirst = result[0].templates === undefined,
					doSecond = result[1].templates === undefined;
				if ( doFirst ) {
					firstP = request( result[0].title, result[1].title );
				} else {
					firstP = $.Deferred().reject();
				}
				firstP.then( function () {
					if ( doSecond ) {
						request( result[1].title, result[0].title )
						.then( function () {
							mergeRequested.resolve();
						}, reject );
					} else {
						reject();
					}
				}, function () {
					if ( doSecond ) {
						request( result[1].title, result[0].title )
						.then( reject, reject );
					} else {
						reject();
					}
				} );
			}, reject );
		}, reject );

		return mergeRequested.promise();
	}

	function add( array, wiki ) {
		return function() {
			array.push( wiki );
		};
	}

	function notifyWikis( conflicts, skipped, done ) {
		var wiki;
		while ( true ) {
			if ( !conflicts.length ) {
				return;
			}
			wiki = conflicts.pop();
			if ( !templateLinks[wiki] ) {
				skipped.push( wiki );
				continue;
			}
			break;
		}

		var site = wb.sites.getSite( wiki ),
			wikiApi = new mw.ForeignApi( site.getApi() ),
			deferred = $.Deferred();

		requestMerge(
			wikiApi,
			wiki,
			entity.sitelinks[wiki].title,
			targetentity.sitelinks[wiki].title
		)
		.then( add( done, wiki ), add( skipped, wiki ) )
		.then( function () { deferred.resolve(); }, function () { deferred.resolve(); } );

		return deferred.then( function () {
			return notifyWikis( conflicts, skipped, done );
		} );
	}

	function getTemplates() {
		return repoApi.getEntities( [ 'Q6919004', 'Q5625904', 'Q6041546', 'Q25990047' ], [ 'sitelinks' ] )
		.then( function ( data ) {
			allTemplateLinks = {};
			$.each( data.entities, function ( id, entity ) {
				if ( id === 'Q6919004' ) {
					templateLinks = entity.sitelinks;
				}
				$.each( entity.sitelinks || {}, function ( wiki, link ) {
					if ( allTemplateLinks[wiki] === undefined ) {
						allTemplateLinks[wiki] = [];
					}
					allTemplateLinks[wiki].push( link.title );
				} );
			} );
		} );
	}

	function markItemAsDuplicate() {
		for ( var j in ( entity.claims && entity.claims.P31 ) || [] ) {
			if ( entity.claims.P31[j].mainsnak.datavalue && entity.claims.P31[j].mainsnak.datavalue.value.id == 'Q17362920' ) {
				marked = false;
				return $.Deferred().resolve().promise();
			}
		}

		return wikidataApi.postWithEditToken( {
			formatversion: 2,
			action: 'wbcreateclaim',
			entity: entity.id,
			property: 'P31',
			snaktype: 'value',
			value: JSON.stringify( { id: 'Q17362920' } ),
			baserevid: baseRevId,
			summary: credit
		} )
		.then( function ( data ) {
			return wikidataApi.postWithEditToken( {
				formatversion: 2,
				action: 'wbsetqualifier',
				claim: data.claim.id,
				property: 'P642',
				snaktype: 'value',
				value: JSON.stringify( { id: targetitem } ),
				baserevid: data.pageinfo.lastrevid,
				summary: credit
			} );
		} )
		.then( function ( data ) {
			if ( onWikidata ) {
				mw.config.set( 'wgCurRevisionId', data.pageinfo.lastrevid );
			}
			marked = true;
		} );
	}

	function run() {
		var input = $( '#mad-target' ).val(),
			skipped = [],
			done = [],
			error = false,
			chain;
		createSpinner();
		if ( onWikidata ) {
			targetitem = input.toUpperCase();
			if ( entity.id === targetitem ) {
				showError( 'sameid' );
				return;
			}
			if ( targetitem.match( /^Q[1-9]\d*$/g ) === null ) {
				showError( 'invalidid' );
				return;
			}
			chain = repoApi.getEntities( targetitem, [ 'info', 'sitelinks' ] )
			.then( function ( data ) {
				if ( !data.entities.hasOwnProperty( targetitem ) ) {
					error = true; // xxx: hack
					showError( 'notexisting', mw.html.element( 'a', { href: mw.util.getUrl( targetitem ) }, targetitem ) );
					return;
				}
	
				targetentity = data.entities[targetitem];
	
				if ( targetentity.hasOwnProperty( 'redirects' ) ) {
					targetitem = targetentity.redirects.to;
				}
	
				var conflicts = [];
				for ( var wiki in entity.sitelinks ) {
					if ( targetentity.sitelinks && targetentity.sitelinks[wiki] ) {
						conflicts.push( wiki );
					}
				}

				if ( !conflicts.length ) {
					return;
				}

				return $.when(
					markItemAsDuplicate(),
					getTemplates()
				)
				.then( function () {
					return notifyWikis( conflicts, skipped, done );
				} );
			} );
		} else {
			const pagename = mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
			targetentity = null;
			if ( pagename === input ) {
				showError( 'error-samepage' );
				return;
			}
			chain = repoApi.getEntitiesByPage( dbname, input, [ 'info', 'sitelinks' ] )
			.then( function ( data ) {
				$.each( data.entities, function ( i, e ) {
					if ( i !== '-1' ) {
						targetentity = e;
						targetitem = e.id;
					}
					return false;
				} );

				var markedAsDuplicate;
				if ( entity && targetentity && $( '#mark-item-toggle' ).prop( 'checked' ) === true ) {
					markedAsDuplicate = markItemAsDuplicate();
				} else {
					markedAsDuplicate = $.Deferred().resolve();
				}
				return $.when(
					markedAsDuplicate,
					getTemplates()
				)
				.then( function () {
					if ( !templateLinks[dbname] ) {
						skipped.push( dbname );
						return;
					}

					var deferred = $.Deferred();
					requestMerge( localApi, dbname, pagename, input )
					.then( add( done, dbname ), add( skipped, dbname ) )
					.then( function () { deferred.resolve(); }, function () { deferred.resolve(); } );
					return deferred;
				} );
			} );
		}
		chain.then( function () {
			if ( !error ) { // xxx: hack
				success( skipped, done );
			}
		}, onError );
	}

	function openDialog() {
		$( '#mad-result' ).empty();
		$( '#mad' ).dialog( 'open' );
	}

	function init() {
		var portletLink = mw.util.addPortletLink(
			'p-tb',
			'#',
			mw.msg( 'mark-as-duplicate' ),
			't-mark-as-duplicate',
			mw.msg( 'mark-as-duplicate' )
		);

		// Create dialog
		$( '<div>' )
		.attr( 'id', 'mad' )
		.append(
			$( '<form>' )
			.submit( function ( event ) {
				event.preventDefault();
				run();
			} )
			.append(
				$( '<fieldset>' )
				.attr( 'id', 'mark-as-duplicate-form' )
				.append(
					$( '<legend>' )
					.text( mw.msg( 'mark-as-duplicate' ) ),
					// </legend>
					$( '<p>' )
					.attr( 'id', 'mark-as-duplicate-intro' )
					.text( mw.msg( onWikidata ? 'mark-as-duplicate-intro-item' : 'mark-as-duplicate-intro-page' ) ),
					// </p>
					$( '<p>' )
					.append(
						$( '<label>' )
						.attr( {
							'for': 'mad-target',
							'class': 'mad-label'
						} )
						.text( mw.msg( onWikidata ? 'targetitem' : 'targetpage' ) + ' ' ),
						// </label>
						$( '<input>' )
						.attr( {
							'type': 'text',
							'id': 'mad-target',
							'class': 'mad-input'
						} )
					),
					$( '<p>' )
					.append(
						$( '<input>' )
						.attr( {
							'type': 'checkbox',
							'id': 'watch-toggle'
						} )
					) // </input>
					.append(
						$( '<label>' )
						.attr( 'for', 'watch-toggle' )
						.text( mw.msg( 'watch-pages' ) )
					), // </label>
					onWikidata ? '' : $( '<p>' )
					.append(
						$( '<input>' )
						.attr( {
							'type': 'checkbox',
							'id': 'mark-item-toggle'
						} )
					) // </input>
					.append(
						$( '<label>' )
						.attr( 'for', 'mark-item-toggle' )
						.text( mw.msg( 'mark-item-as-duplicate' ) )
					) // </label>
				) // </p>
			), // </fieldset>
			// </form>
			$( '<p>' )
			.attr( 'id', 'mad-result' )
		)
		.dialog( {
			dialogClass: 'mad-dialog',
			title: '<img src="//upload.wikimedia.org/wikipedia/commons/thumb/5/52/Merge-arrows.svg/20px-Merge-arrows.svg.png" width="20" alt="!" title="!" style="margin-right: 1.5em;">' + mw.message( 'mark-as-duplicate' ).escaped(),
			autoOpen: false,
			modal: true,
			width: 500,
			buttons: [ {
				id: 'mad-button-mark',
				text: mw.msg( 'mark-as-duplicate' ),
				click: function ( event ) {
					event.preventDefault();
					$( '#watch-toggle, #mark-item-toggle' ).attr( 'disabled', true );
					run();
				}
			}, {
				id: 'mad-button-close',
				text: mw.msg( 'close' ),
				click: function ( event ) {
					event.preventDefault();
					$( '#mad' ).dialog( 'close' );
				}
			} ]
		} );

		$( portletLink ).click( function ( event ) {
			event.preventDefault();
			openDialog();
		} );
	}

	var entityLoaded = $.Deferred(),
		apiLoaded = mw.loader.using( [
			'mediawiki.api', 'mediawiki.ForeignApi', 'wikibase.api.RepoApi',
			'wikibase.sites'
		] )
		.then( function () {
			localApi = new mw.Api();
			wb = wikibase;
			if ( onWikidata ) {
				wikidataApi = localApi;
				repoApi = new wb.api.RepoApi( localApi );
			} else {
				wikidataApi = new mw.ForeignApi( wb.sites.getSite( 'wikidatawiki' ).getApi() );
				repoApi = new wb.api.RepoApi( wikidataApi );
			}
		} );

	if ( onWikidata ) {
		baseRevId = mw.config.get( 'wgCurRevisionId' );
		mw.hook( 'wikibase.entityPage.entityLoaded' ).add( function ( e ) {
			entity = e;
			entityLoaded.resolve();
		} );
	} else {
		apiLoaded
		.then( function () {
			// XXX: heavy request on each page view
			return repoApi.getEntitiesByPage(
				dbname,
				mw.config.get( 'wgPageName' ),
				[ 'info', 'claims' ]
			)
		} )
		.then( function ( data ) {
			$.each( data.entities, function ( i, e ) {
				if ( i !== '-1' ) {
					entity = e;
					baseRevId = e.lastrevid;
				}
				entityLoaded.resolve();
				return false;
			} );
		}, function () {
			entityLoaded.reject();
		} );
	}

	$.when(
		mw.loader.using( [
			'jquery.spinner', 'jquery.ui', 'mediawiki.jqueryMsg',
			'mediawiki.util'
		] ),
		apiLoaded,
		entityLoaded,
		$.ready
	)
	.then( init );

} ( mediaWiki, jQuery ) );
//</nowiki>