// ==UserScript==
// @name Kaskus Spoiler Alert
// @namespace http://userscripts.org/scripts/show/73498
// @description add a warning message when a spoiler contains hidden link
// @include *.kaskus.*/thread/*
// @include *.kaskus.*/post/*
// @include *.kaskus.*/product/*
// @include *.kaskus.*/show_post/*
// @include *.kaskus.*/group/discussion/*
// @include *.kaskus.co.id/thread/*
// @include *.kaskus.co.id/post/*
// @include *.kaskus.co.id/product/*
// @include *.kaskus.co.id/show_post/*
// @include *.kaskus.co.id/group/discussion/*
// @version 2.66
// @author arifhn
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// ==/UserScript==
/**
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* Changelog:
* ==========
*
* 2.6
* - fix new kaskus
* - add setting key into greasemonkey/tampermonkey menu
* - add 'Show All' menu to 'Thread Tools'
* - fix group page
*
* 2.5
* - fix button width on kaskus beta
*
* 2.4
* - support livebeta.kaskus.us
*
* 2.3
* - change shortcut for setting, now use Ctrl+Alt+S
* - fix update notification
*
* 2.2
* - add settings for enable/disable link preview
* - add close button to popup box
*
* 2.1 fix bug on Firefox 3.6.x
*
* 2.0
* - rewrite source code
* - support Firefox 6 - support IDWS
*
* 1.20
* add button 'Show All' at top & bottom thread.
* This button can show/hide all spoiler in current page
*
* 1.19
* - add browser support Firefox 5.x
* - add html tag id (now easy to identify html tag created by KSA)
*
* 1.18
* fix bug: hidden link not detected if bbcode contains space/text between
* URL and SPOILER, sample bbcode:
* [URL=http://www.foo.com/#] extra space
* [SPOILER=bar]lol[/SPOILER][/URL]
* thanks to tuxie.forte
*
* 1.17
* add new feature: preview URL (title + original link)
*
* 1.16
* add browser support Firefox 4.x
*
* 1.15
* - support any standard vbulletin forum (tested on kaskus.us, indoforum.org)
* - change all warning messages to english - add script update notification
*
* 1.14
* support google chrome
*
* 1.13
* add http://www.kaskus.us/group.php* to @include and
* change the script to support it
*
* 1.12
* fix bug: link alert (too sensitive, now only check domain name)
*
* 1.11
* - scroll page to closed spoiler after 'Hide All' clicked
* - hide button Show/Hide All if post contains 1 spoiler
* - add new feature: fake link alert (show info if link text not equal to
* link url)
*
* 1.10
* add http://www.kaskus.us/showpost.php* to @include,
* thanks to hermawanadhis
*
* 1.9
* revert back to 1.7 design with two button ('Show All' and 'Show') and
* remove the popup menu
*
* 1.8
* - move 'Show All' and 'Show Children' button into popup menu
* - fixed bug: button label 'Show'/'Hide'
*
* 1.7
* - change button 'Show All' -> 'Show Children' (open/close child spoiler)
* - add button 'Show All' (open/close all spoilers)
*
* 1.6
* add button 'Show All' to open/close all child spoiler
*
* 1.5
* exclude kaskus smiley from picture counter
*
* 1.4
* add new feature: show how many picture and spoiler inside spoiler
*
* 1.3
* rewrite link detection thanks to "p1nk3d_books"
*
* 1.2
* fixed bug, hidden link not detected if font color changed thanks to
* "p1nk3d_books"
*
* 1.1
* remove link from spoiler and show the hidden link thanks to "firo sXe"
* (kaskusid 650045)
*
* 1.0
* first release
*
*/
(function() {
var script = {
putValue: function(key, value) {
if(typeof window.localStorage != 'undefined') {
localStorage[key] = value;
}else {
GM_setValue(key, value);
}
},
getValue: function(key, defValue) {
if(typeof window.localStorage != 'undefined') {
ret = localStorage[key];
if(ret == null) {
ret = defValue;
}
return ret;
}else {
return GM_getValue(key, defValue);
}
return defValue;
}
};
// returns true if string contains s
String.prototype.contains = function(s) {
return (this.indexOf(s) != -1);
};
// returns true if string starts with s
String.prototype.startsWith = function(s) {
return (this.substring(0, s.length) == s);
};
String.prototype.escapeHTML = function() {
return this.replace(/&/g, "&").replace(/</g, "<").replace(/>/g,
">");
};
/*
* // dont run in frames, prevents detection and misuse of unsafewindow try {
* var unsafeWindow = unsafeWindow || window.wrappedJSObject || window;
* if(unsafeWindow.frameElement != null) return; } catch(e) {}
*/
var URL = {
// Returns the filename component of the path
// + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// * example 1: basename('/www/site/home.htm', '.htm'); // * returns 1:
// 'home'
// * example 2: basename('ecra.php?p=1');
filename : function(path) {
var suffix = null;
var tailcut = "\\?";
var b = path.replace(/^.*[\/\\]/g, '');
if (typeof (suffix) == 'string'
&& b.substr(b.length - suffix.length) == suffix)
b = b.substr(0, b.length - suffix.length);
if (typeof (tailcut) == 'string')
b = b.replace(new RegExp(tailcut + ".*$", "g"), '');
return b;
},
domain : function(url) {
var m = url.match(/(http:\/\/[^?\/]+)/);
if (m) {
return m[1];
}
return null;
}
};
function getCurrentYPos() {
if (document.body && document.body.scrollTop)
return document.body.scrollTop;
if (document.documentElement && document.documentElement.scrollTop)
return document.documentElement.scrollTop;
if (window.pageYOffset)
return window.pageYOffset;
return 0;
}
function click(elm) {
var evt = document.createEvent('MouseEvents');
evt.initEvent('click', true, true);
elm.dispatchEvent(evt);
}
// Utility function for mouseout listener
function isChildOf(parent, child) {
if (child != null) {
while (child.parentNode) {
if ((child = child.parentNode) == parent) {
return true;
}
}
}
return false;
}
function getElement(q, root, single) {
if (root && typeof root == 'string') {
root = $(root, null, true);
if (!root) {
return null;
}
}
root = root || document;
if (q[0] == '#') {
return root.getElementById(q.substr(1));
} else if (q[0] == '/' || (q[0] == '.' && q[1] == '/')) {
if (single) {
return document.evaluate(q, root, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
} else {
var i, r = [], x = document.evaluate(q, root, null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
while ((i = x.iterateNext()))
r.push(i);
return r;
}
} else if (q[0] == '.') {
return root.getElementsByClassName(q.substr(1));
}
return root.getElementsByTagName(q);
}
function VBLink(el, parent, idx) {
this.id = 'KSA-link-' + parent.id + '-' + idx;
this.element = el;
this.vbPost = parent;
this.noPreviewURL = function() {
var configLinkPreview = script.getValue("KSA_LINK_PREVIEW", "true");
if(configLinkPreview == 'false') {
return true;
}
var is_ff = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (!is_ff) { // disable preview URL on browser other than firefox
return true;
}
var url = this.element.href;
var blacklist = new Array('/\.exe$/', '/\.rar$/', '/\.7z$/',
'/\.mp3$/', '/\.zip$/',
'/^http:\/\/report.kaskusnetworks.com\/index\.php\/laporhansip/');
for ( var i = 0; i < blacklist.length; ++i) {
var p = new RegExp(blacklist[i]);
if (p.test(url)) {
return true;
}
}
var pid = this.vbPost.id.match(/([0-9]+)/)[1];
if (url.contains(pid)) {
return true;
}
if (url.contains('/member.php?')) {
return true;
}
if (this.element.innerHTML
.contains('http://static.kaskus.us/images/buttons/viewpost.gif')) {
return true;
}
return false;
};
this.showPreview = function(event) {
if (event.pageX || event.pageY) {
posx = event.pageX;
posy = event.pageY;
} else if (event.clientX || e.clientY) {
posx = event.clientX + document.body.scrollLeft
+ document.documentElement.scrollLeft;
posy = event.clientY + document.body.scrollTop
+ document.documentElement.scrollTop;
}
var top = (posy + 20) + 'px';
var left = (posx - 50) + 'px';
if (!this.contentTitle) {
GM_xmlhttpRequest( {
vbLink : this,
method : 'GET',
url : this.element.href,
onerror : function(rsp) {
this.vbLink.contentTitle = 'Error';
this.vbLink.contentURL = 'Invalid or blocked URL';
VBPage.showPopup(left, top, this.vbLink.contentTitle,
this.vbLink.contentURL + '<br/><br/><font color="red">You can disable this popup in Thread Tools menu</font>');
},
onload : function(rsp) {
if (rsp.status == 200) {
this.vbLink.contentTitle = rsp.finalUrl;
this.vbLink.contentURL = rsp.finalUrl;
var patt = new RegExp("<title>([^<]*)</title>", "i");
var title = patt.exec(rsp.responseText);
if (title) {
this.vbLink.contentTitle = title[1];
}
} else {
this.vbLink.contentTitle = 'Error';
this.vbLink.contentURL = 'Invalid or blocked URL';
}
VBPage.showPopup(left, top, this.vbLink.contentTitle,
this.vbLink.contentURL + '<br/><br/><font color="red">You can disable this popup in Thread Tools menu</font>');
}
});
} else {
VBPage.showPopup(left, top, this.contentTitle, this.contentURL + '<br/><br/><font color="red">You can disable this popup in Thread Tools menu</font>');
}
};
this.hidePreview = function(event) {
var a = this.element;
var current_mouse_target = null;
if (event.toElement) {
current_mouse_target = event.toElement;
} else if (event.relatedTarget) {
current_mouse_target = event.relatedTarget;
}
// Code inside this if is executed when leaving the link and it's
// children, for good
if (a != current_mouse_target
&& !isChildOf(a, current_mouse_target)) {
VBPage.hidePopup();
}
};
this.setupKSA = function(index) {
var btn = getElement('.//input[@type="button" and @value="Show"]',
this.element);
if (btn.length > 0) { // kalau ada button 'Show' berarti spoiler
// jebakan
// move children element of the link to its parent node
var achildren = this.element.childNodes;
var n = achildren.length;
for ( var k = 0; k < n; ++k) {
this.element.parentNode.insertBefore(achildren[0],
this.element);
}
// change link title to another text
this.element.innerHTML = ' Hidden Link >> ' + this.element.href
.escapeHTML();
this.element.style.color = 'red';
this.element.style.textDecoration = 'none';
this.element.id = this.id + '-trap';
btn[0].parentNode.appendChild(this.element);
} else {
// cek link jebakan
var patt = new RegExp("^\s*http:\/\/[^?\/]+", "i");
this.title = patt.exec(this.element.innerHTML.trim());
this.url = patt.exec(this.element.href);
// kalo innerHTML starts with http dan gak sama dgn href
if (this.title && this.url
&& this.title.toString() != this.url.toString()) {
// buat info link jebakan
var info = document.createElement('span');
info.id = this.id + '-info';
info.className = 'smallfont';
info.style.color = 'red';
info.innerHTML = ' Original Link >> ' + this.element.href;
if (this.element.nextSibling) {
this.element.parentNode.insertBefore(info,
this.element.nextSibling); // add info to page
} else {
this.element.parentNode.appendChild(info);
}
}
if (!this.noPreviewURL()) {
this.element.vbLink = this;
this.element.setAttribute('linkid', this.id);
this.element.addEventListener('mouseover', function(event) {
if (this.vbLink) {
this.vbLink.showPreview(event);
} else {
VBPage.getLink(this.getAttribute('linkid'))
.showPreview(event);
}
}, false);
this.element.addEventListener('mouseout', function(event) {
if (this.vbLink) {
this.vbLink.hidePreview(event);
} else {
VBPage.getLink(this.getAttribute('linkid'))
.hidePreview(event);
}
}, false);
}
}
};
}
function VBSpoiler(el, parent, idx) {
if (parent != 'none') {
this.id = 'KSA-spoiler-' + parent.id + '-' + idx;
el.id = 'KSA-spoiler-' + parent.id + '-' + idx;
}
this.element = el;
this.vbPost = parent;
this.content = getElement(
'.//div//div[starts-with(@class,"content_spoiler")]', el, true);
// check wether this is top spoiler in current post
this.isTopSpoiler = function() {
return null;// this.vbPost != 'none';
};
// +++++++++++++++++++++++++++++++++++++++++++++++++++ KSA functions
this.showClick = function(str) {
if (str == 'Show') {
this.btShow.value = 'Hide';
this.btShowAll.value = 'Hide All';
this.content.style.display = '';
} else {
this.btShow.value = 'Show';
this.btShowAll.value = 'Show All';
this.content.style.display = 'none';
}
var bt = getElement('.//input[@type="button"]', this.element);
for ( var i = 0; i < bt.length; ++i) {
if (bt[i].value == str && bt[i] != this.btShow
&& bt[i] != this.btShowAll) {
click(bt[i]);
}
}
/*
* if(str.contains('Hide') && this.isTopSpoiler) {
* this.element.scrollIntoView(true); }
*/
};
this.setupKSA = function() {
this.innerSpoilers = getElement(
'.//div[contains(@id,"KSA-spoiler-")]', this.element).length;
// get button Show
var btShowOri = getElement(
'.//input[@type="button" and @value="Show"]', this.element,
true);
if(btShowOri) {
btShowOri.removeAttribute('onclick');
this.btShow = btShowOri.cloneNode(true);
btShowOri.parentNode.replaceChild(this.btShow, btShowOri);
}else {
var div = getElement('.//div[@class="smallfont"]', this.element, true);
this.btShow = document.createElement('input');
this.btShow.value = "Show";
this.btShow.type = 'button';
this.btShow.title = 'Show/Hide spoiler';
div.appendChild(this.btShow);
this.content.style.display = 'none';
}
this.btShow.vbSpoiler = this;
this.btShow.setAttribute('spoilerid', this.id);
this.btShow.removeAttribute('onclick');
this.btShow.setAttribute('style', 'margin: 2px; font-size: 10px; padding: 2px; width: 60px !important');
this.btShow.addEventListener('click', function() {
if (this.vbSpoiler) {
this.vbSpoiler.showClick(this.value);
} else {
VBPage.getSpoiler(this.getAttribute('spoilerid'))
.showClick(this.value);
}
}, true);
// add button Show All
this.btShowAll = document.createElement('input');
this.btShowAll.value = "Show All";
this.btShowAll.type = 'button';
this.btShowAll.title = 'Show/Hide all spoilers';
this.btShowAll.setAttribute('style', 'margin: 2px; font-size: 10px; padding: 2px; width: 60px !important');
this.btShowAll.vbPost = this.vbPost;
this.btShowAll.setAttribute('postid', this.vbPost.id);
this.btShowAll.addEventListener('click', function() {
if (this.vbPost) {
// substring to remove 'All'
this.vbPost.showAllClick(this.value.substring(0, 4));
} else {
VBPage.getPost(this.getAttribute('postid')).showAllClick(
this.value.substring(0, 4));
}
}, true);
this.btShow.parentNode.insertBefore(this.btShowAll, this.btShow);
var infoMsg = '';
// cek jumlah gambar dalam spoiler
var imgs = getElement('.//img', this.element);
if (imgs.length > 0) {
var maxSize = 0;
var idx = -1;
var totalGambar = 0;
for ( var k = 0; k < imgs.length; ++k) {
if (imgs[k].src
.indexOf('http://static.kaskus.us/images/smilies') == -1) { // ignore
// kaskus
// smiley
var size = imgs[k].width * imgs[k].height;
if (size > 3000) { // ignore icon < 50x50
if (maxSize < size) {
maxSize = size;
idx = k;
}
++totalGambar;
}
}
}
if (totalGambar > 0) {
infoMsg = totalGambar + ' pics inside (max: '
+ imgs[idx].width + 'x' + imgs[idx].height + ') ';
}
}
if (this.innerSpoilers > 0) {
if (infoMsg.length > 0) {
infoMsg += ', ';
}
infoMsg += this.innerSpoilers + ' spoiler(s)';
}
// tampilkan info
if (infoMsg.length > 0) {
var info = document.createElement('span');
info.id = this.id + '-info';
info.className = 'smallfont';
info.style.color = 'darkblue';
info.innerHTML = infoMsg;
var anchor = getElement('.//div[@class="alt2"]', this.element,
true);
this.element.insertBefore(info, anchor);
}
};
}
function VBPost(el) {
this.id = el.id;
this.element = el;
// return all spoilers in this post
this.getSpoilers = function() {
var ret = [];
var el = getElement('.//div[@class="spoiler"]', this.element);
for ( var i = 0; i < el.length; ++i) {
var sp = el[i];
ret.push(new VBSpoiler(sp, this, i));
}
return ret;
};
this.getSpoiler = function(id) {
for ( var i = 0; i < this.spoilers.length; ++i) {
if (this.spoilers[i].id == id) {
return this.spoilers[i];
}
}
};
this.getLinks = function() {
var ret = [];
var el = getElement('.//div[@class="entry"]//a', this.element);
for ( var i = 0; i < el.length; ++i) {
var l = el[i];
ret.push(new VBLink(l, this, i));
}
return ret;
};
this.getLink = function(id) {
for ( var i = 0; i < this.links.length; ++i) {
if (this.links[i].id == id) {
return this.links[i];
}
}
};
this.showAllClick = function(str) {
for ( var i = 0; i < this.spoilers.length; ++i) {
this.spoilers[i].showClick(str);
}
/*
* if(str.contains('Hide')) { this.element.scrollIntoView(true); }
*/
};
this.setupKSA = function() {
this.spoilers = this.getSpoilers();
for ( var i = 0; i < this.spoilers.length; ++i) {
this.spoilers[i].setupKSA(i);
}
if (this.spoilers.length == 1) {
// hide btn 'show all' if there is only 1 spoiler
this.spoilers[0].btShowAll.style.display = 'none';
}
this.links = this.getLinks();
for ( var i = 0; i < this.links.length; ++i) {
this.links[i].setupKSA(i);
}
};
}
// VBulletin page
var VBPage = {
getPosts : function() {
var ret = [];
if (location.href.contains('/group/discussion/')) {
var el = getElement('.//section[@id="xxxxx-postNumberHere"]');
for ( var i = 0; i < el.length; ++i) {
var p = el[i].parentNode.parentNode;
p.id = 'post_' + i;
ret.push(new VBPost(p));
}
} else {
var el = getElement('.//*[starts-with(@id,"post")]');
var pattern = new RegExp('^post_?[0-9a-f]+$'); // match post1234abc or
// post_1234
for ( var i = 0; i < el.length; ++i) {
if (pattern.test(el[i].id)) {
ret.push(new VBPost(el[i]));
}
}
}
return ret;
},
getPost : function(id) {
for ( var i = 0; i < this.posts.length; ++i) {
if (this.posts[i].id == id) {
return this.posts[i];
}
}
},
getSpoiler : function(id) {
var ids = id.split('-');
return this.getPost(ids[2]).getSpoiler(id);
},
getLink : function(id) {
var ids = id.split('-');
return this.getPost(ids[2]).getLink(id);
},
showPopup : function(x, y, title, content) {
this.popup.style.top = y;
this.popup.style.left = x;
this.popup.innerHTML = '<table cellspacing="1" width="350px"><tr><td class="thead"><b>'
+ title
+ '</b><div style="float:right"><a id="ksaClosePopup">x</a></div></td></tr><tr><td class="vbmenu_option vbmenu_option_alink">'
+ content + '</td></tr></table>';
this.popup.style.display = '';
getElement('#ksaClosePopup').addEventListener('click', function() {
VBPage.hidePopup();
}, true);
},
hidePopup : function() {
this.popup.style.display = 'none';
},
showAllClick : function(item) {
var str = item.getAttribute('ksa-action');
if(str == null) {
str = 'Show';
}
for ( var i = 0; i < this.posts.length; ++i) {
this.posts[i].showAllClick(str);
}
var items = getElement('.//a[@ksa-tools-id="ksa-all-spoiler"]');
for(var i = 0; i < items.length; ++i) {
if(str == 'Show') {
items[i].setAttribute('ksa-action', 'Hide');
items[i].innerHTML = '<i class="fa fa-caret-down"></i> KSA - Hide all spoiler';
}else {
items[i].setAttribute('ksa-action', 'Show');
items[i].innerHTML = '<i class="fa fa-caret-down"></i> KSA - Show all spoiler';
}
}
},
onKeyDown : function(event) {
// Ctrl+Alt+S
if (event.ctrlKey && event.altKey && event.keyCode == 83) {
VBPage.openThreadTools();
}
},
// because we only have one setting item then we use 'Thread Tools' menu for it
openThreadTools : function() {
var threadtools = getElement('.//div[contains(@class,"tools-panel dropdown")]', null, true);
if(threadtools) {
threadtools.setAttribute('class', threadtools.getAttribute('class') + ' open');
document.documentElement.scrollTop = 0;
}
},
closeThreadTools : function() {
var threadtools = getElement('.//div[contains(@class,"tools-panel dropdown open")]', null, true);
if(threadtools) {
attr = threadtools.getAttribute('class');
threadtools.setAttribute('class',attr.substring(0, attr.length - 5));
}
},
addThreadTools : function(title, icon, id, callback) {
var dropdown = getElement('.//ul[@aria-labelledby="thread-tools"]');
if(dropdown && dropdown.length > 0) {
for(var i = 0; i < dropdown.length; ++i) {
var item = document.createElement('li');
var link = document.createElement('a');
link.href = '#';
link.innerHTML = '<i class="fa '+icon+'"></i>'+title;
link.addEventListener('click', callback);
link.setAttribute('ksa-tools-id', id);
item.appendChild(link);
dropdown[i].appendChild(item);
}
}else {
// try to set mobile site layout
var tools = getElement('.//div[@class="tools"]', null, true);
var item = document.createElement('span');
item.setAttribute('class', 'p-right');
item.innerHTML = ' | ';
var link = document.createElement('a');
link.href = '#';
link.innerHTML = '<i class="fa '+icon+'"></i>'+title;
link.addEventListener('click', callback);
link.setAttribute('ksa-tools-id', id);
item.insertBefore(link, item.firstChild);
tools.appendChild(item);
}
},
setupKSA : function() {
this.posts = this.getPosts();
for ( var i = 0; i < this.posts.length; ++i) {
this.posts[i].setupKSA();
}
this.popup = document.createElement('div');
this.popup.setAttribute('class', 'vbmenu_popup');
this.popup.style.display = 'none';
this.popup.style.position = 'absolute';
this.popup.style.top = '50px';
this.popup.style.left = '50px';
this.popup.style.backgroundColor = '#eeeeee';
getElement('body')[0].appendChild(this.popup);
// setup show all spoiler
this.addThreadTools(' KSA - Show all spoiler', 'fa-caret-down', 'ksa-all-spoiler', function() {
VBPage.showAllClick(this);
VBPage.closeThreadTools();
});
// add link preview setting to 'Thread Tools'
var configLinkPreview = script.getValue("KSA_LINK_PREVIEW", 'true') == 'true'?' KSA - Hide link preview':' KSA - Show link preview';
this.addThreadTools(configLinkPreview, 'fa-gear', 'ksa-link-preview', function() {
var config = script.getValue("KSA_LINK_PREVIEW", 'true');
if(config == 'true') {
script.putValue("KSA_LINK_PREVIEW", 'false');
}else {
script.putValue("KSA_LINK_PREVIEW", 'true');
}
location.reload();
});
// add keyboard listener
window.addEventListener('keydown', function(e) {
VBPage.onKeyDown(e);
}, true);
if(GM_registerMenuCommand != 'undefined') {
GM_registerMenuCommand("KSA Setting", VBPage.openThreadTools);
}
}
};
VBPage.setupKSA();
})();