Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

Dumping an Invision Community database from PHP: ANSI_QUOTES, multi-member gzip, and the 0x trap

Writing a database dump in pure PHP is not hard. Writing one that actually restores is, because every way it fails is silent. These are the ones that cost real time on Invision Community specifically.

Why pure PHP at all

Invision Community's own Configuration Error notification lists exec, system, passthru, pcntl_exec, popen, proc_open and shell_exec as dangerous and tells administrators to disable them. Take that advice — as you should — and mysqldump becomes unreachable. Any backup approach that shells out is broken on exactly the servers that followed the security guidance.

1. The suite connects with ANSI_QUOTES

IPS\Db issues this on every connection:

SET sql_mode='STRICT_ALL_TABLES,ONLY_FULL_GROUP_BY,ANSI_QUOTES'

Two consequences, both fatal and neither obvious.

SHOW CREATE TABLE returns double-quoted identifiers.

CREATE TABLE "core_members" (
  "member_id" int unsigned NOT NULL AUTO_INCREMENT,
  ...

That only parses in a session that is also in ANSI_QUOTES. Restore it from any ordinary MySQL client — or from a standalone script that does not boot the suite — and every single CREATE TABLE dies with a syntax error pointing at the table name. Drop the mode for that one statement and put it straight back:

$previous = $db->query( 'SELECT @@SESSION.sql_mode AS m' )->fetch_assoc()['m'];
$db->query( "SET SESSION sql_mode = '" . str_replace( ',ANSI_QUOTES', '', $previous ) . "'" );
$create = $db->query( "SHOW CREATE TABLE `{$table}`" );
$db->query( "SET SESSION sql_mode = '{$previous}'" );

Double-quoted string literals stop being strings. This is a perfectly ordinary statement that fails under ANSI_QUOTES:

SHOW FULL TABLES WHERE Table_type = "BASE TABLE"
-- ERROR: Unknown column 'BASE TABLE' in 'where clause'

MySQL read it as an identifier. Use single quotes for every literal in code that runs against this connection.

2. Appending to a gzip file makes it multi-member, and gzdecode() only reads the first part

A chunked dump opens the archive, appends, and closes it once per chunk. That is a multi-member gzip stream: several concatenated deflate members in one file. It is completely valid, and gunzip, zcat and PHP's gzopen all handle it.

gzdecode() does not. It returns the first member and reports no error:

gzopen loop read : 26526 bytes
gzdecode read    :   254 bytes

Nothing throws. Nothing warns. You get a backup containing its first few hundred rows and no indication anything is wrong until you need it. Always read these with a loop:

$out = '';
$h = gzopen( $path, 'rb' );
while ( !gzeof( $h ) ) { $out .= gzread( $h, 262144 ); }
gzclose( $h );

And write a footer marker as the last thing in the archive. Its presence on read-back is the only cheap proof the stream was not truncated.

3. An empty binary value becomes a bare 0x

Binary columns have to be hex-encoded — a BLOB can hold a NUL byte or an invalid UTF-8 sequence, and escaping it stores something subtly different. But:

'0x' . bin2hex( '' )   // "0x"
-- ERROR: Unknown column '0x' in 'field list'

MySQL reads a bare 0x as an identifier. Every empty BLOB in the database fails to restore. Special-case it:

if ( $value === '' ) { return "''"; }
return '0x' . bin2hex( $value );

Detect binary columns by SHOW FULL COLUMNS where Collation is NULL — more reliable than matching type names, since VARBINARY and BLOB report differently but both have no character set.

4. Statement splitting: strip leading comments, do not skip the chunk

If the dump writes a banner before each table:

--
-- core_members
--

DROP TABLE IF EXISTS `core_members`;
CREATE TABLE `core_members` ( ... );

then splitting the archive on a statement terminator produces a chunk that starts with comment lines and ends with the DROP. The obvious guard —

if ( str_starts_with( $sql, '--' ) ) { continue; }   // WRONG

— throws away every DROP. Every CREATE then fails with "table already exists", and the restore reports hundreds of errors while changing nothing. Strip the comments instead:

$sql = preg_replace( "/^(?:[ \t]*--[^\n]*\n)+/", '', $sql );

5. Two more worth setting

  • SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO' in the dump header. Without it a legitimate 0 in an AUTO_INCREMENT column is replaced by a fresh id on restore, breaking every row that referenced it.
  • SET FOREIGN_KEY_CHECKS=0 around the replay, or table order decides whether the restore succeeds.

Consistency: say which kind of backup it is

A dump that finishes in one request can run inside START TRANSACTION WITH CONSISTENT SNAPSHOT and is a genuine point-in-time copy. A dump split across requests cannot be — no transaction survives the end of a request — so its first table is read minutes before its last, while people are posting. That is the same limitation as mysqldump without --single-transaction, and it is fine, but the resulting archive can contain a reply whose topic is missing. Record which kind each archive is and show it, rather than letting an administrator assume.

And verify by reading, not by remembering

Comparing the dumper's row counter against the dumper's manifest proves nothing — both come from the same variable. Open the finished file, count what is in it, and compare that. Because real_escape_string converts newlines inside values to a literal \n and binary is hex, no value can contain a raw newline, so a tuple is reliably one line and the count is exact rather than a guess.



User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.