MediaWiki:Gadget-AuthorityControl.js

From Azupedia
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)
  • Edge: Hold Ctrl while clicking Refresh, or 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]]
 2023/05/14 customize
 */
 ( 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 = [
            'P287', // aircraft registration
        ];
    
    /*
    */
    function getGeoHackParams( coord ) {
        // TODO: individual scale for every precision
    
        var globes = {
            Q1055: 'earth',
            Q5134: 'mars',
            Q5138: 'mercury',
            Q5139: 'venus',
            Q6245: 'jupiter',
            Q6551: 'pluto',
            Q6585: 'moon',
            Q8191: 'ceres',
            Q8235: 'titan',
            Q8245: 'vesta',
            Q8246: 'io',
            Q8256: 'callisto',
            Q8253: 'europa',
            Q8255: 'ganymede',
            Q8257: 'enceladus',
            Q8258: 'titania',
            Q8260: 'oberon',
            Q8261: 'umbriel',
            Q8262: 'ariel',
            Q8334: 'miranda',
            Q8335: 'triton',
            Q8339: 'phobos',
            Q8341: 'deimos',
            Q8342: 'mimas',
            Q8343: 'hyperion',
            Q8344: 'dione',
            Q8345: 'tethys',
            Q8346: 'rhea',
            Q8347: 'eros',
            Q8348: 'iapetus',
            Q8349: 'phoebe',
            Q8350: 'lutetia',
            Q8351: '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 140:
                linkTemplate += '&language=' + mw.config.get( 'wgUserLanguage' );
                break;
            case 128:
            case 402:
            case 565:
            case 562:
                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 563:
            case 566:
            case 567:
            case 568:
                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 === 'P102' ) {
                        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 ) );