MediaWiki:Gadget-AuthorityControl.js

From Wikidata
Jump to navigation Jump to search

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.
/* AuthorityControl.js
 * Provides a link to various Authority Control tools for some Wikidata statements that
 * are not external identifiers.
 *
 * Original gadget coded by [[User:Ricordisamoa]]
 */
( function ( mw, wb, $ ) {
'use strict';

if ( [ 0, 120, 146 ].indexOf( mw.config.get( 'wgNamespaceNumber' ) ) === -1 || !mw.config.exists( 'wbEntityId' ) ) {
	// Only entity pages feature appropriate statements.
	return;
}

var PROPERTIES = {},
	specialHandlingProperties = [
		'P426', // aircraft registration
	];

/*
*/
function getGeoHackParams( coord ) {
	// TODO: individual scale for every precision

	var globes = {
		Q2: 'earth',
		Q111: 'mars',
		Q308: 'mercury',
		Q313: 'venus',
		Q319: 'jupiter',
		Q339: 'pluto',
		Q405: 'moon',
		Q596: 'ceres',
		Q2565: 'titan',
		Q3030: 'vesta',
		Q3123: 'io',
		Q3134: 'callisto',
		Q3143: 'europa',
		Q3169: 'ganymede',
		Q3303: 'enceladus',
		Q3322: 'titania',
		Q3332: 'oberon',
		Q3338: 'umbriel',
		Q3343: 'ariel',
		Q3352: 'miranda',
		Q3359: 'triton',
		Q7547: 'phobos',
		Q7548: 'deimos',
		Q15034: 'mimas',
		Q15037: 'hyperion',
		Q15040: 'dione',
		Q15047: 'tethys',
		Q15050: 'rhea',
		Q16711: 'eros',
		Q17958: 'iapetus',
		Q17975: 'phoebe',
		Q107556: 'lutetia',
		Q158244: 'gaspra'
	};

	var globeQKey = coord.globe.replace( 'http://www.wikidata.org/entity/', '' );
	var globe = globes[ globeQKey ];

	return coord.latitude + '_N_' + coord.longitude + '_E_globe:' + globe;
}

/**
 * Get the snak value formatted with a link.
 *
 * @param {number} numericPropertyId Refers to PROPERTIES.
 * @param {string} value
 */
function getLinkValueForString( numericPropertyId, value ) {
	var linkValue;

	switch ( Number( numericPropertyId ) ) {
		default:
			linkValue = value;
	}

	return linkValue;
}

function makeLink( numericPropertyId, linkValue, displayText ) {
	var linkTemplate = PROPERTIES[ numericPropertyId ];

	switch ( Number( numericPropertyId ) ) {
		case 426:
			if ( linkValue.substring( 0, 1 ) === 'N' ) {
				linkTemplate = 'https://registry.faa.gov/AircraftInquiry/Search/NNumberResult?nNumberTxt=$1';
			} else if ( linkValue.substring( 0, 2 ) === 'G-' ) {
				// FIXME: this is said to be non-functional https://www.wikidata.org/w/index.php?oldid=1808076598#Update_to_FAA_URL
				linkTemplate = 'https://www.caa.co.uk/application.aspx?catid=60&pagetype=65&appid=1&mode=detailnosummary&fullregmark=$1';
				linkValue = linkValue.substring( 2 );
			} else {
				return linkValue;
			}
			break;
		case 625:
			linkTemplate += '&language=' + mw.config.get( 'wgUserLanguage' );
			break;
		case 1793:
		case 8460:
		case 8966:
		case 10999:
			linkTemplate = PROPERTIES[ numericPropertyId ];
			if ( linkTemplate === 'https://regex101.com/?regex=$1' ) {
				// Escape the character used as the delimiter, which for
				// regex101.com is slashes.
				// URL encode the value to avoid problems when a regex contains
				// characters with a special meaning in URLs, like & and #.

				try {
					// try to encode / as \/, if not encoded yet
					linkValue = (new RegExp(linkValue)).source;
				} catch (error) {
					// display anyway
				}
				linkValue = encodeURIComponent( linkValue );
			}
			break;
		case 233:
		case 2017:
		case 8533:
		case 10718:
			linkValue = encodeURIComponent( linkValue );
			break;
		default:
			linkTemplate = PROPERTIES[ numericPropertyId ];
	}

	var link = linkTemplate.replace( /\$1/g, linkValue );

	try {
		var prot = (new URL(link)).protocol;
		// Disallow javascript links to prevent xss.
		// Use URL parser to handle cases with spaces and other bypasses
		if (prot === 'javascript:' || prot === 'data:') {
		    return $( '<span>' ).text( displayText )
		}
	} catch (error) {
		return $( '<span>' ).text( displayText );
	}
	
	return $( '<a>' )
		.text( displayText )
		.attr( 'href', link )
		// Show the 'external link' icon:
		.addClass( 'external' );
}

function createLinkForString( numericPropertyId, value ) {
	var linkValue = getLinkValueForString( numericPropertyId, value );
	return makeLink( numericPropertyId, linkValue, value );
}

function createLinkForSnakValue( numericPropertyId, dataValue, displayText ) {
	var dataValueType = dataValue.getType(),
		value = dataValue.toJSON();

	// @fixme shouldn't happen but in case of any unexpected data value types,
	// then there should be better error handling here.
	var linkValue = '';

	if ( dataValueType === 'string' ) {
		linkValue = getLinkValueForString( numericPropertyId, value );
	} else if ( dataValueType === 'globecoordinate' ) {
		linkValue = getGeoHackParams( value );
	}

	return makeLink( numericPropertyId, linkValue, displayText );
}

function linkSnakView( el, propertySelector, valueSelector ) {
	var $propLink = $( el ).find( propertySelector );

	var title = $propLink.attr( 'title' );

	if ( title ) {
		var titleParts = title.split( ':P' ),
			numericPropertyId = titleParts[ 1 ];

		if ( PROPERTIES.hasOwnProperty( numericPropertyId ) ) {
			var $value = $( el ).find( valueSelector ).first(),
				$link = createLinkForString( numericPropertyId, $value.text() );

			$value.html( $link );
		}
	}
}

function handleSnak( snak, snakView ) {
	if ( !( snak.getType && snak.getType() == 'value' ) ) {
		return;
	}

	var numericPropertyId = snak.getPropertyId().slice( 1 );
	if ( !( PROPERTIES.hasOwnProperty( numericPropertyId ) ) ) {
		return;
	}
	var $snakValue = $( snakView ).find( '.wikibase-snakview-value' );
	if ( $snakValue.find( '.wikibase-kartographer-caption' ).length ) {
		// If this is a Kartographer map, don't mangle the whole snak value,
		// but just the caption.
		$snakValue = $snakValue.find( '.wikibase-kartographer-caption' );
	}

	var displayText = extractDisplayText( $snakValue ),
		snakLink = createLinkForSnakValue( numericPropertyId, snak.getValue(), displayText );

	$snakValue.html( snakLink );
}

function extractDisplayText( $snakValue ) {
	var $snakValueClone = $snakValue.clone();

	$snakValueClone.children().remove();

	return $snakValueClone.text();
}

/**
 * Initializes the gadget.
 * This procedure needs to be performed as good as possible. jQuery selector usage should be limited
 * to a minimum.
 */
function initGadget() {
	if ( $.isEmptyObject( PROPERTIES ) ) {
		return;
	}

	$( ':wikibase-statementview' ).each( function () {
		var statementview = $.data( this, 'statementview' ),
			statement = statementview.value(),
			claim = statement.getClaim(),
			qualifierGroups = claim.getQualifiers().getGroupedSnakLists();

		handleSnak( claim.getMainSnak(), statementview.$mainSnak[ 0 ] );

		$( '.wikibase-statementview-qualifiers .wikibase-snaklistview', this ).each( function( i ) {
			var qualifiers = qualifierGroups[i].toArray();
			$( '.wikibase-snakview', this ).each( function( n ) {
				handleSnak( qualifiers[n], this );
			} );
		} );

	} );

	$( '.wikibase-referenceview .wikibase-snaklistview-listview' ).each( function () {
		linkSnakView( this, '.wikibase-snakview-property > a', '.wikibase-snakview-value' );
	} );
}

function getProperties( entity ) {
	var api = new mw.Api(),
		repoApi = new wb.api.RepoApi( api ),
		propertyIds = [],
		alreadyLinkedPropertyIds = [];

	function addSnak( snak ) {
		var snakPropertyId = snak.property,
			$firstSnakValue;

		if ( snak.snaktype !== 'value' ||
			( snak.datavalue.type !== 'string' && snak.datavalue.type !== 'globecoordinate' ) ) {
			return;
		}
		if ( propertyIds.indexOf( snakPropertyId ) !== -1 ) {
			return;
		}
		if ( specialHandlingProperties.indexOf( snakPropertyId ) === -1 ) {
			if ( alreadyLinkedPropertyIds.indexOf( snakPropertyId ) !== -1 ) {
				return;
			}
			$firstSnakValue = $( '#' + snakPropertyId ).find( '.wikibase-snakview-variation-valuesnak:first' );
			if ( $firstSnakValue.find( '> a:not(.oo-ui-widget)' ).length > 0 || $firstSnakValue.find( '> div.thumb' ).length > 0 ) {
				alreadyLinkedPropertyIds.push( snakPropertyId );
				return;
			}
		}

		propertyIds.push( snakPropertyId );
	}

	function analyzeClaims( claims ) {
		var prop;
		for ( prop in claims ) {
			$.each( claims[ prop ], function ( i, claim ) {
				addSnak( claim.mainsnak );
				$.each( claim.references || [], function ( i, ref ) {
					for ( prop in ref.snaks ) {
						$.each( ref.snaks[ prop ], function ( i, cl ) {
							addSnak( cl );
						} );
					}
				} );
				for ( prop in claim.qualifiers || {} ) {
					$.each( claim.qualifiers[ prop ], function ( i, cl ) {
						addSnak( cl );
					} );
				}
			} );
		}
	}

	analyzeClaims( entity.claims );
	$.each( entity.forms || [], function ( _, form ) {
		analyzeClaims( form.claims || {} );
	} );
	$.each( entity.senses || [], function ( _, sense ) {
		analyzeClaims( sense.claims || {} );
	} );

	if ( !propertyIds.length ) {
		return $.Deferred().resolve();
	}
	return repoApi.getEntities( propertyIds, 'claims' )
	.then( function ( data ) {
		if ( !data || !data.entities ) {
			// Unexpected response from the API
			return;
		}
		$.each( data.entities, function ( entityId, entity ) {
			if ( entity.datatype === 'external-id' && $.inArray( entity.id, specialHandlingProperties ) === -1 ) {
				// No need to format these
				return true;
			}
			if ( entity.datatype === 'commonsMedia' ) {
				// No need to format these, but we can't exclude them earlier
				// as we don't have the property datatype.
				// XXX: This probably also applies to math etc.
				return true;
			}
			$.each( entity.claims, function ( claimId, claims ) {
				if ( claimId === 'P1630' ) {
					var i, len = claims.length;
					for ( i = 0; i < len; i++ ) {
						if ( claims[ i ].rank !== 'preferred' ) {
							continue;
						}
						if ( claims[ i ].mainsnak.snaktype === 'value' ) {
							PROPERTIES[ entityId.slice( 1 ) ] = claims[ i ].mainsnak.datavalue.value;
						}
						return false;
					}
					for ( i = 0; i < len; i++ ) {
						if ( claims[ i ].rank !== 'normal' ) {
							continue;
						}
						if ( claims[ i ].mainsnak.snaktype === 'value' ) {
							PROPERTIES[ entityId.slice( 1 ) ] = claims[ i ].mainsnak.datavalue.value;
							break;
						}
					}
					return false;
				}
			} );
		} );
	} );
}

var rendered = $.Deferred(),
	loaded = $.Deferred();

$.when(
	rendered,
	loaded,
	$.ready
).then( function () {
	initGadget();
} );

mw.hook( 'wikibase.entityPage.entityLoaded' ).add( function ( entity ) {
	getProperties( entity ).then( function () {
		loaded.resolve();
	} );
} );

mw.hook( 'wikibase.entityPage.entityView.rendered' ).add( function () {
	rendered.resolve();
} );

}( mediaWiki, wikibase, jQuery ) );