User:Jeblad/external-articles-dev-v0.1/script.js

From Meta, a Wikimedia project coordination wiki

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.
var marker = 'external-article';
var agent = 'Gadget/ExternalArticle (0.1)';
var entity = null;

var domains = {
	snl: {
		encyclopedia: 'Store norske leksikon',
		url: 'https://snl.no/',
		category: 'Artikler med SNL-lenker fra Wikidata',
		check: true,
		property: 'P4342'
	},
	nbl: {
		encyclopedia: 'Norsk biografisk leksikon',
		url: 'https://nbl.snl.no/',
		category: 'Artikler med NBL-lenker fra Wikidata',
		check: true,
		property: 'P5080'
	},
	nkl: {
		encyclopedia: 'Norsk kunstnerleksikon',
		url: 'https://nkl.snl.no/',
		category: 'Artikler med NKL-lenker fra Wikidata',
		check: true,
		property: 'P5081'
	},
	sml: {
		encyclopedia: 'Store medisinske leksikon',
		url: 'https://sml.snl.no/',
		category: 'Artikler med SML-lenker fra Wikidata',
		check: true,
		property: 'P5082'
	},
	nb: {
		encyclopedia: 'Naturbase',
		url: '',
		check: true,
		property: 'P1732'
	}
};

domains.snl.apiCall = function( context, title ) {
	jQuery.ajax( {
		dataType: "json",
		url: context.url + mw.util.wikiUrlencode( title ) + '.json',
		context: context,
		headers: {
			'User-Agent': navigator.userAgent+' '+agent
		},
		success: function( data, textStatus, jqXHR ){
			buildNotification( this, data );
		},
		error: function( jqXHR, textStatus, errorThrown ){
			if ( textStatus === 'error' && errorThrown === '' ) {
				prepareRequest( context );
			}
		}
	} );
};

domains.nbl.apiCall = domains.snl.apiCall;
domains.nkl.apiCall = domains.snl.apiCall;
domains.sml.apiCall = domains.snl.apiCall;

var fallbacks = {
	'external-article-title': 'External article',
	'external-article-sitesub': 'From $1',
	'external-article-enable': 'Enable externals',
	'external-article-enable-desc': 'Enable loading of similar external pages',
	'external-article-disable': 'Disable externals',
	'external-article-disable-desc': 'Disable loading of similar external pages',
	'external-article-button-dismiss': 'Dismiss',
	'external-article-button-connect': 'Connect',
	'external-article-connect-success-desc': 'External article is permanently added to the item.',
	'external-article-connect-failure-property-desc': 'Adding the external article to the item trough the property failed.',
	'external-article-connect-failure-qualifier-desc': 'Adding a qualifier to property for the external article failed.',
	'external-article-connect-failure-token-desc': 'Getting the necessary token to be able to edit the item failed.',
	'external-article-purge-failure-desc': 'Purging the page failed.',
	'external-article-automatic-dismiss-desc': 'Alternate titles are exhausted and the external article is temporarily dismissed.',
	'external-article-missing-call-handler': 'Could not find a call handler for requesting the external article.',
	'external-article-failure-title': 'Failure',
	'external-article-general-api-failure-desc': 'General unspecific API failure.',
	'external-article-general-api-warning-desc': 'General specific API warning.',
	'external-article-loader-failure-desc': 'Loader failed to deliver the modules.',
	'external-article-not-connected-desc': 'Could not find a connected item.',
	'external-article-property-exist-desc': 'The property already exist in the item.',
	'external-article-previously-dismissed-desc': 'The external articles are previously dismissed, either manually or automatically.',
	'external-article-edit-page': 'Edit page',
	'external-article-mapping-relation': 'External is',
	'external-article-skos-exact': 'exact',
	'external-article-skos-close': 'close',
	'external-article-skos-related': 'related',
	'external-article-skos-narrower': 'narrower',
	'external-article-skos-broader': 'broader'
};

var logWarnings = function( data ) {
	for ( var lst in data.warnings ) {
		for ( var msg in data.warnings ) {
			mw.log( 'ExternalArticle ('+lst+'): ', data.warnings[lst][ msg ] );
		}
	}
};

var buildFailNotice = function( context, msg ) {
	var $html = $( '<div>' );
	var $siteSub = $('<div>')
	.addClass( 'mw-external-content-sitesub' )
	.text( mw.message( 'external-article-sitesub', context.encyclopedia ).escaped() );
	var $siteTitle = $('<div>')
	.addClass( 'mw-external-content-sitetitle' )
	.text( mw.message( 'external-article-failure-title' ) );
	var $para = $( '<p>' )
	.text( msg.escaped() );
	var $content = $( '<div>' )
	.addClass( 'mw-external-content' )
	.append( $siteTitle )
	.append( $siteSub )
	.append( $para )
	.appendTo( $html );
	mw.notify( $html, {
		title: mw.message( 'external-article-title' ).escaped(),
		autoHide: false,
		tag: context.encyclopedia
	} );
};

var createFailHandler = function( context, msg ) {
	return function() {
		buildFailNotice( context, msg || mw.message( 'external-article-general-api-failure-desc' ) );
	};
};

var createDoneHandler = function( context, msg ) {
	return function( data ) {
		if ( data.warnings ) {
			logWarnings( data );
			buildFailNotice( context, msg || mw.message( 'external-article-general-api-warning-desc' ) );
		}
	};
};

var purge = function( context ) {
	mw.log( 'ExternalArticle (entered purge): ', context );
	var api = new mw.Api();
	var articleId = mw.config.get( 'wgArticleId' );
	return api.post( {
		action: 'purge',
		pageids: articleId
	} )
	.fail ( createFailHandler( context ) )
	.done( createDoneHandler( context, mw.message( 'external-article-purge-failure-desc' ) ) );
};

var apiSetQualifier = function( context, csrfToken, claimId, qualifier ) {
	mw.log( 'ExternalArticle (entered setQualifier): ', context );
	var foreign = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
	return foreign.post( {
		action: 'wbsetqualifier',
		snaktype: 'value',
		token: csrfToken,
		claim: claimId,
		property: 'P4390',
		value: JSON.stringify({ 'id': qualifier }),
		summary: agent
	} )
	.fail ( createFailHandler( context ) )
	.done( createDoneHandler( context, mw.message( 'external-article-connect-failure-qualifier-desc' ) ) );
};

var apiCreateClaim = function( context, csrfToken, entityId, title ) {
	mw.log( 'ExternalArticle (entered createClaim): ', context );
	var foreign = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
	return foreign.post( {
		action: 'wbcreateclaim',
		snaktype: 'value',
		token: csrfToken,
		entity: entityId,
		property:context.property,
		value:JSON.stringify(title.replace(/\s+/g, '_')),
		summary: agent
	} )
	.fail ( createFailHandler( context ) )
	.done( createDoneHandler( context, mw.message( 'external-article-connect-failure-property-desc' ) ) );
};

var apiQueryToken = function( context ) {
	mw.log( 'ExternalArticle (entered queryToken): ', context );
	var foreign = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
	return foreign.get( {
		action: 'query',
		meta: 'tokens'
	} )
	.fail ( createFailHandler( context ) )
	.done( createDoneHandler( context, mw.message( 'external-article-connect-failure-token-desc' ) ) );
};

var connect = function( context, ext, qualifier ) {
	var csrfToken = null;
	return apiQueryToken( context )
	.done( function( data, success ) {
		csrfToken = data.query.tokens.csrftoken;
		apiCreateClaim( context, csrfToken, entity.id, ext.title )
		.done( function( data ) {
			apiSetQualifier( context, csrfToken, data.claim.id, qualifier )
			.done( function( data ) {
				mw.notify( mw.message( 'external-article-connect-success-desc' ).escaped(), {
					title: context.encyclopedia,
					autoHide: true,
					autoHideSeconds: 'short',
					tag: context.encyclopedia
				} );
				purge( context );
			} );
		} );
	} );
};

var buildNotification = function( context, data ) {
	var $html = $( '<div>' );
	var $siteSub = $('<div>')
	.addClass( 'mw-external-content-sitesub' );
	var $siteTitle = $('<div>')
	.addClass( 'mw-external-content-sitetitle' );
	var $content = $( '<div>' )
	.addClass( 'mw-external-content' )
	.css( {
		'max-height': '350px',
		'overflow': 'hidden',
		'text-overflow': 'ellipsis'
	} )
	.append( $siteTitle )
	.append( $siteSub );
	var connectBtn = new OO.ui.ButtonInputWidget( {
		label: mw.message( 'external-article-button-connect' ).escaped(),
		value: 'connect',
		flags: [ 'progressive' ]
	} );
	var dismissBtn = new OO.ui.ButtonInputWidget( {
		label: mw.message( 'external-article-button-dismiss' ).escaped(),
		value: 'dismiss',
		flags: []
	} );
	/*
	var editVal = sessionStorage.getItem( marker + '-' + context.name + '-edit' );
	var editCbx = new OO.ui.CheckboxInputWidget( {
		value: 'edit',
		selected: 'edit' === editVal
	} );
	*/
	var assocLst = new OO.ui.DropdownInputWidget( {
		label: null,
		options: [
			{ data: 'Q39893449', label: mw.message( 'external-article-skos-exact' ).escaped() },
			{ data: 'Q39893184', label: mw.message( 'external-article-skos-close' ).escaped() },
			{ data: 'Q39894604', label: mw.message( 'external-article-skos-related' ).escaped() },
			{ data: 'Q39893967', label: mw.message( 'external-article-skos-narrower' ).escaped() },
			{ data: 'Q39894595', label: mw.message( 'external-article-skos-broader' ).escaped() }
		]
	} );
	var fieldset = new OO.ui.FieldsetLayout( {
		label: null,
		items: [
			new OO.ui.FieldLayout( assocLst, {
				label: mw.message( 'external-article-mapping-relation' ).escaped()
			} ),
			/*
			new OO.ui.FieldLayout( editCbx, {
				label: mw.message( 'external-article-edit-page' ).escaped(),
				align: 'inline'
			} ),
			*/
			new OO.ui.FieldLayout(
				new OO.ui.Widget( {
					content: [
						new OO.ui.HorizontalLayout( {
							items: [
								dismissBtn,
								connectBtn
								] } ) ] } ) )
		] } );
	var form = new OO.ui.FormLayout( {
		$content: fieldset.$element,
		action: '/api/formhandler',
		method: 'get'
	} );
	/*
	editCbx.$element.on( 'change', function () {
		sessionStorage.setItem( marker + '-' + context.name + '-edit', editCbx.isSelected() ? 'edit' : 'no-edit' );
	} );
	*/
	connectBtn.$element.on( 'click', null, context, function( event ) {
		connect( context, data, assocLst.value );
		return true;
	} );
	dismissBtn.$element.on( 'click', null, context, function( event ) {
		/*
		mw.notify( mw.message( 'external-article-button-dismiss-success-desc' ).escaped(), {
			title: context.encyclopedia,
			autoHide: true,
			autoHideSeconds: 'short',
			tag: context.encyclopedia
		} );
		*/
		queryRequest( context );
		return true;
	} );
	$( '<a>' ).attr( { 'href': data.url } ).text( data.title ).appendTo( $siteTitle );
	$siteSub.text( mw.message( 'external-article-sitesub', context.encyclopedia ).escaped() );
	$( data.xhtml_body ).find( 'p:first' ).appendTo( $content );
	$content.appendTo( $html );
	form.$element.appendTo( $html );
	form.$element.on( 'click', function( event ) {
			event.stopPropagation();
	} );
	mw.notify( $html, {
		title: mw.message( 'external-article-title' ).escaped(),
		autoHide: false,
		tag: context.encyclopedia,
		type: marker
	} );
};

var prepareRequest = function( context ) {
	if ( context.index === undefined ) {
		context.index = -1;
	}
	if ( context.index >= context.titles.length ) {
		var aid = marker + '-' + context.name + '-' + mw.config.get( 'wgArticleId' );
		sessionStorage.setItem( aid, 'dismissed' );
		mw.notify( mw.message( 'external-article-automatic-dismiss-desc' ).escaped(), {
			title: context.encyclopedia,
			autoHide: true,
			autoHideSeconds: 'short',
			tag: context.encyclopedia
		} );
		return;
	}
	if ( context.apiCall === undefined ) {
		mw.notify( mw.message( 'external-article-missing-call-handler' ).escaped(), {
			title: context.encyclopedia,
			autoHide: true,
			autoHideSeconds: 'short',
			tag: context.encyclopedia
		} );
		return;
	}
	context.apiCall( context, context.titles[ context.index++ ] );
	//apiSNLRequest();
};

var stripParenthesis = function( str ) {
	return str.replace( /\s*\(.*?\)\s*$/, '' );
};

var toLowerCaseFirst = function( str ) {
	return str[ 0 ].toLowerCase() + str.slice( 1 );
};

var inContentNamespace = function( namespaces ) {
	return 0 <= namespaces.indexOf( mw.config.get( 'wgNamespaceNumber' ) );
}

var isAction = function( action ) {
	return 'view' === mw.config.get( 'wgAction' )
}

$.when( mw.loader.using( [ 'mediawiki.api.messages', 'mediawiki.util', 'mediawiki.ForeignApi', 'oojs-ui-core' ] ) )
	.then( function() {
		"use strict";
		return new mw.Api().getMessages( Object.keys( fallbacks ), { amlang: mw.config.get( 'wgUserLanguage' ) } )
		.fail( function() {
			$.each( fallbacks, function( key, message ) {
				mw.messages.set( key, message );
			} );
		} )
		.then( function( messages ) {
			var msgs = {};
			jQuery.extend( msgs, fallbacks, messages );
			$.each( msgs, function( key, message ) {
				mw.messages.set( key, message );
			} );
		} );
	} )
	.fail( function() {
		$.each( fallbacks, function( key, message ) {
			mw.messages.set( key, message );
		} );
		mw.notify( mw.message( 'external-article-loader-failure-desc' ).escaped(), {
			title: mw.message( 'external-article-failure-title' ).escaped(),
			autoHide: true
		} );
	})
	.then( function() {
		"use strict";
		// bail out?
		if ( !inContentNamespace( mw.config.get( 'wgContentNamespaces' ) )
			|| !isAction( 'view' )
			|| mw.util.getParamValue( 'type' )
			|| mw.util.getParamValue( 'diff' )
			|| mw.util.getParamValue( 'redirect' )
			|| mw.util.getParamValue( 'veaction' )
		) {
			mw.log( 'ExternalArticle: ', 'terminates' );
			return;
		}
		// check if the external article is previously dismissed
		Object.keys( domains ).map( function( name ) {
			var aid = marker + '-' + name + '-' + mw.config.get( 'wgArticleId' );
			var domain = domains[ name ];
			domain.name = name;
			domain.check &= sessionStorage.getItem( aid ) !== 'dismissed';
		} );
		// extend caction menu
		var queryDomains = function() {
			var name = null;	// @todo should use "let", but this is es5
			var domain = null;	// @todo should use "let", but this is es5
			for ( name in domains ) {
				domain = domains[ name ];
				if ( domain.check && domain.titles ) {
					prepareRequest( domain );
				}
			}
		};
		var dismissDomains = function() {
			$( '.mw-notification-type-'+marker ).trigger( 'click' );
		};
		mw.util.addPortletLink(
			'p-cactions', '#',
			mw.message( 'external-article-enable' ).escaped(),
			'ca-enable-externals',
			mw.message( 'external-article-enable-desc' ).escaped(),
			null, null
		);
		$( '#ca-enable-externals' ).on( 'click', function() {
			$( '#ca-enable-externals' ).hide();
			$( '#ca-disable-externals' ).show();
			sessionStorage.setItem( marker + '-' + 'state', 'enabled' );
			queryDomains();
		} );
		mw.util.addPortletLink(
			'p-cactions', '#',
			mw.message( 'external-article-disable' ).escaped(),
			'ca-disable-externals',
			mw.message( 'external-article-disable-desc' ).escaped(),
			null, null
		);
		$( '#ca-disable-externals' ).on( 'click', function() {
			$( '#ca-disable-externals' ).hide();
			$( '#ca-enable-externals' ).show();
			sessionStorage.setItem( marker + '-' + 'state', 'disabled' );
			dismissDomains();
		} );
		var state = sessionStorage.getItem( marker + '-' + 'state' );
		$( ( ( state || 'enabled' ) === 'enabled' ) ? '#ca-enable-externals' : '#ca-disable-externals' ).hide();

		var foreign = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );
		var title = mw.config.get( 'wgTitle' );
		var dbName = mw.config.get( 'wgDBname' );
		var language = mw.config.get( 'wgContentLanguage' );
		foreign.get( { 
			action: 'wbgetentities',
			titles: title,
			sites: dbName,
			props: 'aliases|labels|sitelinks|claims',
			languages: language,
			sitefilter: dbName
		} )
		.done( function( data, success ) {
			mw.log( 'ExternalArticle (wbgetentities): ', data );
			if ( data.warnings || !data.success ) {
				var content;
				if ( !data.success ) {
					content = mw.message( 'external-article-general-api-failure-desc' ).escaped();
				}
				else {
					content = $( '<dl>' );
					for ( var msg in data.warnings.wbgetentities ) {
						$( '<li>' ).text( data.warnings.wbgetentities[ msg ] ).appendTo( content );
					}
				}
				mw.notify( content, {
					title: mw.message( 'external-article-failure-title' ).escaped(),
					autoHide: true
				} );
				return;
			}
			//var entity = null;
			var found = false;
			if ( data.entities ) {
				for ( var eid in data.entities ) {
					entity = data.entities[ eid ];
					if ( entity.sitelinks ) {
						for ( var sid in entity.sitelinks ) {
							var sitelink = entity.sitelinks[ sid ];
							found = sitelink.site == dbName && sitelink.title == title;
							if ( found ) {
								break;
							}
						}
					}
					if ( found ) {
						break;
					}
				}
			}
			if ( !found ) {
				mw.log( 'ExternalArticle: ', 'about to terminate, entity not found' );
				mw.notify( mw.message( 'external-article-not-connected-desc' ).escaped(), {
					title: mw.message( 'external-article-failure-title' ).escaped(),
					autoHide: true
				} );
				return;
			}
			// check if the external article is previously dismissed
			var name = null;	// @todo should use "let", but this is es5
			var domain = null;	// @todo should use "let", but this is es5
			for ( name in domains ) {
				domain = domains[ name ];
				domain.check &= !entity.claims[domain.property];
				if ( false && !domain.check ) {
					mw.notify( mw.message( entity.claims[domain.property]
						? 'external-article-property-exist-desc'
						: 'external-article-previously-dismissed-desc' ).escaped(), {
						title: domain.encyclopedia,
						autoHide: true,
						autoHideSeconds: 'short'
					} );
					continue;
				}
				var titles = [];
				var i, l;
				titles.push( stripParenthesis( mw.config.get( 'wgTitle' ) ) );
				titles.push( toLowerCaseFirst( stripParenthesis( mw.config.get( 'wgTitle' ) ) ) );
				if ( entity.labels && entity.labels[ language ] ) {
					var label = entity.labels[ language ];
					titles.push( stripParenthesis( label.value ) );
					titles.push( toLowerCaseFirst( stripParenthesis( label.value ) ) );
				}
				if ( entity.aliases && entity.aliases[ language ] ) {
					var aliases = entity.aliases[ language ];
					for ( i=0,l=aliases.length; i<l; i++ ) {
						var alias = aliases[ i ];
						titles.push( stripParenthesis( alias.value ) );
						titles.push( toLowerCaseFirst( stripParenthesis( alias.value ) ) );
					}
				}
				var seen = {};
				domain.titles = [];
				for ( i=0,l=titles.length; i<l; i++ ) {
					var titleString = titles[ i ];
					if ( !seen[ titleString ] ) {
						domain.titles.push( titleString );
					}
					seen[ titleString ] = true;
				}
			}
			var state = sessionStorage.getItem( marker + '-' + 'state' );
			if ( ( state || 'enabled' ) === 'enabled' ) {
				queryDomains();
			}
		} );
	} );