User:A2569875/Module:Graph/ThirdPartyAPIAddon.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// User:A2569875/Module:Graph/ThirdPartyAPIAddon.js
/**
* 維基百科舊版圖表第三方外部 API 擴充套件 (NASA / NOAA 等非維基站點支援)
*
* 運作機制:
* 1. 專門讀取官方小工具忽略之 .gr-async-chart-unsupported[data-chart-spec] 容器。
* 2. 首次執行時透過 MediaWiki OOUI 彈出風險告知視窗,確認後記錄於 localStorage。
* 3. 若使用者選擇「不同意並移除」,腳本會自動搜尋使用者個人 common.js / skin.js 移除自身引用;
* 若無法自動移除,則以 OOUI 彈窗提示使用者手動移除。
*/
( function () {
'use strict';
window.mw = window.mw || {};
mw.libs = mw.libs || {};
var GW = mw.libs.GraphWidget = mw.libs.GraphWidget || {};
var CONSENT_STORAGE_KEY = 'mw_graph_3rdparty_addon_consent_v1';
var ADDON_KEYWORD = 'ThirdPartyAPIAddon.js';
var CORE_FALLBACK_PAGES = [
'MediaWiki:Gadget-Graph/Core.js',
'User:A2569875/Module:Graph/Core.js',
'User:a2569875/Module:Graph/Core.js'
];
var consentChecked = false;
var consentGranted = false;
var consentDialogPromise = null;
// =========================================================================
// 1. 讀取與寫入 localStorage 風險同意狀態
// =========================================================================
function hasStoredConsent() {
try {
return !!( window.localStorage && localStorage.getItem( CONSENT_STORAGE_KEY ) === 'accepted' );
} catch ( e ) {
return false;
}
}
function saveStoredConsent() {
try {
if ( window.localStorage ) {
localStorage.setItem( CONSENT_STORAGE_KEY, 'accepted' );
}
} catch ( e ) { /* empty */ }
}
// =========================================================================
// 2. 自動搜尋並從使用者個人 JS 頁面移除 ThirdPartyAPIAddon.js
// =========================================================================
function removeAddonLinesFromCode( code ) {
if ( typeof code !== 'string' || code.indexOf( ADDON_KEYWORD ) === -1 ) {
return null;
}
var lines = code.split( '\n' );
var out = [];
var removedCount = 0;
for ( var i = 0; i < lines.length; i++ ) {
if ( lines[ i ].indexOf( ADDON_KEYWORD ) !== -1 ) {
removedCount++;
} else {
out.push( lines[ i ] );
}
}
return removedCount > 0 ? out.join( '\n' ) : null;
}
function autoRemoveSelfFromUserScripts() {
return mw.loader.using( [ 'mediawiki.api', 'mediawiki.user', 'oojs-ui-core', 'oojs-ui-windows' ] ).then( function () {
var userName = mw.config.get( 'wgUserName' );
if ( !userName ) {
return OO.ui.alert(
'您目前尚未登入,或此腳本是由瀏覽器擴充功能載入。請自行從瀏覽器擴充功能(如 Tampermonkey / Greasemonkey)中移除 ThirdPartyAPIAddon.js。',
{ title: '請手動移除擴充腳本', size: 'medium' }
);
}
var skin = mw.config.get( 'skin' ) || 'vector-2022';
var candidatePages = [
'User:' + userName + '/common.js',
'User:' + userName + '/' + skin + '.js',
'User:' + userName + '/vector.js',
'User:' + userName + '/vector-2022.js',
'User:' + userName + '/monobook.js',
'User:' + userName + '/minerva.js'
];
// 去重
var uniquePages = [];
for ( var i = 0; i < candidatePages.length; i++ ) {
if ( uniquePages.indexOf( candidatePages[ i ] ) === -1 ) {
uniquePages.push( candidatePages[ i ] );
}
}
var api = new mw.Api();
return api.get( {
action: 'query',
prop: 'revisions',
titles: uniquePages.join( '|' ),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
} ).then( function ( data ) {
var pages = ( data && data.query && data.query.pages ) || [];
var editPromises = [];
var modifiedTitles = [];
for ( var p = 0; p < pages.length; p++ ) {
var pageObj = pages[ p ];
if ( !pageObj || pageObj.missing ) {
continue;
}
var rev = pageObj.revisions && pageObj.revisions[ 0 ];
var content = ( rev && rev.slots && rev.slots.main && ( rev.slots.main.content || rev.slots.main[ '*' ] ) ) ||
( rev && ( rev.content || rev[ '*' ] ) ) || '';
var cleaned = removeAddonLinesFromCode( content );
if ( cleaned !== null ) {
modifiedTitles.push( pageObj.title );
editPromises.push(
api.postWithEditToken( {
action: 'edit',
title: pageObj.title,
text: cleaned,
summary: '自動移除 ' + ADDON_KEYWORD + '(使用者於風險告知彈窗選擇停用並移除)'
} )
);
}
}
if ( editPromises.length === 0 ) {
return OO.ui.alert(
'系統在您的個人腳本頁(如 Special:MyPage/common.js)中未找到對 ' + ADDON_KEYWORD + ' 的直接引用。若您是透過全域腳本(meta:Special:MyPage/global.js)、其他子頁面或瀏覽器擴充功能載入,請手動將其移除。',
{ title: '無法自動定位腳本,請手動移除', size: 'medium' }
);
}
return window.jQuery.when.apply( window.jQuery, editPromises ).then( function () {
return OO.ui.alert(
'已成功從您的個人腳本頁(' + modifiedTitles.join( '、' ) + ')自動移除 ' + ADDON_KEYWORD + '。頁面即將重新整理以生效。',
{ title: '已自動移除擴充腳本', size: 'medium' }
).then( function () {
window.location.reload();
} );
}, function () {
return OO.ui.alert(
'自動編輯個人腳本頁時發生錯誤,請手動前往 Special:MyPage/common.js 移除包含 ' + ADDON_KEYWORD + ' 的程式碼行。',
{ title: '自動移除失敗,請手動移除', size: 'medium' }
);
} );
} );
} );
}
// =========================================================================
// 3. OOUI 首次安裝風險告知彈窗
// =========================================================================
function ensureUserConsent() {
var $ = window.jQuery;
if ( consentGranted || hasStoredConsent() ) {
consentGranted = true;
consentChecked = true;
return $.Deferred().resolve( true ).promise();
}
if ( consentDialogPromise ) {
return consentDialogPromise;
}
var dfd = $.Deferred();
consentDialogPromise = dfd.promise();
mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-windows' ] ).then( function () {
var messageContainer = document.createElement( 'div' );
messageContainer.style.lineHeight = '1.6';
var p1 = document.createElement( 'p' );
p1.textContent = '您已載入「舊版圖表第三方外部 API 擴充套件(ThirdPartyAPIAddon.js)」。此腳本用於還原條目中依賴 NASA、NOAA 等非維基媒體官方網站之動態資料圖表。';
var p2 = document.createElement( 'p' );
p2.textContent = '⚠️ 資安與隱私風險告知:啟用此功能後,您的瀏覽器將直接向第三方外部伺服器請求資料(外部網站將可得知您的 IP 位址),且外部回傳之資料不受維基媒體基金會(WMF)與中文維基百科社群審核管控。';
var p3 = document.createElement( 'p' );
p3.style.fontWeight = 'bold';
p3.textContent = '若您點擊「確定啟用」,即代表您已知悉並同意自行承擔潛在之跨網站腳本(XSS)與外部連線風險(維基媒體基金會不為第三方資料背書),系統將記住您的選擇不再重複詢問;若您不同意,請點擊「取消並自動移除」,系統將自動協助從您的個人腳本頁移除此擴充套件。';
messageContainer.appendChild( p1 );
messageContainer.appendChild( p2 );
messageContainer.appendChild( p3 );
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );
var messageDialog = new OO.ui.MessageDialog();
windowManager.addWindows( [ messageDialog ] );
var instance = windowManager.openWindow( messageDialog, {
title: '第三方外部 API 圖表擴充套件 — 風險告知與免責聲明',
message: $( messageContainer ),
size: 'large',
actions: [
{
action: 'accept',
label: '確定啟用(我願意自行承擔風險)',
flags: [ 'primary', 'progressive' ]
},
{
action: 'reject',
label: '取消並自動移除此腳本',
flags: [ 'safe', 'destructive' ]
}
]
} );
instance.closed.then( function ( closeData ) {
windowManager.destroy();
if ( closeData && closeData.action === 'accept' ) {
saveStoredConsent();
consentGranted = true;
consentChecked = true;
dfd.resolve( true );
} else {
consentGranted = false;
consentChecked = true;
autoRemoveSelfFromUserScripts();
dfd.resolve( false );
}
} );
}, function () {
dfd.resolve( false );
} );
return consentDialogPromise;
}
// =========================================================================
// 4. 確保 Core.js 與對應圖表渲染器 (Chart2D / ChartPolar / Chart3D) 已就緒
// =========================================================================
function ensureCoreLoaded() {
var $ = window.jQuery;
if ( GW.ensureRendererForType && GW.extractRecords && GW.applySafeTransforms ) {
return $.Deferred().resolve().promise();
}
var scriptPath = ( window.mw && mw.config && mw.config.get( 'wgScript' ) ) || '/w/index.php';
var dfd = $.Deferred();
var tryIdx = function ( idx ) {
if ( GW.ensureRendererForType ) {
dfd.resolve();
return;
}
if ( idx >= CORE_FALLBACK_PAGES.length ) {
dfd.reject( new Error( '無法載入 Graph Core.js' ) );
return;
}
var rawUrl = scriptPath + '?title=' + encodeURIComponent( CORE_FALLBACK_PAGES[ idx ] ) + '&action=raw&ctype=text/javascript';
mw.loader.getScript( rawUrl ).then( function () {
if ( GW.ensureRendererForType ) {
dfd.resolve();
} else {
tryIdx( idx + 1 );
}
}, function () {
tryIdx( idx + 1 );
} );
};
tryIdx( 0 );
return dfd.promise();
}
// 解析第三方 API 常見的 CSV 資料 (例如 NASA GISS 氣溫、NOAA 觀測數據)
function parseExternalCSV( text ) {
if ( typeof text !== 'string' || !text.trim() ) {
return [];
}
var lines = text.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' ).split( '\n' );
var headers = null;
var out = [];
var splitLine = function ( line ) {
var res = [];
var cur = '';
var inQuotes = false;
for ( var i = 0; i < line.length; i++ ) {
var ch = line.charAt( i );
if ( inQuotes ) {
if ( ch === '"' ) {
if ( line.charAt( i + 1 ) === '"' ) {
cur += '"';
i++;
} else {
inQuotes = false;
}
} else {
cur += ch;
}
} else if ( ch === '"' ) {
inQuotes = true;
} else if ( ch === ',' ) {
res.push( cur.trim() );
cur = '';
} else {
cur += ch;
}
}
res.push( cur.trim() );
return res;
};
for ( var l = 0; l < lines.length; l++ ) {
var line = lines[ l ].trim();
if ( !line || line.charAt( 0 ) === '#' ) {
continue;
}
if ( !headers ) {
headers = splitLine( line );
} else {
var vals = splitLine( line );
var row = {};
for ( var c = 0; c < headers.length; c++ ) {
var h = headers[ c ];
if ( h ) {
var v = vals[ c ] !== undefined ? vals[ c ] : '';
var num = Number( v );
row[ h ] = ( v !== '' && !isNaN( num ) ) ? num : v;
}
}
out.push( row );
}
}
return out;
}
function validateThirdPartyUrl( rawUrl ) {
if ( typeof rawUrl !== 'string' || !rawUrl.trim() ) {
return null;
}
var u = rawUrl.trim();
if ( u.indexOf( '//' ) === 0 ) {
u = 'https:' + u;
} else if ( /^http:\/\//i.test( u ) ) {
u = 'https://' + u.slice( 7 );
}
var parsed;
try {
parsed = new URL( u );
} catch ( e ) {
return null;
}
if ( parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.pathname.indexOf( '..' ) !== -1 ) {
return null;
}
return parsed.href;
}
function fetchThirdPartyData( url, formatSpec ) {
var $ = window.jQuery;
var isCsv = ( formatSpec && String( formatSpec.type || '' ).toLowerCase() === 'csv' ) || /\.csv(?:\?|$)/i.test( url );
var dfd = $.Deferred();
$.ajax( {
url: url,
dataType: isCsv ? 'text' : 'json',
cache: true
} ).done( function ( data ) {
if ( typeof data === 'string' ) {
try {
dfd.resolve( JSON.parse( data ) );
} catch ( e ) {
dfd.resolve( parseExternalCSV( data ) );
}
} else {
dfd.resolve( data );
}
} ).fail( function ( jqXHR, textStatus, err ) {
dfd.reject( new Error( '第三方 API 載入失敗: ' + ( err || textStatus ) ) );
} );
return dfd.promise();
}
// =========================================================================
// 5. 掃描並渲染 .gr-async-chart-unsupported 容器
// =========================================================================
function processUnsupportedContainer( container ) {
if ( !container || container.getAttribute( 'data-gr-3p-loaded' ) === '1' ) {
return;
}
container.setAttribute( 'data-gr-3p-loaded', '1' );
var rawSpec = container.getAttribute( 'data-chart-spec' );
if ( !rawSpec ) {
return;
}
var spec;
try {
spec = JSON.parse( rawSpec );
} catch ( e ) {
return;
}
var targetUrl = validateThirdPartyUrl( spec.apiUrl || spec.rawUrl );
if ( !targetUrl ) {
return;
}
ensureCoreLoaded().then( function () {
if ( GW.applyConvertedI18n ) {
GW.applyConvertedI18n( container, spec );
}
return window.jQuery.when(
GW.ensureRendererForType( spec.type ),
fetchThirdPartyData( targetUrl, spec.format )
);
} ).then( function ( renderer, rawData ) {
if ( typeof renderer !== 'function' ) {
return;
}
var rawList = Array.isArray( rawData ) ? rawData : GW.extractRecords( rawData, spec.format );
var rows = GW.applySafeTransforms ? GW.applySafeTransforms( rawList, spec.transforms ) : rawList;
if ( GW.applyRowI18n ) {
rows = GW.applyRowI18n( rows, spec._i18nDict );
}
renderer( container, spec, rows, {
notice: spec.title
} );
} ).then( null, function ( err ) {
if ( window.console && console.warn ) {
console.warn( '[ThirdPartyAPIAddon.js] 略過第三方圖表渲染:', err );
}
} );
}
function scanUnsupportedRoot( root ) {
if ( !root ) {
return;
}
var containers = [];
if ( root.nodeType === 1 && root.classList && root.classList.contains( 'gr-async-chart-unsupported' ) && root.hasAttribute( 'data-chart-spec' ) ) {
containers.push( root );
}
if ( root.querySelectorAll ) {
var list = root.querySelectorAll( '.gr-async-chart-unsupported[data-chart-spec]' );
for ( var i = 0; i < list.length; i++ ) {
containers.push( list[ i ] );
}
}
if ( containers.length === 0 ) {
return;
}
// 若尚未獲得使用者同意(或首次安裝),先觸發 OOUI 風險告知彈窗
ensureUserConsent().then( function ( allowed ) {
if ( !allowed ) {
return;
}
for ( var j = 0; j < containers.length; j++ ) {
processUnsupportedContainer( containers[ j ] );
}
} );
}
function bootAddon() {
// 首次安裝時,即使當前頁面沒有第三方圖表,也立即彈出一次 OOUI 風險確認視窗以完成授權記錄
if ( !hasStoredConsent() && !consentChecked ) {
ensureUserConsent().then( function ( allowed ) {
if ( allowed ) {
scanUnsupportedRoot( document );
}
} );
} else {
scanUnsupportedRoot( document );
}
if ( window.mw && typeof mw.hook === 'function' ) {
mw.hook( 'wikipage.content' ).add( function ( $content ) {
if ( consentGranted || hasStoredConsent() ) {
if ( $content && $content.length ) {
for ( var i = 0; i < $content.length; i++ ) {
scanUnsupportedRoot( $content[ i ] );
}
}
scanUnsupportedRoot( document );
}
} );
}
}
if ( window.mw && mw.loader && typeof mw.loader.using === 'function' ) {
mw.loader.using( [ 'jquery', 'mediawiki.base', 'mediawiki.util' ] ).then( bootAddon );
} else {
( window.RLQ = window.RLQ || [] ).push( function () {
if ( window.mw && mw.loader && typeof mw.loader.using === 'function' ) {
mw.loader.using( [ 'jquery', 'mediawiki.base', 'mediawiki.util' ] ).then( bootAddon );
}
} );
}
}() );