The One with the Thoughts of Frans

Archive for Scripting

Web Apps Opera Bork Edition

It’s been 18 years since Opera published their classic bork edition, to protest that MSN would have loaded properly in the browser if only it were served the same code as Internet Explorer.

“Hergee berger snooger bork,” says Mary Lambert, product line manager desktop, Opera Software. “This is a joke. However, we are trying to make an important point. The MSN site is sending Opera users what appear to be intentionally distorted pages. The Bork edition illustrates how browsers could also distort content, as the Bork edition does. The real point here is that the success of the Web depends on software and Web site developers behaving well and rising above corporate rivalry.”

Since I’m a Vivaldi user, today’s the first time I noticed that Chrome on Android artificially restricts installing webpages as apps on the homescreen. Only webpages that specify a manifest.json can receive such a hallowed treatment, instead of every single webpage ever made. For the rest of the internet, there’s only a shortcut. While the situation is not quite comparable, I found the design principle sufficiently distasteful to revive Opera’s classic bork script, in this case specifically targeting the Chrome browser.

You can put it on your website or in your TamperMonkey to remind you when you accidentally open Chrome. The classic result looks like this:

A borked Vivaldi announcement as seen in Chrome.
// http://web.archive.org/web/20050301075735/http://www.opera.com/js/bork/enchefizer.js

/* -*- mode: C++; mode: font-lock; tab-width: 4 -*-
 * 2003-02-10
 *
 * The Enchefizer code is based on a script fetched from 
 *   http://tbrowne.best.vwh.net/chef/
 * written by Andriy Rozeluk , which is
 * based on a Java version written by Josh Vura-Weis
 * , which is based on a UNIX version
 * from 1993 written by John Hagerman  and
 * Jeff Allen 
 *
 * Subsequently hacked by Opera Software to work inside a page by 
 * traversing the DOM tree, and to improve performance. 
 *
 * Typical usage is to add the following text to the bottom of a page:
 *       
 */

const classicOperaBork = () => {
    /* USER CONFIGURATION BEGINS */
    var victim=false;   // false (apply to any page) or regex to match page URL
    //var victim=/^http:\/\/(?:(?:www|msdn).microsoft.com|www.msn.com)/;
    var delay=50;       // ms between replacements, set to 0 to disable waiting
    var units=30;       // number of text nodes to translate each time
    var highlight=true; // highlight the text we're working on
    /* USER CONFIGURATION ENDS */

    var textnodes=[];   // text nodes in the doc
    var nextnode=0;     // next node to process

    function nextWordPos(line)
    {
		var p = line.search(/[ \n\t\\,<.>/?;:\'\"\[{\]}|=+\-_!@#$%^&*()~`]/);
        return p == -1 ? line.length+1 : p;
    }

    function encheferizeLine(line)
    {
		var buff="", word="", t="", out="", wp;

		while(line.length > 0)
		{
			wp = nextWordPos(line);
			word = line.substring(0,wp);
			t = line.charAt(wp);
			line = line.substring(wp+1,line.length);
			out = out + encheferizeWord(word) + t;
		}
		if(t == ".")
		{
	        out = out + "\nBork Bork Bork!";
		}

		return out;
    }

    function encheferizeWord(word)
    {
		if(word.toLowerCase() == "bork") return word;
      
		var letter, count, len, buff, i_seen, isLast;
      
		count=0;
		len=word.length;
		buff=""
		i_seen=false;
      
		while(count0){
               
				} 
			} else if(letter=='t'){
				if(count==len-2 && word.charAt(count+1)=='h'){
					buff = buff + "t";
					count+=2;
					continue;
				} else if(count<=len-3 && word.charAt(count+1)=='h'
						  && word.charAt(count+2)=='e'){
					buff = buff + "zee";
					count+=3;
					continue;
				} 
			} else if(letter=='T' && count<=len-3 && word.charAt(count+1)=='h'
					  && word.charAt(count+2)=='e'){                                                
				buff = buff + "Zee";
				count+=3;
				continue;
			} else if(letter=='v'){
				buff = buff + "f";
				count++;
				continue;
			} else if(letter=='V'){
				buff = buff + "F";
				count++;
				continue;
			} else if(letter=='w'){
				buff = buff + "v";
				count++;
				continue;
			} else if(letter=='W'){
				buff = buff + "V";
				count++;
				continue;
			}
			//End of rules.  Whatever is left stays itself
			buff = buff + letter;
			count++;
		}
		
		return(buff);
    }

    function bork()
    {
		var limit = delay == 0 ? Number.MAX_VALUE : units;
		var start=nextnode;
		var oldc = new Array();
		var n, i, candidate;

		if (highlight)
		{
			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				oldc[i] = candidate.parentNode.style.backgroundColor;
			}

			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				candidate.parentNode.style.backgroundColor = 'red';
			}
		}

		for ( i=0 ; i < limit && nextnode < textnodes.length ; nextnode++, i++ )
		{
			candidate = textnodes[nextnode];
            candidate.replaceData(0,candidate.length,encheferizeLine(candidate.data));
		}

		if (highlight)
		{
			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				candidate.parentNode.style.backgroundColor = oldc[i];
			}
		}

		bork_more();
    }

    function bork_more()
    {
		if (nextnode < textnodes.length)
		{
			setTimeout( bork, delay );
		}
    }

    /* In large docs traversal is a bottleneck at startup; we could
       CPS it or otherwise reify the traversal state to interleave
       traversal with the translation.
	*/
    function find_textnodes(elm, acc)
    {
		if (elm.nodeType == 3)
		{
			if (!elm.data.match(/^[\s\n\r]*$/))
			{
				acc.push(elm);
			}
		}
		else
		{
			var c = elm.childNodes;
			for ( var i=0 ; i < c.length ; i++ )
			{
				find_textnodes(c.item(i),acc);
			}
		}
		return acc;
    }

    /* run page's onload handler, then do our thing */
    var res = false;

    if (typeof old_onload == "function")
	{
		res = old_onload();
	}

    if (/*window == top &&*/ (!victim || window.location.href.match(victim)) )
    {
		textnodes = find_textnodes(document.body, new Array());
		nextnode=0;
		bork_more();
    }
    return res;
}
if (window.navigator.userAgentData.brands.filter(e => e.brand === 'Google Chrome').length > 0) {
    document.addEventListener('DOMContentLoaded', classicOperaBork);
}

CommentsTags:

SSH publickey denied?

I was suddenly having trouble connecting to GitHub, after pulling in an OpenSSH update to version 7. Chances are that means the problem is security-related, meaning it’s worthwhile to take the time to investigate the cause.

$ git pull
Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

A little debugging showed the following:

$ ssh -vT git@github.com
OpenSSH_7.1p2 Debian-2, OpenSSL 1.0.2f  28 Jan 2016
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: Applying options for *
debug1: Connecting to github.com [192.30.252.130] port 22.
debug1: Connection established.
[…]
debug1: Skipping ssh-dss key /home/frans/.ssh/id_dsa for not in PubkeyAcceptedKeyTypes
[…]
debug1: No more authentication methods to try.
Permission denied (publickey).

Of course I could quickly fix the problem by adding PubkeyAcceptedKeyTypes ssh-dss to ~/.ssh/config, but checking OpenSSH.com tells me that “OpenSSH 7.0 and greater similarly disables the ssh-dss (DSA) public key algorithm. It too is weak and we recommend against its use.” So, although I could obviously re-enable it easily, I guess I’ll have to generate a new key. I hope GitHub’s guide is accurate for generating something sufficiently secure, because I’m kind of ticked off that something I generated in 2013 is already considered “legacy.” I hope I’m to blame and not an earlier version of GitHub’s guide.

Incidentally, to change the passphrase one would use the -p option, e.g.:

ssh-keygen -f id_rsa -p

Comments

Custom page number count in Prince

Prince makes it really easy to do all of the usual things with page numbers, like a different numbering scheme in the front matter and whatnot. Unfortunately you can’t counter-increment on @page, but thanks to Prince.addScriptFunc() you’ve got something better.

h2 {counter-reset: page 50}

@page {
	@bottom-left {
		content: prince-script(fixpagenum, counter(page));
		margin-left: 2cm;
	}
}

In this CSS, instead of passing regular generated content, we’re passing a prince-script. That script has to be defined somewhere, like this.

Prince.addScriptFunc("fixpagenum", function(pagenum) {
	pagenum = Number(pagenum);
	pagenum = pagenum + pagenum - 50;
	return pagenum;
});

The rationale in this case was to generate two separate documents, starting at page 50, one only left pages and the other only right pages. (Of course, the other one started at page 51.) I combined them with pdftk’s shuffle command.

pdftk left.pdf right.pdf shuffle output combined.pdf

I don’t think there’s a way to do something like this purely in Prince using CSS, but I’d love to be proved wrong.

Comments

Stop Hijacking Annoyances

Here’s another rescue from My Opera. The script was compiled by the user pehage on February 3, 2012, after a quick pointer by me. It is Opera UserJS, so it won’t work in any other browser. Don’t forget to enable User JavaScript on HTTPS if you so desire. In Opera 12 it no longer pops up a warning all the time.

// ==UserScript==
// @name Stop Hijacking Annoyances
// @include *
// ==/UserScript==

var annoyances = ["focus", "focusin", "focusout", /*"click",*/ "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "select", "submit", "keydown", "keypress", "keyup"];

for (var i=0; i<annoyances.length; i++) {
	//opera.postError(annoyances[i]);
	window.opera.addEventListener("BeforeEventListener." + annoyances[i], function (e) {
		e.preventDefault();
		//opera.postError(e);
	}, false);
}

And here is a simple testcase I created.

<!DOCTYPE html>
<html>
<head>
<title>keypress hijacker</title>
<script>
document.addEventListener('keypress', function(e){alert('document.addEventlistener on keypress ' + String.fromCharCode(e.which))}, false);
window.onkeydown = function(e){alert('window.onkeydown ' + String.fromCharCode(e.which))}
</script>
</head>
<body>
This page hijacks keypress events in order to display an alert message.
</body>
</html>

Comments

Data URI Bookmarklet

This is a repost of something I wrote on My Opera forums on 8 January 2013. The My Opera forums will be shutting down on 1 March 2014.


The easiest method [to base64 encode an image] might be to use the newly supported (since v12) FileReader object, which has a readAsDataURL method. I wrote a quick proof of concept using XHR because I’m not really sure how else to get data to it (aside from a file input which is no good here). Alternatively you could load the image in a canvas and call toDataURL, but then you’ll lose stuff like metadata and I’m not even sure if the compression and such will remain the same.

var xhr = new XMLHttpRequest(),
	reader = new FileReader();
xhr.open('GET', location.href, true);
xhr.responseType = 'blob';
xhr.addEventListener('load', function () {
	if (xhr.status === 200) {
		reader.addEventListener('load', function (e) {
			location.href = e.target.result;
		});
		var responseWithMimeType = new Blob(new Array(xhr.response), {
			'type': xhr.getResponseHeader('Content-Type')
		});
		reader.readAsDataURL(responseWithMimeType);
	}
});
xhr.send();

It does seem a bit roundabout, so I’d love to hear it if anyone has more efficient suggestions.

I just realized you may not be aware of how to make a bookmarklet. To save myself the effort of removing comments and such I used http://chris.zarate.org/bookmarkleter to obtain the following result:

javascript:(function(){var%20xhr=new%20XMLHttpRequest(),reader=new%20FileReader();xhr.open('GET',location.href,true);xhr.responseType='blob';xhr.addEventListener('load',function(){if(xhr.status===200){reader.addEventListener('load',function(e){location.href=e.target.result;});var%20responseWithMimeType=new%20Blob(new%20Array(xhr.response),{'type':xhr.getResponseHeader('Content-Type')});reader.readAsDataURL(responseWithMimeType);}});xhr.send();})();

Create a new bookmark, paste that into the URL and give it a nickname of your choice.

Easier still, just drag this or right-click, bookmark link: To Data URI. There might be some security restrictions on MyOpera against javascript links, so I’m not sure if it’ll work.

But for local files a simple page would do just fine. All you need is input type=”file” and a tiny bit of scripting similar to the above. Something like this should do:

<!DOCTYPE html>
<html>
<head>
<title>File to Data URL</title>
</head>
<body>

<form>
<input type="file">
</form>

<script>
var file = document.querySelector('input[type=file]'), reader = new FileReader();

file.addEventListener('change', function(e) {
	reader.readAsDataURL(e.target.files[0]);
});

reader.addEventListener('load', function(e) {
	location.href = e.target.result;
});
</script>

</body>
</html>

Comments

Adding a User Font Size Preference to Simple Machines

Users should be able to choose their default font size easily through their browser, and their choice should be respected. But because all browsers default to 16px and most people never change the default, many sites—including Wikipedia—set their font-size to about 80% of that value to end up at a font size of 12px or 13px. Thus users might be prevented from lowering their font size on a site that actually respects their preferences if they so desire.

Because I don’t believe that either joining the 80% crowd or specifying a size in pixels is generally the right answer, I decided to add a user preference that overrides the font-size on the HTML element. For this to work as intended, your stylesheet needs to be entirely in percentages, em, rem, or use equivalent keywords like small and large.

First, in Admin > Configuration > Core Features, enable Advanced Profile Fields. Then, you can add custom profile fields in Admin > Configuration > Features and Options > Profile Fields. I added one named “Font size” with an input mask of /^[0-9]{1,3}(\.[0-9]{1,2})?(em|pt|px|%)$/.

Assuming your theme is based on the default, start editing index.template.php. Then, under the linked stylesheets you can put the override:

	// Custom user font-size
	if ($context['user']['is_logged']) {
		global $user_profile;
		loadMemberData($context['user']['id']);
		
		if (!empty($user_profile[$context['user']['id']]['options']['cust_fontsi'])) {
			echo '
	<style>', 'html {font-size: ', $user_profile[$context['user']['id']]['options']['cust_fontsi'], '}', '</style>';
		}
	}

To find out exactly what kind of useful values you can obtain from e.g. $context and $user_profile, you can use var_dump($user_profile).

Comments

Textarea Backup Localstorage v1.21

I figured I’d drop a note that I updated my Textarea Backup UserJS last month. What follows is the description from ExtendOpera.


Textarea Backup Localstorage

Retains what you type into textareas and contentEditable elements.

This script is only compatible with Opera 10.50 and up. If you need to use it with an older version use Textarea Backup but be advised that it comes with some disadvantages.

  1. Can automatically place previously typed text in textareas.
  2. Can add an unobtrusive menu in the top right corner of textareas

Actions menu screenshot (note the transparent object in the top right corner of the textarea):

Development can be followed on GitHub. Don’t be shy, open an issue or send me a pull request if you think you have something to contribute! 😉

Changelog

1.21 July 25, 2013. Sorry, I was a bit hasty about that last one. I passed my testcase, but only noticed that many contentEditables work differently in practice on this very site.

  • Fixed a bug that occurred when BODY was contentEditable, as is typical in iframes.
  • Properly compare initial value of contentEditable element to backed up value so you’ll only get prompted to overwrite when relevant.
  • Full changelog.

1.20 July 25, 2013. I wasn’t going to make yet another release in three days, but these new features could be implemented much faster than I initially predicted.

  • Trustworthy old persistent preferences support added. I also uploaded a sample settings file.
  • Fixed the keep_after_submission bug, so setting it to false is safe again.
  • Removed the form requirement.
  • Support contentEditable. This is a pretty big one, seeing how it was the most obvious missing feature.

1.11 July 24, 2013. Added configuration switches for the new feature.

  • For Opera 11.6x and Opera 12.x it now defaults to off because of potential performance concerns.

1.10 July 23, 2013. Added support for dynamically added textareas.

  • This new feature will only work in Opera 11.6x and up.

Comments

parseIntHelper()

The results of parseInt() are somewhat inconsistent, so I wrote this to clean it up (also see there). What to do with it? Who knows; it might come in handy someday.

function parseIntHelper(numberString, radix) {
	numberString = (typeof numberString === 'number') ? numberString.toString() : numberString; // I guess we should support number input just like parseInt.
	radix = (typeof radix === 'undefined') ? 10 : radix; // Set radix to 10 if undefined.
	
	numberString = numberString.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim. Taken from http://blog.stevenlevithan.com/archives/faster-trim-javascript
	
	// Check if octal; non-standard but in line with other languages
	if ( numberString.indexOf('0o') === 0 || numberString.indexOf('0O') === 0 ) {
		numberString = numberString.slice(2); // Remove '0o'.
		radix = 8;
	}
	
	// Check if hex
	if ( numberString.indexOf('0x') === 0 || numberString.indexOf('0X') === 0 ) {
		numberString = numberString.slice(2); // Remove '0x'.
		radix = 16;
	}
	
	for (var i=0; i<numberString.length; i++) {
		var nStr = numberString[i];
		if (nStr === '.') {
			continue; // Ignore decimal mark, or rather the radix point. Maybe a check against more than 1 of these?
		}
		var parsed = parseInt(nStr, radix);
		if (isNaN(parsed)) {
			return parsed; // Return NaN if found
		}
	}
	return parseInt(numberString, radix); // Return regular parseInt if there was no shenanigans. Note that we do not support octal numbers prefixed with 0 because it's not in the ECMAScript 5 spec, although we do support the equally non-standard prefixed with 0o. That's because the behavior prefixed with 0 is just too unreliable, while we control what happens with 0o.
}

Comments

Using WordPress Excerpts in Meta Description

For a long time I’ve been aware of the fact that few, if any, WordPress themes seemed to do anything with the META element’s description feature. I never bothered to look into a solution, especially since I never used to add excerpts to my posts half a decade ago. However, I’ve bothered to do so ever since I started notifying people about updates on Twitter. It already makes the search results much more readable if you’re looking for something in the archives of this site, and I figured I should do the same for search engines.

Some uneventful searching later I found what I was looking for, but it definitely wasn’t right for me: I’ve got a huge volume of posts without any excerpts, so printing empty descriptions no matter what would be silly at best, and besides there are more descriptions out there than merely those of posts. After all, categories and even my site itself have a description as well. The comment by Matthew Slyman was much more to my liking, which I then customized as follows:

<?php
$desc;
if ( is_single() ) {
	$desc = get_the_excerpt();
}
elseif ( is_page() ) {
	$desc = get_the_excerpt();
}
elseif ( is_category() ) {
	$desc = category_description();
}
elseif ( is_home() ) {
	$desc = get_bloginfo('description');
}
$desc = htmlspecialchars(trim(strip_tags($desc)));
if (!empty($desc)) {
	echo '<meta name="description" content="';
	echo $desc;
	echo '"/>';
}
?>

Add the whole thing anywhere in your HEAD element in header.php. If excerpts seem to be missing from pages, there’s a simple solution. If you want to reuse the code above for some reason, wrap some sort of function around it and stick it in functions.php. Enjoy.

Comments

Word Count

Since I wanted to know the actual number of words in a paper in near-MLA format and couldn’t find my previous (simple) PHP script, I reimplemented an equally simplistic word counter in Javascript. It strips out citations between parentheses. Suggestions welcome and use at your own risk.

Comments

Older Entries »