/* eslint-disable no-multi-spaces */
/* eslint-disable dot-notation */
// ==UserScript==
// @name 网易云音乐-MyFreeMP3扩展
// @name:zh-CN 网易云音乐-MyFreeMP3扩展
// @name:en Netease Music - MyFreeMP3 Extender
// @namespace 163Music-MyFreeMP3-Extender
// @version 1.0
// @description 利用MyFreeMP3扩展网易云音乐功能
// @description:zh-CN 利用MyFreeMP3扩展网易云音乐功能
// @description:en Extend netease music with MyFreeMP3
// @author PY-DNG
// @license GPL-v3
// @match http*://music.163.com/*
// @connect 59.110.45.28
// @connect music.163.net
// @connect music.126.net
// @icon https://s1.music.126.net/style/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-start
// @noframes
// ==/UserScript==
(function __MAIN__() {
'use strict';
const CONST = {
Text: {
V5NOCANQU: '要听龙叔的话,V5不能屈,听完歌快去给你网易爸爸充VIP吧'
},
Number: {
Interval_Fastest: 1,
Interval_Fast: 50,
Interval_Balanced: 500,
MaxSearchPage: 3,
}
}
// Prepare
const md5Script = document.createElement('script');
md5Script.src = 'https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.js';
document.head.appendChild(md5Script);
// Arguments: level=LogLevel.Info, logContent, asObject=false
// Needs one call "DoLog();" to get it initialized before using it!
function DoLog() {
const win = typeof unsafeWindow === 'object' ? unsafeWindow : window;
// Global log levels set
win.LogLevel = {
None: 0,
Error: 1,
Success: 2,
Warning: 3,
Info: 4,
}
win.LogLevelMap = {};
win.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
win.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
win.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
win.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
win.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
// Current log level
DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
// Log counter
DoLog.logCount === undefined && (DoLog.logCount = 0);
if (++DoLog.logCount > 512) {
console.clear();
DoLog.logCount = 0;
}
// Get args
let level, logContent, asObject;
switch (arguments.length) {
case 1:
level = LogLevel.Info;
logContent = arguments[0];
asObject = false;
break;
case 2:
level = arguments[0];
logContent = arguments[1];
asObject = false;
break;
case 3:
level = arguments[0];
logContent = arguments[1];
asObject = arguments[2];
break;
default:
level = LogLevel.Info;
logContent = 'DoLog initialized.';
asObject = false;
break;
}
// Log when log level permits
if (level <= DoLog.logLevel) {
let msg = '%c' + LogLevelMap[level].prefix;
let subst = LogLevelMap[level].color;
if (asObject) {
msg += ' %o';
} else {
switch(typeof(logContent)) {
case 'string': msg += ' %s'; break;
case 'number': msg += ' %d'; break;
case 'object': msg += ' %o'; break;
}
}
console.log(msg, subst, logContent);
}
}
DoLog();
main();
function main() {
// Wait for document.body
if (!document.body) {
setTimeout(main, CONST.Number.Interval_Fast);
return false;
}
// Commons
hookPlay();
// Page functions
const ITM = new IntervalTaskManager();
const pageChangeDetecter = (function(callback, emitOnInit=false) {
let href = location.href;
emitOnInit && callback(null, href);
return function detecter() {
const new_href = location.href;
if (href !== new_href) {
callback(href, new_href);
href = new_href;
}
}
}) (deliverPageFuncs, true);
ITM.time = CONST.Number.Interval_Fast;
ITM.addTask(pageChangeDetecter);
ITM.start();
function deliverPageFuncs(href, new_href) {
const pageFuncs = [{
reg: /^https?:\/\/music\.163\.com\/#\/song\?.+$/,
func: pageSong,
checker: function() {
const ifr = $('#g_iframe'); if (!ifr) {return false;}
const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
const elm = !!$(oDoc, '.cnt>.m-info');
return elm;
}
},{
reg: /^https?:\/\/music\.163\.com\/#\/(artist|album|discover\/toplist)\?.+$/,
func: replacePredata,
sync: false
},{
reg: /^https?:\/\/music\.163\.com\//,
func: listDownload,
checker: function() {
const ifr = $('#g_iframe'); if (!ifr) {return false;}
const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
return !!oDoc.body;
}
},{
reg: /^https?:\/\/music\.163\.com\//,
func: playlistDownload
},{
reg: /^https?:\/\/music\.163\.com\/#\/album\?.+$/,
func: pageAlbum,
checker: function() {
const ifr = $('#g_iframe'); if (!ifr) {return false;}
const oDoc = ifr.contentDocument; if (!oDoc) {return false;}
const elm = !!$(oDoc, '#content-operation');
return elm;
}
}];
for (const pageFunc of pageFuncs) {
test_exec(pageFunc);
}
function test_exec(pageFunc) {
pageFunc.reg.test(location.href) && ((((pageFunc.sync || !pageFunc.hasOwnProperty('sync')) ? iframeDocSync() : true) && (pageFunc.checker ? ({
'string': () => ($(pageFunc.checker)),
'function': pageFunc.checker,
})[typeof pageFunc.checker]() : true)) ? true : (setTimeout(test_exec.bind(null, pageFunc), CONST.Number.Interval_Balanced), DoLog('waiting: ' + location.href), false)) && pageFunc.func(href, new_href);
}
}
}
function hookPlay() {
// Play
try {
const RATES = {
'none': 0,
'standard': 128000,
'lossless': 999000,
};
const APIH = new APIHooker();
let dlLevel, dlRate, plLevel, plRate;
APIH.hook(/^https?:\/\/music\.163\.com\/weapi\/v3\/song\/detail(\?[a-zA-Z0-9=_]+)?$/, function(xhr) {
const json = JSON.parse(xhr.response);
const privilege = json['privileges'][0];
dlLevel = privilege['downloadMaxBrLevel'];
dlRate = RATES[dlLevel];
plLevel = privilege['playMaxBrLevel'];
plRate = RATES[plLevel];
privilege['dlLevel'] = dlLevel;
privilege['dl'] = dlRate;
privilege['plLevel'] = plLevel;
privilege['pl'] = plRate;
const response = JSON.stringify(json)
const propDesc = {
value: response,
writable: false,
configurable: false,
enumerable: true
}
Object.defineProperties(xhr, {
'response': propDesc,
'responseText': propDesc
})
return true;
});
APIH.hook(/^\/weapi\/song\/enhance\/player\/url\/v1(\?[a-zA-Z0-9=_]+)?$/, function(xhr, _this, args, onreadystatechange) {
const ifr = $('#g_iframe');
const oDoc = ifr.contentDocument;
// Get data
const json = JSON.parse(xhr.response);
const data = json['data'][0];
const name = $('.play a.name').innerText;
const artist = $('.play .by>span').children[0].innerText;
const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
const cover = $('.m-playbar .head img').src;
const cpath = getUrlPath(cover);
// Only hook unplayable songs
if (data['url']) {return true};
search({
text: fname,
callback: function(s_json) {
const list = s_json.data.list;
const song = list.find(function(song) {
// Search result
const qualities = [2000, 320, 128];
const q = qualities.find((q) => (song.quality.includes(q)));
const s_url = song[({
2000: 'url_flac',
320: 'url_320',
128: 'url_128'
})[q]];
const s_ftype = ({
2000: 'flac',
320: 'mp3',
128: 'mp3'
})[q];
const s_lrc = song.lrc;
const s_cover = song.cover;
const s_name = song.name;
const s_artist = song.artist;
const s_fname = name + ' - ' + artist;
const s_cpath = getUrlPath(s_cover);
if (s_cpath === cpath) {
// Song found, request final url
song.url = s_url;
return true;
}
}) || list[0];
const abort = GM_xmlhttpRequest({
method: 'GET',
url: song.url,
onprogress: function(e) {
abort();
// modify xhr and continue stack
data['code'] = 200;
data['br'] = plRate;
data['level'] = plLevel;
data['type'] = 'mp3';
data['url'] = e.finalUrl;
const response = JSON.stringify(json);
const propDesc = {
value: response,
writable: false,
configurable: true,
enumerable: true
};
Object.defineProperties(xhr, {
'response': propDesc,
'responseText': propDesc
});
continueStack();
}
}).abort
},
});
// Suspend stack until search & find the song
return false;
function continueStack() {
onreadystatechange.apply(_this, args);;
}
});
} catch (err) {
console.error(err);
DoLog(LogLevel.Error, 'hooking error');
}
}
function listDownload() {
const iframe = $('#g_iframe');
const oDoc = iframe.contentDocument;
const body = oDoc.body;
if (!body) {
DoLog(LogLevel.Warning, 'listDownload: list not found');
return false;
}
const AEL = getPureAEL();
AEL.call(body, 'click', function(e) {
const elm = e.target;
if (elm.getAttribute('data-res-action') === 'download') {
e.stopPropagation();
const elm_share = elm.previousElementSibling;
const name = elm_share.getAttribute('data-res-name');
const artist = elm_share.getAttribute('data-res-author');
const cover = elm_share.getAttribute('data-res-pic');
downloadSong(name, artist, cover);
}
}, {capture: true});
}
function playlistDownload() {
const AEL = getPureAEL();
AEL.call(document.body, 'click', function(e) {
const elm = e.target;
if (elm.getAttribute('data-action') === 'download') {
e.stopPropagation();
const li = elm.parentElement.parentElement.parentElement;
const name = $(li, '.col-2').innerText;
const artist = $(li, '.col-4').innerText;
downloadSong(name, artist);
}
}, {capture: true});
}
function pageSong() {
const ifr = $('#g_iframe');
const oDoc = ifr.contentDocument;
const name = $(oDoc, '.tit>em').innerText;
const artist = $(oDoc, '.cnt>.des>span>a').innerText;
const cover = $(oDoc, '.u-cover>img.j-img').src;
const AEL = getPureAEL();
// GUI
if ($(oDoc, '.vip-song')) {
const content_operation = $(oDoc, '#content-operation');
const vip_group = $(content_operation, '.u-vip-btn-group');
const vip_play = $(vip_group, 'a[data-res-action="play"]');
const vip_add = $(vip_group, 'a[data-res-action="addto"]');
// Style
vip_play.classList.remove('u-btni-vipply');
vip_play.classList.add('u-btni-addply');
vip_add.classList.remove('u-btni-vipadd');
vip_add.classList.add('u-btni-add');
content_operation.insertAdjacentElement('afterbegin', vip_add);
content_operation.insertAdjacentElement('afterbegin', vip_play);
content_operation.removeChild(vip_group);
// Text
vip_play.title = CONST.Text.V5NOCANQU;
vip_play.children[0].childNodes[1].nodeValue = '播放';
}
// Download
const dlButton = $(oDoc, '#content-operation>a[data-res-action="download"]');
AEL.call(dlButton, 'click', dlOnclick, {useCapture: true});
function dlOnclick(e) {
e.stopPropagation();
downloadSong(name, artist, cover);
}
}
function pageAlbum() {
const iframe = $('#g_iframe');
const oDoc = iframe.contentDocument;
const oWin = iframe.contentWindow;
// GUI
if ($(oDoc, '.vip-album')) {
const content_operation = $(oDoc, '#content-operation');
const vip_group = $(content_operation, '.u-vip-btn-group');
const vip_play = $(vip_group, 'a[data-res-action="play"]');
const vip_add = $(vip_group, 'a[data-res-action="addto"]');
// Style
vip_play.classList.remove('u-btni-vipply');
vip_play.classList.add('u-btni-addply');
vip_add.classList.remove('u-btni-vipadd');
vip_add.classList.add('u-btni-add');
content_operation.insertAdjacentElement('afterbegin', vip_add);
content_operation.insertAdjacentElement('afterbegin', vip_play);
content_operation.removeChild(vip_group);
// Text
vip_play.title = CONST.Text.V5NOCANQU;
vip_play.children[0].childNodes[1].nodeValue = '播放';
}
}
function replacePredata() {
const iframe = $('#g_iframe');
const oDoc = iframe.contentDocument;
const oWin = iframe.contentWindow;
const envReady = oDoc && iframeDocSync();
const elmData = oDoc && $(oDoc, '#song-list-pre-data');
if (!elmData) {
// No elmData found.
if (envReady && $(oDoc, '#song-list-pre-cache table')) {
// Too late. Data has already been dealed.
DoLog(LogLevel.Error, 'Predata hook failed.');
DoLog([$(oDoc, '#song-list-pre-cache table'), oDoc.URL, oWin.location.href]);
} else {
// Data has not been loaded!
DoLog('No predata found');
if (envReady) {
// Hook Element.prototype.getElementsByTagName to make changeValue called.
DoLog('Environment ready, hooking getElementsByTagName...');
const hooker = new Hooker();
const id = hooker.hook(oWin, 'Element.prototype.getElementsByTagName', false, false, {
dealer: function(_this, args) {
if (_this.id === 'song-list-pre-cache' && args[0] === 'textarea') {
const elmData = $(_this, 'textarea');
changeValue(elmData);
hooker.unhook(id);
DoLog('Value changed, getElementsByTagName unhooked...');
}
return [_this, args];
}
}).id;
DoLog(LogLevel.Success, 'getElementsByTagName Hooked...');
} else {
// Environment not ready yet, wait for it
DoLog('Environment not ready, waiting...');
setTimeout(replacePredata, CONST.Number.Interval_Fastest);
}
}
return false;
} else {
// elmData Found! Go change value directly.
DoLog('Changing value directly');
changeValue(elmData);
}
function changeValue(elmData) {
const RATES = {
'none': 0,
'standard': 128000,
'lossless': 999000,
};
const list = JSON.parse(elmData.value);
for (const song of list) {
const privilege = song.privilege;
const dlLevel = privilege.downloadMaxBrLevel;
const dlRate = RATES[dlLevel];
const plLevel = privilege.playMaxBrLevel;
const plRate = RATES[plLevel];
privilege.dlLevel = dlLevel;
privilege.dl = dlRate;
privilege.plLevel = plLevel;
privilege.pl = plRate;
}
elmData.value = JSON.stringify(list);
DoLog(LogLevel.Success, 'Predata replaced');
}
}
function downloadSong(name, artist, cover) {
// Check arguments
if (!name || !artist) {
DoLog(LogLevel.Error, 'downloadSong: name or artist missing');
return false;
}
!cover && DoLog('downloadSong: cover not provided');
// Gather info
const fname = replaceText('{$NAME} - {$ARTIST}', {'{$NAME}': name, '{$ARTIST}': artist});
const cpath = getUrlPath(cover);
search_song();
function search_song(page=1) {
search({
text: fname,
page: page,
callback: onsearch,
});
function onsearch(json) {
const isLastPage = (page === CONST.Number.MaxSearchPage || json.data.more === '0');
!get_song(json, isLastPage) && search_song(page+1);
}
}
function get_song(json, force=false) {
const list = json.data.list;
const song = choose(list, force);
if (song) {
dl_GM(song.url, song.fname + '.' + song.ftype);
dl(song.lrc, song.fname + '.lrc');
dl(song.cover, song.fname + song.cpath.match(/\.[a-zA-Z]+?$/)[0]);
return true;
} else {
return false;
}
function choose(list, force) {
const my_list = list.map((song) => {
const qualities = [2000, 320, 128];
const q = qualities.find((q) => (song.quality.includes(q)));
const s_url = song[({
2000: 'url_flac',
320: 'url_320',
128: 'url_128'
})[q]];
const s_ftype = ({
2000: 'flac',
320: 'mp3',
128: 'mp3'
})[q];
const s_lrc = song.lrc;
const s_cover = song.cover;
const s_name = song.name;
const s_artist = song.artist;
const s_fname = name + ' - ' + artist;
const s_cpath = getUrlPath(s_cover);
return {
ftype: s_ftype,
url: s_url,
lrc: s_lrc,
cover: s_cover,
artist: s_artist,
fname: s_fname,
cpath: s_cpath
}
})
return my_list.find((song) => (song.cpath === cpath || !cpath)) || (force ? my_list[0] : null);
}
}
}
function search(details, retry=3) {
const text = details.text;
const page = details.page || '1';
const type = details.type || 'YQD';
const callback = details.callback;
if (!text || !callback) {
throw new Error('Argument text or callback missing');
}
const url = 'http://59.110.45.28/m/api/search';
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://tools.liumingye.cn/music_old/'
},
data: encode('text='+text+'&page='+page+'&type='+type),
onload: function(res) {
let json;
try {
json = JSON.parse(res.responseText);
if (json.code !== 200) {
throw new Error('dataerror');
} else {
callback(json);
}
} catch(e) {
--retry >= 0 && search(details, retry);
return false;
}
}
});
}
function encode(plainText) {
const now = new Date().getTime();
const md5Data = md5('<G6sX,Lk~^2:Y%4Z');
let left = md5(md5Data.substr(0, 16));
let right = md5(md5Data.substr(16, 32));
let nowMD5 = md5(now).substr(-4);
let Var_10 = (left + md5((left + nowMD5)));
let Var_11 = Var_10['length'];
let Var_12 = ((((now / 1000 + 86400) >> 0) + md5((plainText + right)).substr(0, 16)) + plainText);
let Var_13 = '';
for (let i = 0, Var_15 = Var_12.length;
(i < Var_15); i++) {
let Var_16 = Var_12.charCodeAt(i);
if ((Var_16 < 128)) {
Var_13 += String['fromCharCode'](Var_16);
} else if ((Var_16 > 127) && (Var_16 < 2048)) {
Var_13 += String['fromCharCode'](((Var_16 >> 6) | 192));
Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
} else {
Var_13 += String['fromCharCode'](((Var_16 >> 12) | 224));
Var_13 += String['fromCharCode']((((Var_16 >> 6) & 63) | 128));
Var_13 += String['fromCharCode'](((Var_16 & 63) | 128));
}
}
let Var_17 = Var_13.length;
let Var_18 = [];
for (let i = 0; i <= 255; i++) {
Var_18[i] = Var_10[(i % Var_11)].charCodeAt();
}
let Var_19 = [];
for (let Var_04 = 0;
(Var_04 < 256); Var_04++) {
Var_19.push(Var_04);
}
for (let Var_20 = 0, Var_04 = 0;
(Var_04 < 256); Var_04++) {
Var_20 = (((Var_20 + Var_19[Var_04]) + Var_18[Var_04]) % 256);
let Var_21 = Var_19[Var_04];
Var_19[Var_04] = Var_19[Var_20];
Var_19[Var_20] = Var_21;
}
let Var_22 = '';
for (let Var_23 = 0, Var_20 = 0, Var_04 = 0;
(Var_04 < Var_17); Var_04++) {
let Var_24 = '0|2|4|3|5|1'.split('|'),
Var_25 = 0;
while (true) {
switch (Var_24[Var_25++]) {
case '0':
Var_23 = ((Var_23 + 1) % 256);
continue;
case '1':
Var_22 += String.fromCharCode(Var_13[Var_04].charCodeAt() ^ Var_19[((Var_19[Var_23] + Var_19[Var_20]) % 256)]);
continue;
case '2':
Var_20 = ((Var_20 + Var_19[Var_23]) % 256);
continue;
case '3':
Var_19[Var_23] = Var_19[Var_20];
continue;
case '4':
var Var_21 = Var_19[Var_23];
continue;
case '5':
Var_19[Var_20] = Var_21;
continue;
}
break;
}
}
let Var_26 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
for (var Var_27, Var_28, Var_29 = 0, Var_30 = Var_26, Var_31 = ''; Var_22.charAt((Var_29 | 0)) || (Var_30 = '=', (Var_29 % 1)); Var_31 += Var_30.charAt((63 & (Var_27 >> (8 - ((Var_29 % 1) * 8)))))) {
Var_28 = Var_22['charCodeAt'](Var_29 += 0.75);
Var_27 = ((Var_27 << 8) | Var_28);
}
Var_22 = (nowMD5 + Var_31.replace(/=/g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '.');
return (('data=' + Var_22) + '&v=2');
}
function dl(url, name) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: function(res) {
const ourl = URL.createObjectURL(res.response);
const a = document.createElement('a');
a.download = name;
a.href = ourl;
a.click();
setTimeout(function() {
URL.revokeObjectURL(ourl);
}, 0);
}
});
}
function dl_browser(url, name) {
const a = $CrE('a');
a.href = url;
a.download = name;
a.click();
}
function dl_GM(url, name) {
GM_download(url, name);
}
// Basic functions
// querySelector
function $() {
switch(arguments.length) {
case 2:
return arguments[0].querySelector(arguments[1]);
break;
default:
return document.querySelector(arguments[0]);
}
}
// querySelectorAll
function $All() {
switch(arguments.length) {
case 2:
return arguments[0].querySelectorAll(arguments[1]);
break;
default:
return document.querySelectorAll(arguments[0]);
}
}
// createElement
function $CrE() {
switch(arguments.length) {
case 2:
return arguments[0].createElement(arguments[1]);
break;
default:
return document.createElement(arguments[0]);
}
}
// Get the pathname of a given url
function getUrlPath(url) {
if (typeof url === 'string') {
const a = $CrE('a');
a.href = url;
return a.pathname;
} else {
return null;
}
}
// Replace model text with no mismatching of replacing replaced text
// e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
// replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
// replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
// replaceText('abcd', {}) === 'abcd'
/* Note:
replaceText will replace in sort of replacer's iterating sort
e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
not always the case, and the order is complex. As a result, it's best not to rely on property order.
So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
replace irrelevance replacer keys only.
*/
function replaceText(text, replacer) {
if (Object.entries(replacer).length === 0) {return text;}
const [models, targets] = Object.entries(replacer);
const len = models.length;
let text_arr = [{text: text, replacable: true}];
for (const [model, target] of Object.entries(replacer)) {
text_arr = replace(text_arr, model, target);
}
return text_arr.map((text_obj) => (text_obj.text)).join('');
function replace(text_arr, model, target) {
const result_arr = [];
for (const text_obj of text_arr) {
if (text_obj.replacable) {
const splited = text_obj.text.split(model);
for (const part of splited) {
result_arr.push({text: part, replacable: true});
result_arr.push({text: target, replacable: false});
}
result_arr.pop();
} else {
result_arr.push(text_obj);
}
}
return result_arr;
}
}
function iframeDocSync() {
const iframe = $('#g_iframe');
const oDoc = iframe && iframe.contentDocument;
if (oDoc) {
const top_path = document.URL.replace(/^https?:\/\/music\.163\.com\/#\//, '').replace(/^my\/m\//, '');
const ifr_path = oDoc.URL.replace(/^https?:\/\/music\.163\.com\//, '').replace(/^my\/#\//, '');
return top_path === ifr_path;
} else {
return false;
}
}
// Get unpolluted addEventListener
function getPureAEL(parentDocument=document) {
const ifr = makeIfr(parentDocument);
const oWin = ifr.contentWindow;
const oDoc = ifr.contentDocument;
const AEL = oWin.XMLHttpRequest.prototype.addEventListener;
return AEL;
}
// Get unpolluted addEventListener
function getPureREL(parentDocument=document) {
const ifr = makeIfr(parentDocument);
const oWin = ifr.contentWindow;
const oDoc = ifr.contentDocument;
const REL = oWin.XMLHttpRequest.prototype.removeEventListener;
return REL;
}
function makeIfr(parentDocument=document) {
const ifr = $CrE(parentDocument, 'iframe');
ifr.srcdoc = '<html></html>';
ifr.style.width = ifr.style.height = ifr.style.border = ifr.style.padding = ifr.style.margin = '0';
parentDocument.body.appendChild(ifr);
return ifr;
}
function APIHooker() {
const AH = this;
const hooker = new Hooker();
const hooker_hooks = [];
const hooks = [];
const addEventListener = (function() {
const AEL = getPureAEL();
return function() {
const args = Array.from(arguments);
const _this = args.shift();
AEL.apply(_this, args);
}
}) ();
const removeEventListener = (function() {
const REL = getPureREL();
return function() {
const args = Array.from(arguments);
const _this = args.shift();
REL.apply(_this, args);
}
}) ();
AH.hook = hook;
AH.unhook = unhook;
AH.pageOnchange = recover;
inject();
setInterval(inject, CONST.Number.Interval_Balanced);
function hook(urlMatcher, xhrDealer) {
return hooks.push({
id: hooks.length,
matcher: urlMatcher,
dealer: xhrDealer,
xhrs: []
}) - 1;
}
function unhook(id) {
hooks.splice(id, 1);
}
function inject() {
const iframe = $('#g_iframe');
const oWin = iframe ? iframe.contentWindow : null;
const hook_dealers = {
open: function(_this, args) {
const xhr = _this;
for (const hook of hooks) {
matchUrl(args[1], hook.matcher) && hook.xhrs.push(xhr);
}
return [_this, args];
},
send: function(_this, args) {
const xhr = _this;
for (const hook of hooks) {
if (hook.xhrs.includes(xhr)) {
// After first readystatechange event, change onreadystatechange to our onProgress function
let onreadystatechange;
addEventListener(xhr, 'readystatechange', function(e) {
onreadystatechange = xhr.onreadystatechange;
xhr.onreadystatechange = onProgress;
}, {
capture: false,
passive: true,
once: true
});
// Recieves last 3 readystatechange event, apply dealer function, and continue onreadystatechange stack
function onProgress(e) {
let args = Array.from(arguments);
// When onload, apply xhr dealer
let continueStack = true;
if (xhr.status === 200 && xhr.readyState === 4) {
continueStack = hook.dealer(xhr, this, args, onreadystatechange);
}
continueStack && typeof onreadystatechange === 'function' && onreadystatechange.apply(this, args);
}
}
}
return [_this, args];
},
}
let do_inject = false;
// Hook open: filter all xhr that should be hooked
try {
if (window.XMLHttpRequest.prototype.open.name !== 'hooker') {
hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.open', false, false, {
dealer: hook_dealers.open
}));
do_inject = true;
}
if (oWin && oWin.XMLHttpRequest.prototype.open.name !== 'hooker') {
hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.open', false, false, {
dealer: hook_dealers.open
}));
do_inject = true;
}
// Hook send: change eventListeners for each hooked xhr, and apply xhr dealer
if (window.XMLHttpRequest.prototype.send.name !== 'hooker') {
hooker_hooks.push(hooker.hook(window, 'XMLHttpRequest.prototype.send', false, false, {
dealer: hook_dealers.send
}));
do_inject = true;
}
if (oWin && oWin.XMLHttpRequest.prototype.send.name !== 'hooker') {
hooker_hooks.push(hooker.hook(oWin, 'XMLHttpRequest.prototype.send', false, false, {
dealer: hook_dealers.send
}));
do_inject = true;
}
} catch(err) {}
do_inject && DoLog(LogLevel.Success, 'Hooker injected');
}
function recover() {
hooker_hooks.forEach((hook) => (hooker.unhook(hook.id)));
DoLog(LogLevel.Success, 'Hooker removed');
}
function matchUrl(url, matcher) {
if (matcher instanceof RegExp) {
return !!url.match(matcher);
}
if (typeof matcher === 'function') {
return matcher(url);
}
}
function idmaker() {
let i = 0;
return function() {
return i++;
}
}
}
function Hooker() {
const H = this;
const makeid = idmaker();
const map = H.map = {};
H.hook = hook;
H.unhook = unhook;
function hook(base, path, log=false, apply_debugger=false, hook_return=false) {
// target
path = arrPath(path);
let parent = base;
for (let i = 0; i < path.length - 1; i++) {
const prop = path[i];
parent = parent[prop];
}
const prop = path[path.length-1];
const target = parent[prop];
// Only hook functions
if (typeof target !== 'function') {
throw new TypeError('hooker.hook: Hook functions only');
}
// Check args valid
if (hook_return) {
if (typeof hook_return !== 'object' || hook_return === null) {
throw new TypeError('hooker.hook: Argument hook_return should be false or an object');
}
if (!hook_return.hasOwnProperty('value') && typeof hook_return.dealer !== 'function') {
throw new TypeError('hooker.hook: Argument hook_return should contain one of following properties: value, dealer');
}
if (hook_return.hasOwnProperty('value') && typeof hook_return.dealer === 'function') {
throw new TypeError('hooker.hook: Argument hook_return should not contain both of following properties: value, dealer');
}
}
// hooker function
const hooker = function hooker() {
let _this = this === H ? null : this;
let args = Array.from(arguments);
const config = map[id].config;
const hook_return = config.hook_return;
// hook functions
config.log && console.log([base, path.join('.')], _this, args);
if (config.apply_debugger) {debugger;}
if (hook_return && typeof hook_return.dealer === 'function') {
[_this, args] = hook_return.dealer(_this, args);
}
// continue stack
return hook_return && hook_return.hasOwnProperty('value') ? hook_return.value : target.apply(_this, args);
}
parent[prop] = hooker;
// Id
const id = makeid();
map[id] = {
id: id,
prop: prop,
parent: parent,
target: target,
hooker: hooker,
config: {
log: log,
apply_debugger: apply_debugger,
hook_return: hook_return
}
};
return map[id];
}
function unhook(id) {
// unhook
try {
const hookObj = map[id];
hookObj.parent[hookObj.prop] = hookObj.target;
delete map[id];
} catch(err) {
console.error(err);
DoLog(LogLevel.Error, 'unhook error');
}
}
function arrPath(path) {
return Array.isArray(path) ? path : path.split('.')
}
function idmaker() {
let i = 0;
return function() {
return i++;
}
}
}
function IntervalTaskManager() {
const tasks = this.tasks = [];
this.time = 500;
this.interval = -1;
defineProperty(this, 'working', {
get: () => (this.interval >= 0)
});
this.addTask = function(fn) {
tasks.push(fn);
}
this.removeTask = function(fn_idx) {
const idx = typeof fn_idx === 'number' ? fn_idx : tasks.indexOf(fn_idx)
tasks.splice(idx, 1)
}
this.clearTasks = function() {
tasks.splice(0, Infinity)
}
this.start = function() {
if (!this.working) {
this.interval = setInterval(this.do, this.time);
return true;
} else {
return false;
}
}
this.stop = function() {
if (this.working) {
clearInterval(this.interval);
this.interval = -1;
return true;
} else {
return false;
}
}
this.do = function() {
for (const task of tasks) {
task();
}
}
}
function defineProperty(obj, prop, desc) {
desc.configurable = false;
desc.enumerable = true;
Object.defineProperty(obj, prop, desc);
}
})();