Category Archives: Software Development

This category contains all my posts about software development. Tutorials, solutions to problems I’ve met, interesting things I’ve stumbled over, code samples, stuff I don’t want to forget, et cetera.

jQuery: How to extract a tag from an HTML response

Making a website, and using ajax for some things. Sometimes things fail and return custom error pages. I made them to be helpful, but since you can only see them in the browser developer console, they were a bit of a hassle to look at.

To see what the error was much easier, I figured I could just parse the returned HTML, extract the message I knew was there, and insert it into the page that way.

And you’d think the following would work fine:

$(document).ajaxError(function(event, jqxhr, settings, error)
{
    // Find the message in the response HTML
    var m = $(jqxhr.responseText)
        .find('#message');

    // Except .find() doesn't find anything

    // And we replace our DOM with nothing
    $('#content')
        .replaceWith(m);
});

But… No… Apparently, since the response was a complete HTML page, i.e. including html, head and body tags, jQuery was getting a bit tricked up when trying to parse it. Actually not sure if it’s jQuery or native browser parsing behind that’s causing it, but where there’s a will, there’s a way:

$(document).ajaxError(function(event, jqxhr, settings, error)
{
    // Find the inner HTML of the body tag
    var body = /<body.*>([\s\S]+)<\/body>/
        .exec(jqxhr.responseText);

    // Parse the HTML
    body = $.parseHTML(body[1])

    // Append the HTML to a non-special root tag
    body = $('<output>').append(body);

    // And *now* we can finally find our message
    var message = body.find('#message');

    // And add it to our DOM
    $('#content')
        .replaceWith(m);
});

¯\_(ツ)_/¯

PHP: preg_match_all_callback

There are several PCRE functions available, but today I looked for one that just wasn’t there: preg_match_all_callback().

Could’ve maybe used preg_replace_callback(), but felt wrong since I didn’t actually want to do any replacing. I just needed my function to be called for each match.

So I wrote it myself. Noting it here, in case I (or someone else) needs it again.

<?php
/**
 * Perform a global regular expression match
 * and calls the callback for each match.
 */

function preg_match_all_callback(
        string $pattern,
        string $subject,
        callable $callback)
{
    $r = preg_match_all($pattern, $subject, $matches, PREG_SET_ORDER);
    foreach($matches ?? [] as $match)
        $callback($match);
    return $r;
}

And, in case someone reads this post and knows it actually does exist… and if that someone is you, please do leave a comment!

And, yes, I could’ve just written those 3 lines where I needed them, but what’s the fun in that? And besides, the shorter the code where it counts, the easier what counts is to read.

Usage

preg_match_all_callback('/(\w)\w*/', 'Hello World', 'var_dump');
array (size=2)
  0 => string 'Hello' (length=5)
  1 => string 'H' (length=1)

array (size=2)
  0 => string 'World' (length=5)
  1 => string 'W' (length=1)

PHP: Get headers with actual HEAD request

PHP has a function called get_headers which, as you’d guess, gives you the headers returned from an HTTP request. However it actually uses a GET, rather than HEAD, request.

Figured out you can change this by setting a stream context, so wrapped it in a function. And posting it here in case I need it again.

Also added a cleanup of the returned array, as I found it a bit ugly when the request included redirects. See difference below code.

Note: I silence the get_headers call because it throws several warnings, e.g. if the hostname fails lookup, and I’m not really interested in why it fails.

function get_head(string $url, array $opts = [])
{
    // Store previous default context
    $prev = stream_context_get_options(stream_context_get_default());

    // Set new one with head and a small timeout
    stream_context_set_default(['http' => $opts +
        [
            'method' => 'HEAD',
            'timeout' => 2,
        ]]);

    // Do the head request
    $req = @get_headers($url, true);
    if( ! $req)
        return false;

    // Make more sane response
    foreach($req as $h => $v)
    {
        if(is_int($h))
            $headers[$h]['Status'] = $v;
        else
        {
            if(is_string($v))
                $headers[0][$h] = $v;
            else
                foreach($v as $x => $y)
                    $headers[$x][$h] = $y;
        }

    }

    // Restore previous default context and return
    stream_context_set_default($prev);
    return $headers;
}

Example response:

<?php get_head('http://geekality.net');

array (size=2)
  0 =>
    array (size=8)
      'Status' => string 'HTTP/1.1 301 Moved Permanently' (length=30)
      'Date' => string 'Mon, 06 Feb 2017 01:20:48 GMT' (length=29)
      'Server' => string 'Apache' (length=6)
      'Location' => string 'http://www.geekality.net/' (length=25)
      'Vary' => string 'Accept-Encoding' (length=15)
      'Connection' => string 'close' (length=5)
      'Content-Type' => string 'text/html; charset=iso-8859-1' (length=29)
      'Link' => string '<http://www.geekality.net/wp-json/>; rel="https://api.w.org/"' (length=61)
  1 =>
    array (size=6)
      'Date' => string 'Mon, 06 Feb 2017 01:20:48 GMT' (length=29)
      'Server' => string 'Apache' (length=6)
      'Vary' => string 'Accept-Encoding' (length=15)
      'Connection' => string 'close' (length=5)
      'Content-Type' => string 'text/html; charset=UTF-8' (length=24)
      'Status' => string 'HTTP/1.1 200 OK' (length=15)

Example response without my cleanup:

<?php get_head('http://geekality.net');

array (size=9)
  0 => string 'HTTP/1.1 301 Moved Permanently' (length=30)
  'Date' =>
    array (size=2)
      0 => string 'Mon, 06 Feb 2017 01:14:00 GMT' (length=29)
      1 => string 'Mon, 06 Feb 2017 01:14:01 GMT' (length=29)
  'Server' =>
    array (size=2)
      0 => string 'Apache' (length=6)
      1 => string 'Apache' (length=6)
  'Location' => string 'http://www.geekality.net/' (length=25)
  'Vary' =>
    array (size=2)
      0 => string 'Accept-Encoding' (length=15)
      1 => string 'Accept-Encoding' (length=15)
  'Connection' =>
    array (size=2)
      0 => string 'close' (length=5)
      1 => string 'close' (length=5)
  'Content-Type' =>
    array (size=2)
      0 => string 'text/html; charset=iso-8859-1' (length=29)
      1 => string 'text/html; charset=UTF-8' (length=24)
  1 => string 'HTTP/1.1 200 OK' (length=15)
  'Link' => string '<http://www.geekality.net/wp-json/>; rel="https://api.w.org/"' (length=61)

PHP: Convert RGB to hex and back

Just another note to self, in case I need it again:

/**
 * #rrggbb or #rgb to [r, g, b]
 */

function hex2rgb(string $hex): array
{
    $hex = ltrim($hex, '#');

    if(strlen($hex) == 3)
        return [
            hexdec($hex[0].$hex[0]),
            hexdec($hex[1].$hex[1]),
            hexdec($hex[2].$hex[2]),
        ];
    else
        return [
            hexdec($hex[0].$hex[1]),
            hexdec($hex[2].$hex[3]),
            hexdec($hex[4].$hex[5]),
        ];
}


/**
 * [r, g, b] to #rrggbb
 */

function rgb2hex(array $rgb): string
{
    return '#'
        . sprintf('%02x', $rgb[0])
        . sprintf('%02x', $rgb[1])
        . sprintf('%02x', $rgb[2]);
}

Enabling query logging on MariaDB/MySQL

When developing it can sometimes be very useful to see exactly what queries are sent to the database.

Found out there’s something called a General Query Log, and enabling it was really easy. Just add the following to your my.{ini,cnf} file:

[mysqld]
general-log=1
general-log-file=queries.log
log-output=file

Restart the server and you should now find the log in your data directory (unless no queries done yet).

If you don’t know where your data directory is, just run this query:

SHOW VARIABLES WHERE variable_name = 'datadir'

Excel: Function for nslookup in worksheet

Just a copy of a function I found in a blog post for doing nslookup calls in a worksheet, in case I need it again in the future. I’ve changed the return value for failing lookups to a blank string, “”, rather than “NotFound”, but otherwise it’s unchanged.

Usage, given an IP in cell A1, to get the hostname: =NSLookup(A1; 2)

Public Function NSLookup(lookupVal As String, Optional addressOpt As Integer) As String
   Const ADDRESS_LOOKUP = 1
   Const NAME_LOOKUP = 2
   Const AUTO_DETECT = 0
   
   'Skip everything if the field is blank
  If lookupVal <> "" Then
        Dim oFSO As Object, oShell As Object, oTempFile As Object
        Dim sLine As String, sFilename As String
        Dim intFound As Integer
        Set oFSO = CreateObject("Scripting.FileSystemObject")
        Set oShell = CreateObject("Wscript.Shell")
       
        'Handle the addresOpt operand
       'Regular Expressions are used to complete a substring match for an IP Address
       'If an IP Address is found, a DNS Name Lookup will be forced
       If addressOpt = AUTO_DETECT Then
            ipLookup = FindIP(lookupVal)
            If ipLookup = "" Then
                addressOpt = ADDRESS_LOOKUP
            Else
                addressOpt = NAME_LOOKUP
                lookupVal = ipLookup
            End If
        'Do a regular expression substring match for an IP Address
       ElseIf addressOpt = NAME_LOOKUP Then
            lookupVal = FindIP(lookupVal)
        End If
       
        'Run the nslookup command
       sFilename = oFSO.GetTempName
        oShell.Run "cmd /c nslookup " & lookupVal & " > " & sFilename, 0, True
        Set oTempFile = oFSO.OpenTextFile(sFilename, 1)
        Do While oTempFile.AtEndOfStream <> True
            sLine = oTempFile.Readline
            cmdStr = cmdStr & Trim(sLine) & vbCrLf
        Loop
        oTempFile.Close
        oFSO.DeleteFile (sFilename)
       
        'Process the result
       intFound = InStr(1, cmdStr, "Name:", vbTextCompare)
        If intFound = 0 Then
            NSLookup = ""
            Exit Function
        ElseIf intFound > 0 Then
            'TODO: Cleanup with RegEx
           If addressOpt = ADDRESS_LOOKUP Then
                loc1 = InStr(intFound, cmdStr, "Address:", vbTextCompare) + InStr(intFound, cmdStr, "Addresses:", vbTextCompare)
                loc2 = InStr(loc1, cmdStr, vbCrLf, vbTextCompare)
                nameStr = Trim(Mid(cmdStr, loc1 + 8, loc2 - loc1 - 8))
            ElseIf addressOpt = NAME_LOOKUP Then
                loc1 = InStr(intFound, cmdStr, "Name:", vbTextCompare)
                loc2 = InStr(loc1, cmdStr, vbCrLf, vbTextCompare)
                nameStr = Trim(Mid(cmdStr, loc1 + 5, loc2 - loc1 - 5))
            End If
        End If
        NSLookup = nameStr
    Else
        NSLookup = "N/A"
    End If
End Function

Function FindIP(strTest As String) As String
    Dim RegEx As Object
    Dim valid As Boolean
    Dim Matches As Object
    Dim i As Integer
    Set RegEx = CreateObject("VBScript.RegExp")
   
    RegEx.Pattern = "\b(?:\d{1,3}\.){3}\d{1,3}\b"
    valid = RegEx.test(strTest)
    If valid Then
        Set Matches = RegEx.Execute(strTest)
        FindIP = Matches(0)
    Else
        FindIP = ""
    End If
End Function

Source: jayteknews.blogspot.no