Quoted-Printable PSA

Doing "view source" on a MIME message in 2024 be like:

Please use this instead of PHP's egregious quoted_printable_encode:

My future un-clawed-out eyes thank you in advance.

Previously, previously, previously.

Tags: , , , ,

CSS tricks

Dear Lazyweb, what's the done thing here?

I have a set of images of various sizes. I know their aspect ratios ahead of time. I want to lay them out horizontally such that they have the same height, and allocate horizontal space to them as needed to preserve their aspect ratios. That part's easy. Here's where it goes wrong: I would also like each of these images to have a uniform border, and also a uniform horizontal margin between each image.

Here's what I have now, from tonight's DNA Lounge front page, and you can see how it screws up:

Here's what it looks like if you turn off padding and border (those residual borders are burned into the images.) Things end up the proper height, but without a margin between them, it doesn't look great.

Previously, previously, previously, previously, previously.

Tags: , , , ,

Gmail's absolutely horrific markup and how Chrome makes it worse

Wondering why sometimes people paste stuff into your forms and it comes out double- or triple-spaced? Yeah, me too! Check this shit out. Type this text into TextEdit or Notes:

Line One

Line Three

Line Five


Line Eight
Line Nine
Line Ten

Copy that text from TextEdit or Notes. Compose a Gmail message inside desktop Chrome, paste, hit Send, and look at the HTML that Gmail produced. It's insane:

<div dir="ltr">
<p class="gmail-p1" style="margin:0px;font:12px Helvetica">Line One</p>
<p class="gmail-p2" style="margin:0px;font:12px Helvetica;min-height:14px"><br></p>
<p class="gmail-p1" style="margin:0px;font:12px Helvetica">Line Three</p>
<p class="gmail-p2" style="margin:0px;font:12px Helvetica;min-height:14px"><br></p>
<p class="gmail-p1" style="margin:0px;font:12px Helvetica">Line Five</p>
<p class="gmail-p2" style="margin:0px;font:12px Helvetica;min-height:14px"><br></p>
<p class="gmail-p2" style="margin:0px;font:12px Helvetica;min-height:14px"><br></p>
<p class="gmail-p1" style="margin:0px;font:12px Helvetica">Line Eight</p>
<p class="gmail-p1" style="margin:0px;font:12px Helvetica">Line Nine</p>
<p class="gmail-p1" style="margin:0px;font-variant-numeric:normal;font-variant-east-asian:normal;font-variant-alternates:normal;font-size-adjust:none;font-kerning:auto;font-feature-settings:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:Helvetica"></p>
<p class="gmail-p1" style="margin:0px;font-variant-numeric:normal;font-variant-east-asian:normal;font-variant-alternates:normal;font-size-adjust:none;font-kerning:auto;font-feature-settings:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:Helvetica">Line Ten</p>
</div>

So, first things first, if your application produces markup like that, please leave the software industry immediately. Your work is a net negative on everyone's lives. Tools down, walk away.

Second, here's where it gets fun. Copy that text from the Gmail message and paste it into any plain-text area, such as a <TEXTAREA> in a web page, or into TextEdit after having done Format / Make Plain Text. Or even into Emacs.

If you copy from Safari, what gets pasted is what you see.

If you copy from Chrome, what gets pasted is double- or triple-spaced. "Line Ten" is on line 19.

What Chrome copies:

Line One



Line Three



Line Five





Line Eight

Line Nine

Line Ten
What Firefox copies:

Line One


Line Three


Line Five



Line Eight

Line Nine

Line Ten
What Safari copies:

Line One

Line Three

Line Five


Line Eight
Line Nine
Line Ten

Ok but what's actually going on here? When you strip out all the redundant and useless bullshit, what does that markup actually say?

It says that each line is a paragraph, but one with no margin. In other words, a <DIV>. And then every paragraph break is actually its own paragraph, with one <BR> inside it.

I guess what's going on is, Safari looks at a node with no margin and says, "Aha, I shall copy the text with no extra spacing."

But Chrome looks at it and says, "That's a <P> tag! I know those! Those have a blank line after them!" And then it looks at that <P> with the single <BR> in it says, "That's also a <P> tag! I know those! Those have a blank line after them! I am very smart."

I see that monopolistic integration between the Gmail team and the Chrome team has really led to excellent cross-product interoperability, and consistent interpretation of how all of this crap is supposed to work.

(Firefox, meanwhile, somehow manages to split the difference, and I have no theory on what the hell is going on there.)

Anyway, great job everybody, no notes.

Previously, previously, previously, previously, previously.

Tags: , , , , ,

Rumors of Threads dot net federation have been greatly exaggerated

I used Instagram mainly for following bands, to find out when they had new releases or were going on tour. I'd sure like to still have a feed full of that sort of thing. So, since you can follow (some) Threads accounts from Mastodon now, I got to wondering how many of those folks had moved from one Zuckerberg property to another.

Of the 655 accounts I used to follow on Instagram, 268 (41%) now have Threads accounts. (And I am disappointed in all of them.) Of those 268, exactly ZERO can be followed from Mastodon dot social. If there's something they have to turn on to make it happen, zero of them have done so.

Not even @aoc or @therealelvira!

And yes, I pasted each of the account names into the search field by hand, like an animal.

Previously, previously, previously, previously.

Tags: , , , ,
Current Music: Kite -- It's Ours ♬

Gravatar Dunning-Krugerrands

Automattic's self-immolation continues apace. They are about to find out that pivoting from "here's some mildly-convenient free image hosting to put an avatar on your email address" to "Enterprise-ready, planet-incinerating cryptocurrency grift" is not going to go the way that they think it will.

Today might be a good day to install my "Mirror Gravatar" WordPress plugin, before the mass exodus.

Gravatar blog: "Mastering Decentralized Identity for Business":

The internet is evolving into Web 3.0, with a shift that brings decentralized applications and services to the forefront. [...]

Blockchain technology is fundamental to decentralized identity systems. Here's why: [...]

For instance, Ethereum's smart contract functionality lets you create complex identity management systems that operate autonomously and transparently.

Previously, previously, previously, previously, previously, previously, previously, previously, previously.

Tags: , , , , , ,

To Blog or to Social-Post

Pondering out loud:

Sometimes I post things to Ye Olde Blogge, and sometimes I post them on Mastodon only. How do I decide which? It has been somewhat random, but if I examine my decisions, I think what I have been doing is:

  • Linking to a long funny article, or to Actual Artwork: Blog.

  • Someone's good shitpost: Blog if it is a banger for the ages; Mastodon boost if it is an ephemeral Sensible Chuckle.

  • Actual News, that will still be interesting next year: Blog.

  • Breaking Bullshit News, but funny, but that won't matter tomorrow: I often just boost someone else's Mastodon post, which does not show up on the blog. (E.g. "Republican says something dumb"). Unless it is, like, such a self-own that it amounts to a Quality Shitpost, then I'll re-blog it.

    One reason for boosting others' posts is that I do not see the replies and sometimes that is for the best.

  • My own "Sensible Chuckle" shitpost: sometimes I do these on the blog, and sometimes Mastodon only. The problem with the Mastodon ones is that sometimes I misjudge the popularity of these, and my throwaway one-liner gets a dozen angry replies from the Mastodon HOA or the anarcho-syndicalists, and my blog readers might enjoy skimming that pile-on.

    Plus there's the whole "the blog functions as an archive" aspect of things.

So, I dunno. Maybe I should fall into a pattern of: never make top-level Mastodon posts, only post to the blog and mirror that to Mastodon.

Here's a good "where should this have gone" example.

"Always blog" is in the spirit of POSSE, but in my case there's not a lot of "E = everywhere" in that these days -- pretty much just Mastodon -- as I no longer use Facebook or Twitter. (I do still auto-post a screenshot-breadcrumb to Instagram, because I have a few friends who seem to use that the way I use a feed reader.)

Previously, previously, previously, previously, previously.

Tags: , ,

"Modern web technologies"

Basecamp's web site on Safari 13.1.2, macOS 10.13.6:

Are you curious about what those "modern web technologies" are? As far as I can tell, the technology in question is a recent innovation known as "drawing your checkboxes in the right place", which as we know, only became possible within the last 6 years.

Great job everybody.

Previously, previously, previously, previously, previously, previously, previously.

Tags: , , , , ,

Here is your demo version of Energize for Solaris

I got an email yesterday asking if I had a working copy of the Lucid Energize Programming System for HPUX. I do not! But I dug around and I found a demo version of it for Solaris, as well as a few other ancient CDROMs. Let me know if you get any of these to run!


Previously, previously, previously, previously, previously, previously, previously, previously, previously, previously, previously.

Tags: , , , , , , ,

Blocking bogus URL parameters

Or, "Thanks, but I reject your cache-buster or tracking token."

I decided to change my various pages to reject unknown URL search parameters. URLs mean things, and ideally only one URL will refer to a given document. And mangling URLs instead of respecting the Expires header is antisocial behavior.

There are lots of badly-behaved feed readers out there. You may be about to find out that yours is one of them. Here are a few terrible things that feed readers do:

  • Appending random numbers to the URL of the feed, or of URLs within the feed, to intentionally break cacheing.

  • Interpreting "301 Moved Permanently" not as "update your URL" but instead as "keep using the old URL and then processing that redirect every time until the sun goes dark".

  • Interpreting both "404 Not Found" and "410 Gone" as "I should keep loading this URL once an hour until the sun goes dark, just in case it comes back."

  • And, hoooooboy, a whole lot of other crap that rachelbythebay has been documenting.

Anyway, here's how I did it, if you want to play along. (I decided to allow utm_source and fbclid for now, since they are just absolutely ubiquitous.)

Apache: reject query parameters on HTML documents:

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Reject any HTML or image document that has unknown query parameters:
  RewriteCond %{REQUEST_URI}  /$|\.(html|jpg|gif|png|pl)$
# RewriteCond %{QUERY_STRING} !^$
  RewriteCond %{QUERY_STRING} !^$|^utm_|^fbclid=
  RewriteRule .? - [QSD,E=BADQUERY:1]

  # If it's a bogus query, page is forbidden.
  RewriteCond %{ENV:BADQUERY} =1
  RewriteCond %{REQUEST_FILENAME} -f
  RewriteRule .? - [FORBIDDEN,LAST]

  # If this is the 403 page for a bad query, serve a different error document.
  RewriteCond %{ENV:BADQUERY} =1 [OR]
  RewriteCond %{ENV:REDIRECT_BADQUERY} =1
  RewriteRule ^/?(error/)?4\d\d\.html$ /403q.html [LAST]
</IfModule>

WordPress: reject unknown query parameters:

// Remove some unnecessary query parameters that are allowed by default.
//
add_action ('parse_request', 'jwz_remove_query_vars');
function jwz_remove_query_vars ($query) {

  // If there are no query parameters in the URL, no need for changes.
  if (! preg_match ('/\?/', $_SERVER['REQUEST_URI']))
    return;

  // Omit all built-in WordPress parameters except these:
  //
  $query->public_query_vars =
    array_intersect ($query->public_query_vars,
      [ 'p',                    // Shows up when I first make a post
        's',                    // Searches
        'doing_wp_cron',        // I guess?
        'feed',                 // feed/?feed=rss
      ]);

  // Add these non-WordPress parameters.
  //
  $query->public_query_vars =
    array_unique (array_merge ($query->public_query_vars,
      [ 'replytocom',           // Used in reply notification emails
        'unapproved',           // PERMALINK/?unapproved=N&moderation-hash=X
        'moderation-hash',      //  for just-posted comments being moderated.
        'preview',              // Previewing post edits
        'preview_id',
        'preview_nonce',
	'trashed',		// After deletion by moderator

        'utm_source',           // Bullshit
        'utm_content',
        'utm_medium',
        'utm_campaign',
        'fbclid',               // Facebook bullshit
      ]));

  // If the page does have a legitimate query parameter on it, and it was
  // the result of a rewrite rule, I guess we need to allow those back in?
  // Without this, url_to_postid() fails to parse URLs like "YYYY/MM/SLUG"
  //
  if ($query->matched_query) {
    parse_str ($query->matched_query, $a);
    $query->public_query_vars =
      array_unique (array_merge ($query->public_query_vars,
                                 array_keys ($a)));
  }

}


// Error if there are any unknown query parameters.
//
add_action ('parse_request', 'jwz_disallow_extra_queries');
function jwz_disallow_extra_queries ($query) {
  foreach ($_GET as $k => $v) {
    if (! in_array ($k, $query->public_query_vars)) {
      $k = htmlspecialchars (strip_tags ($k));
      $e = new WP_Error ("unknown_parameter", "unknown parameter \"$k\"");
      wp_die ($e, '',
              [ 'response' => 403,
                'query' => $query ]);
    }
  }
}

More WordPress: serve feed readers an error message in a feed, instead of in an HTTP response, since most of them ignore those:

// Style the "wp_die" page
//
add_filter ('wp_die_handler', function ($r) { return 'jwz_die_handler'; });
function jwz_die_handler ($message, $title = '', $args = []) {

  [ $message, $title, $args ] =
    // Converts a WP_Error object to a string, etc.
    // $message might contain HTML and entities.
    _wp_die_process_input ($message, $title, $args);

  if (!$message || !is_string ($message))
    $message = "Unknown error";

  if (! empty ($args['additional_errors'])) {
    $message = array_merge ([ $message ],
                            wp_list_pluck ($args['additional_errors'],
                                           'message'));
    $message = ("<UL STYLE='text-align: left; display: inline-block;'><LI>" .
                implode ("</LI><LI>", $message) .
                "</LI></UL>");
  }

  $status = $args['response'] ?? 500;

  $query = $args['query'] ?? null;
  $feed_p = ($query && ($query->query_vars['feed'] ?? false));

  // If we get an error serving a feed (e.g., "unknown parameter" for some
  // feed reader trying to do cache-busting) serve them an RSS feed with
  // an error message in it.
  //
  if ($feed_p) {
    $age  = 60 * 60 * 24 * 365 * 10;
    $time = time();
    $now  = gmdate ("D, d M Y H:i:s T", $time);
    $exp  = gmdate ("D, j M Y G:i:s T", $time + $age);
    $tag  = str_replace ('=', '', base64_encode (pack ('N', $time)));
    $url  = get_bloginfo ("url");
    $url2 = ($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] .
             $_SERVER['REQUEST_URI']);
    $name = get_bloginfo ("name");

    $message .= (" <BR><BR>\n\n" .
                 "If you are seeing this, <BR>\n" .
                 "your feed reader is badly behaved. <BR>\n" .
                 "Use a different one.");

    header ("Content-Type: text/xml;charset=UTF-8");
    header ('Expires: ' . $exp);
    header ('Cache-Control: max-age=' . $age);

    print '<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
 <channel>
  <generator>' . wrap_cdata ($name) . '</generator>
  <title>' . wrap_cdata ($name) . '</title>
  <link>' . $url . '</link>
  <description>' . wrap_cdata (get_bloginfo ('description')) . '</description>
  <language>en</language>
  <webMaster>' . wrap_cdata (get_bloginfo ('admin_email') . " ($name)") .
   '</webMaster>
  <pubDate>' . $now . '</pubDate>
  <lastBuildDate>' . $now . '</lastBuildDate>
  <atom:link href="' . $url . '" rel="self" type="application/rss+xml" />
  <item>
   <link>' . $url2 . '</link>
   <guid isPermaLink="false">' . $tag . '</guid>
   <title>' . wrap_cdata ($title) . '</title>
   <description>' . wrap_cdata ($message) . '</description>
  </item>
 </channel>
</rss>
';
    die;
  }


  $body = $message;  // Style your error page here...

  $desc = get_status_header_desc ($status);

  header ("HTTP/1.1 $status $desc");
  header ("Cache-Control: no-cache");
  header ("Pragma: no-cache");
  header ("Expires: " . gmdate('D, d M Y H:i:s T', time()));
  print $body;

  if ($args['exit'])  // Defaulted to true by _wp_die_process_input
    die();
}

function wrap_cdata ($s) {
  $s = html_entity_decode ($s);
  if (preg_match ('/[<>&]/', $s)) {
    $s = str_replace (']]>', ']]]]><![CDATA[>', $s);
    $s = '<![CDATA[' . $s . ']]>';
  }
  return $s;
}

Let me know if I broke something that I didn't mean to break. (There's a good chance I meant to break it.)

Previously, previously, previously, previously.

Tags: , , ,

Mozilla's Original Sin

Some will tell you that Mozilla's worst decision was to accept funding from Google, and that may have been the first domino, but I hold that implementing DRM is what doomed them, as it led to their culture of capitulation. It demonstrated that their decisions were the decisions of a company shipping products, not those of a non-profit devoted to preserving the open web.

Those are different things and are very much in conflict. They picked one. They picked the wrong one.

In light of Mozilla's recent parade of increasingly terrible decisions, there have been cries of "why doesn't someone fork it?" followed by responses of "here are 5 sketchy forks of it that get no development and that nobody uses". And inevitably following that, several people have made comments in the "Mozilla is an advertising company now" thread to the effect that it is now impossible for a non-corporate, open source project to actually implement a web browser, since a full implementation requires implementing DRM systems which you cannot implement without a license that the Content Mafia will not give you.

This is technically true. ("Technically" being the best kind of "true" in some circles.)

Blaming and shaming:

  • It used to be that to watch Netflix (and others) in an open browser required the use of a third party proprietary plugin. That doesn't work any more: now Netflix will only work in a browser that natively implements DRM.

  • That step happened because Mozilla took that license and implented DRM.

  • That happened because: "it's in the W3C spec, we didn't have a choice."

  • How did it get into the spec? Oh, it got into the spec because when the Content Mafia pressured W3C to include it, Mozilla caved. At the end of the day they said, "We approve of this and will implement it". Their mission -- their DUTY -- was to pound their shoe on the god damned table and say: "We do not approve, and will not implement if approved."

    But they went and did it just the same.

"But muh market shares!" See, now we're back to the kitten-meat deli again.

(BTW, how's that market share looking these days? Adding DRM really helped you juice those numbers, did it? Nice hockey-stick growth you got there? Good, good.)

If you were unable to watch Netflix in Mozilla out of the box, yes, that would have impacted their market share. You know what else would have happened? Some third party patch would have solved that problem.

When Netscape released the first version of the Mozilla source with no cryptography in it due to US export restrictions, it was approximately 30 minutes before someone outside the US had patched it back in. I'm not exaggerating, it happened that night. This is the sort of software activism at which the open source community excels, even if it is "technically" illegal. ("Technically", again, being the best kind of illegal in some circles.)

Mozilla had a duty to preserve the open web.

Instead they cosplayed as a startup, chasing product dreams of "growth hacking", with Google's ad money as their stand-in for a VC-funding firehose, with absolutely predictable and tragic results.

And those dreams of growth and market penetration failed catastrophically anyway.

(Except for the C-suite, who made out quite well. And Google, who got exactly what they paid for: a decade of antitrust-prosecution insurance. It was never about ad revenue. The on-paper existence of Firefox as a hypothetical competitor kept the Federal wolves at bay, and that's all Google cared about.)


Now hear me out, but What If...? browser development was in the hands of some kind of nonprofit organization?

As I have said many times:

In my humble but correct opinion, Mozilla should be doing two things and two things only:

  1. Building THE reference implementation web browser, and
  2. Being a jugular-snapping attack dog on standards committees.
  3. There is no 3.

Previously, previously, previously, previously, previously, previously, previously, previously, previously, previously.

Tags: , , , , , , , , , ,

  • Previously