MediaWiki

From Leo's Notes
Last edited on 17 November 2023, at 00:13.

MediaWiki is an opensource wiki engine written in PHP by the Wikimedia Foundation. It is used by both Wikipedia and this site.

Visit the project website at:


Running inside Docker

You can run MediaWiki from a Docker container. A proof of concept can be found at:

In order to make a single MediaWiki container image which can be used regardless of custom skins or extensions, I intentionally separated custom skins and extensions into a separate directory. When the container first starts, a setup script will symlink these additional extensions and skins to the primary directory. Custom extensions and skins need to be placed in the extension and skins volume and then added to the LocalSettings.php configuration file. When adding a new extension or skin, you must restart the container for these changes to take effect.

To get started with my image, create the container with volumes for:

  1. Images / Uploads as /mediawiki/images
  2. Extensions as /extensions
  3. Skins as /skins
  4. The LocalSettings.php configuration file under /config
  5. Database (on a remote server or container), or a SQLite file on a volume

An example docker-compose configuration running a wiki:

wiki:
    image: registry.steamr.com/docker/mediawiki:1.34
    networks:
      - traefik
      - db-net
    expose:
      - "8888"
    environment:
      - VIRTUAL_HOST=wiki.steamr.com
      - APP_ROOT=/mediawiki
      - DOCUMENT_ROOT=/mediawiki
      - DB_HOST=db
      - DB_DATABASE=wiki
      - DB_USERNAME=wiki
      - DB_PASSWORD=wiki
    restart: always
    volumes:
      - /var/volumes/wiki/config:/config
      - /var/volumes/wiki/extensions:/extensions
      - /var/volumes/wiki/skins:/skins
      - /var/volumes/wiki/images:/mediawiki/images

Configuration

MediaWiki only has a single configuration file at LocalSettings.php.

If you wish to run a wiki on the root of a domain, you need to set $wgScriptPath empty like so:

$wgScriptPath = "";
$wgArticlePath = "/$1";

Extensions

Extensions are placed under the /extensions directory. Common extensions are bundled with the base installation of MediaWiki but not enabled by default. Extentions that are bundled can be enabled by adding a wfLoadExtention('extention') call to the LocalSettings.php file.

There are some extensions that this wiki requires:

Intersection
https://github.com/wikimedia/mediawiki-extensions-intersection.git
Generates dynamic lists
Scribunto
https://github.com/wikimedia/mediawiki-extensions-Scribunto.git
Generates scripted outputs using Lua
Math
https://github.com/wikimedia/mediawiki-extensions-Math
Generates math formulas
NativeSvgHandler
https://github.com/wikimedia/mediawiki-extensions-NativeSvgHandler.git
Embeds SVG files as an image for client-side rendering. Requires appending http://www.w3.org/tr/rec-rdf-syntax/ to $validNamespaces in UploadBase.php.
WikiSEO
https://github.com/octfx/wiki-seo/
Enables custom SEO meta keyword and description tags on the wiki.
Score
https://www.mediawiki.org/wiki/Extension:Score
Lets you embed LilyPond music notation into your Wiki

Skins

Skins are placed under the /skins directory. Modern skins are loaded with the wfLoadSkin('skin') call in LocalSettings.php, which reads the skin's skin.json manifest file in the skin's directory. The manifest file contains the skin's name, autoload class files, as well as which resource modules to load loaded, including the stylesheets and javascript files that are part of the skin.

MediaWiki's guide on skinning is relatively up to date albeit a little confusing to understand at first.

The Reader skin used on this wiki:

The OutputPage object is the thing that handles all HTML generation as well as linking javascript and CSS modules. You can use this to inject HTML or certain code to the page with some method calls. See: https://www.mediawiki.org/wiki/Manual:OutputPage.php

Transcluded Pages

There are some pages that are used by the MediaWiki software itself, including:

Some resources are also loaded from pages including:

Skins should be able to handle contents on these MediaWiki pages which are typically shown somewhere on the page. Of course, custom skins can also reference their own set of pages such as the bootstrap/reader skin I'm currently using:

Tasks

Enabling Visual Editor

The Visual Editor has been included out of the box since MediaWiki 1.35. The following steps are no longer required.

Visual Editor is an extension that enables the WYSIWYG editor. This extension requires Parsoid in order to properly save changes.

Installing the Visual Editor extension is simple:

  1. Download the extension to the extensions directory
    $ cd extensions
    $ wget https://extdist.wmflabs.org/dist/extensions/VisualEditor-REL1_34-74116a7.tar.gz
    $ tar -xzf VisualEditor*gz
    
  1. Edit LocalSettings.php and enable the extension.
    wfLoadExtension('VisualEditor');
    $wgDefaultUserOptions['visualeditor-enable'] = 1;
    $wgVirtualRestConfig['modules']['parsoid'] = array(
        // URL to the Parsoid instance
        // Use port 8142 if you use the Debian package
        'url' => 'http://parsoid:8000',
        // Parsoid "domain", see below (optional)
        'domain' => 'wiki',
        # // Parsoid "prefix", see below (optional)
        # 'prefix' => 'localhost'
    );
    

Getting Parsoid running is slightly trickier. I recommend running Parsoid in a docker container to simplify installation. There is one built at thenets/parsoid which works just fine. A clone of that project is available at https://git.steamr.com/docker/parsoid. The container takes in an environment variables to configure which domains Parsoid is to work for. An example docker-compose file with everything working is given below.

version: '3.3'
services:
  wiki:
    image: registry.steamr.com/docker/mediawiki:1.34
    labels:
      - "traefik.enable=true"
      - "traefik.port=8888"
      - "traefik.docker.network=traefik"
      - "traefik.frontend.rule=Host:wiki.steamr.com"
    networks:
      - traefik
      - db-net
    expose:
      - "8888"
    environment:
      - VIRTUAL_HOST=wiki.home.steamr.com
      - APP_ROOT=/mediawiki
      - DOCUMENT_ROOT=/mediawiki
      - DB_HOST=db
      - DB_DATABASE=wiki
      - DB_USERNAME=wiki
      - DB_PASSWORD=wiki
    restart: always
    volumes:
      - /var/volumes/wiki/config:/config
      - /var/volumes/wiki/extensions:/extensions
      - /var/volumes/wiki/skins:/skins
      - /var/volumes/wiki/images:/mediawiki/images

  parsoid:
    image: registry.steamr.com/docker/parsoid:latest
    labels:
      - "traefik.enable=true"
      - "traefik.port=8000"
      - "traefik.docker.network=traefik"
      - "traefik.frontend.rule=Host:parsoid.steamr.com"
    environment:
      - PARSOID_DOMAIN_wiki=http://wiki:8888/api.php
    networks:
      - traefik
    expose:
      - "8000"
    restart: always

The Visual Editor should be functional at this point. If the editor does not start without any visual messages, check the javascript console for error messages. There are certain skin requirements that need to be met in order for the Visual Editor to work outlined at https://www.mediawiki.org/wiki/VisualEditor/Skin_requirements.

Inserting a custom script in <head>

If using a custom skin, use the OutputPage and call addHeadItem('name', '<script>...</script') to inject a custom script block within the document head.

Alternatively, write a specific OutputPageBeforeHTML hook, and from there call addInlineScript.

Adding a custom button to WikiEditor Toolbar

To add a custom button to the WikiEditor toolbar next to the existing bold and italic buttons, edit the MediaWiki:Common.js file. This file can only be changed by interface administrator users. Membership to this group can be assigned at Special:UserRights/username.

The Common.js file used to add the Code and SyntaxHighlight templates used on this wiki is given below.

var customizeToolbar = function () {
	$('#wpTextbox1').wikiEditor('addToToolbar', {
		'section': 'main',
		'group': 'format',
		'tools': {
			'code': {
				label: 'code',
				type: 'button',
				oouiIcon: 'code',
				action: {
					type: 'encapsulate',
					options: {
						pre: "<code>",
						post: "</code>"
					}
				}
			}
		}
	});
	$('#wpTextbox1').wikiEditor('addToToolbar', {
		'section': 'main',
		'group': 'format',
		'tools': {
			'terminal': {
				label: 'highlight',
				type: 'button',
				oouiIcon: 'tag',
				action: {
					type: 'encapsulate',
					options: {
						pre: "{{highlight|lang=terminal|code=\n",
						post: "}}"
					}
				}
			}
		}
	});
};

Documentation for this can be found at:

Remove the 'Retrieved from' footer message

There are a few ways to hide the 'Retrieved from' message that appears at the end of every article.

  1. Edit the MediaWiki:Retrievedfrom page with an Administrator account. Comment out or remove the contents to suppress the message.
  2. Hide the content with CSS by adding to MediaWiki:Common.css the following rule:
    /* hide the "Retrieved from" message */
    .printfooter { display: none; }
    
  3. Edit the template that your skin is using. It'll look something like:
    {{#html-printfooter}}<div class="printfooter">{{{html-printfooter}}}</div>[[:Template:/html-printfooter]]
    
    . Either remove, or comment out the line using mustache Template:! ....

How to promote a user into a sysop, bureaucrat, or interface-admin group

Certain actions on the Wiki are restricted to specific user groups. The default permissions are listed at https://www.mediawiki.org/wiki/Manual:User_rights. To add a user to a specific group:

$ mysql -u $User -p$Password $Database

## In MySQL prompt, determine the user's user_id
MariaDB [wiki]> SELECT user_id, user_name FROM user WHERE user_name = 'leo'
+---------+-------------
| user_id | user_name
|       1 | Leo             
+---------+-------------

## Add the user into the desired group using the user_id above.
MariaDB [wiki]> INSERT INTO `user_groups` VALUES (1, 'sysop'), (1, 'bureaucrat'), (1, 'interface-admin');

Running the Docker container under a different prefix using Traefik

The official Docker image as well as my customized version based off of it expects the Wiki to be served from the domain root. However, if you want to serve the Wiki from a 'subdirectory' like /wiki, you need to do a few things:

  1. In Traefik, your router should have the following rules: traefik.http.routers.wiki-https.rule=(Host(`my-wiki-server.tld`) && PathPrefix(`/wiki`)).
  2. In your Docker container, you need to symlink the /wiki directory to /var/www/html. ln -s /var/www/html /var/www/html/wiki
  3. In your MediaWiki LocalSettings.php configuration, you need to:
    1. Set $wgVirtualRestConfig['modules']['parsoid'] = ['url' => 'http://localhost:8080/wiki/rest.php']; so that Parsoid continues to work.
    2. Set $wgScriptPath = "/wiki";
    3. Set $wgArticlePath = "/wiki/$1";

Running MediaWiki behind a reverse proxy

When running MediaWiki behind a reverse proxy like squid, nginx, or Traefik, edits made by users will appear as originating from reverse proxy server.

To fix this issue, you need to tell MediaWiki which address ranges are from the reverse proxy and to have it use the X-Forwarded-For header instead. Since MediaWiki 1.35, this can be accomplished by adding $wgUsePrivateIPs and $wgCdnServersNoPurge in your LocalSettings.php file:

$wgUsePrivateIPs = true;
$wgUseCdn = true;
$wgCdnServersNoPurge = [];
$wgCdnServersNoPurge[] = "172.18.0.0/16";

Replace 172.18.0.0/16 with the CIDR of your reverse proxy networks (this CIDR is my internal Traefik network for my Docker stack).

Adding the Score extension

The Score extension allows the rendering of music notation using the Lilypond project as the renderer. I ran into a series of hurdles getting this to work for MediaWiki 1.39.

Issues are:

  1. The extention highly recommends the use of ShellBox to isolate Lilypond because Lilypond's insecure and may allow remote execution. Since this Wiki is editable to the public, this has to be done. You'll have to set up a ShellBox instance (as a separate container). MediaWiki/Wikipedia has their own container image which doesn't work outside their infrastructure so I had to make my own.
  2. Lilypond has issues and breaks in my environment. I had to:
    • Modify /usr/bin/lilypond to include export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" otherwise it will fail silently in the extension's script. If you enable verbose mode on lilypond, you will see warning: g_spawn_sync failed (0): gs: Failed to execute child process “gs” (No such file or directory).
    • Symlink the lilypond fonts because on Debian Bullseye, the package versions for Lilypond and Lilypond-fonts are mismatched so the paths are broken (What the heck Debian?)
    • I tried to enable SVG which works after some tweaking to the generator script, but is a full page in size and can't be easily trimmed.
  3. The Score extension's included shell script is questionable...
    • The fuzzy png image apparently is from the ghostscript .ps to .png conversion. I'm not sure why this is even done separately when lilypond also generates the pngs. I hacked this by copying 'file.png' over 'file-page1.png' and commented out the runGhostscript function call in the generatePngAndMidi.sh script. That seems to make the png not shrink down and be fuzzy.

After overcoming all the aforementioned issues, I think I've finally got it working.

  • Use the following Dockerfiles
  • Add the following docker-compose entries
  • In the volume, run:
    $ git clone https://gerrit.wikimedia.org/r/mediawiki/libs/Shellbox shellbox
    
    ## In the fpm container, run as the shellbox user:
    # su shellbox
    $ cd /srv/shellbox
    $ /usr/local/bin/composer install --no-dev
    
  • Enable the plugin:
wfLoadExtension( 'Score' );
$wgScoreTrim = true;
$wgScoreUseSvg = false;
$wgShellboxUrl = 'http://shellbox/shellbox';
$wgShellboxSecretKey = 'secret_key';
  • You have to tweak MediaWiki:Common.css so that the embedded images aren't too big. I made them 4em max-height as I intend to only embed single staff snippets for my notes.

Troubleshooting

Scribunto Lua Failures

If templates cause this error:

Lua error: Internal error: The interpreter exited with status 127.

This likely means that you do not have Lua installed or it is not in the PATH. You will need to specify the Lua path in LocalSettings.php with this line:

$wgScribuntoEngineConf['luastandalone']['luaPath'] = "/usr/bin/lua5.1";

Database Import Incomplete

Database imports from MySQL 5.7.27 to a MariaDB 10.4.7 seems to fail. Imports only appear to complete if the database dump was made without Enclose export in a transaction enabled in PHPMyAdmin but subsequent edits on the destination wiki will result in this error message:

The revision #0 of the page named "some-article" does not exist.

This is usually caused by following an outdated history link to a page that has been deleted. Details can be found in the deletion log.

Solution

It turns out the destination database server (mariadb:10.4.7, in docker) was not set up properly after being upgraded from MariaDB-10.1. On start up, it showed the following error messages:

2019-09-01 21:14:44 0 [Note] Server socket created on IP: '::'.
2019-09-01 21:14:44 0 [Warning] 'user' entry 'root@localhost.localdomain' ignored in --skip-name-resolve mode.
2019-09-01 21:14:44 0 [Warning] 'proxies_priv' entry '@% root@localhost.localdomain' ignored in --skip-name-resolve mode.
2019-09-01 21:14:44 0 [ERROR] Missing system table mysql.roles_mapping; please run mysql_upgrade to create it
2019-09-01 21:14:44 0 [ERROR] Incorrect definition of table mysql.event: expected column 'sql_mode' at position 14 to have type set('REAL_AS_FLOAT','PIPES_AS_CONCAT','ANSI_QUOTES','IGNORE_SPACE','IGNORE_BAD_TABLE_OPTIONS','ONLY_FULL_GROUP_BY','NO_UNSIGNED_SUBTRACTION','NO_DIR_IN_CREATE','POSTGRESQL','ORACLE','MSSQL','DB2','MAXDB','NO_KEY_OPTIONS','NO_TABLE_OPTIONS','NO_FIELD_OPTIONS','MYSQL323','MYSQL40','ANSI','NO_AUTO_VALUE_ON_ZERO','NO_BACKSLASH_ESCAPES','STRICT_TRANS_TABLES','STRICT_ALL_TABLES','NO_ZERO_IN_DATE','NO_ZERO_DATE','INVALID_DATES','ERROR_FOR_DIVISION_BY_ZERO','TRADITIONAL','NO_AUTO_CREATE_USER','HIGH_NOT_PRECEDENCE','NO_ENGINE_SUBSTITUTION','PAD_CHAR_TO_FULL_LENGTH','EMPTY_STRING_IS_NULL','SIMULTANEOUS_ASSIGNMENT'), found type set('REAL_AS_FLOAT','PIPES_AS_CONCAT','ANSI_QUOTES','IGNORE_SPACE','IGNORE_BAD_TABLE_OPTIONS','ONLY_FULL_GROUP_BY','NO_UNSIGNED_SUBTRACTION','NO_DIR_IN_CREATE','POSTGRESQL','ORACLE','MSSQL','DB2','MAXDB','NO_KEY_OPTIONS','NO_TABLE_OPTIONS','NO_FIELD_OPTIONS','MYSQL323','MYSQL40','ANSI','NO_AUTO_VALU
2019-09-01 21:14:44 0 [ERROR] mysqld: Event Scheduler: An error occurred when initializing system tables. Disabling the Event Scheduler.
2019-09-01 21:14:44 6 [Warning] Failed to load slave replication state from table mysql.gtid_slave_pos: 1146: Table 'mysql.gtid_slave_pos' doesn't exist
2019-09-01 21:14:44 0 [Note] Reading of all Master_info entries succeeded
2019-09-01 21:14:44 0 [Note] Added new Master_info '' to hash table
2019-09-01 21:14:44 0 [Note] mysqld: ready for connections.
Version: '10.4.7-MariaDB-1:10.4.7+maria~bionic'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution
2019-09-01 21:14:46 0 [Note] InnoDB: Buffer pool(s) load completed at 190901 21:14:46
2019-09-01 21:14:49 8 [ERROR] InnoDB: Table `mysql`.`innodb_table_stats` not found.
2019-09-01 21:14:49 8 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active
2019-09-01 21:15:13 9 [ERROR] Transaction not registered for MariaDB 2PC, but transaction is active

Running mysql_upgrade fixed these errors and a subsequent database import was successful.

Math Extension

Using the latest Math extension, formulas constantly return errors similar to:

Failed to parse (MathML with SVG or PNG fallback (recommended for modern browsers and accessibility tools): Invalid response ("Math extension cannot connect to Restbase.") from server "https://wikimedia.org/api/rest_v1/":): {\displaystyle V=IR}

I installed Mathoid and tried to set the $wgMathFullRestbaseURL to the service to no avail since $wgMathFullRestbaseURL required the Restbase API rather than the Mathoid API. I did not want to install Restbase for a simple wiki and requiring Restbase will make hosting it on a shared hosting environment tricky.

Solution

It turns out that the Math extension versions 1.30 and prior works.

Save yourself the headache and use MathML, then set the RestbaseURL and MathML URL to Wikipedia's.

$wgDefaultUserOptions['math'] = 'mathml';
$wgMathFullRestbaseURL = 'https://en.wikipedia.org/api/rest_';
$wgMathMathMLUrl = 'https://mathoid-beta.wmflabs.org/';

Visual Editor

All templates are puzzle pieces

All templates within the Visual Editor view appear as puzzle pieces. Investigate by looking at the Parsoid logs. The issue I had which was not obvious was that the template expansion was failing because I referenced the unencrypted HTTP URL rather than the HTTPS one. The 301 HTTPS redirect rule I had caused Parsoid to fail which resulted in templates being rendered as puzzle pieces.

{"name":"parsoid","hostname":"c2774ece52ab","pid":9,"level":40,"logType":"warn","wiki":"wiki$0","title":"Kubernetes","oldId":3829,"reqId":null,"userAgent":"VisualEditor-MediaWiki/1.34.1","msg":"non-200 response: 301 <!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>301 Moved Permanently</title>\n</head><body>\n<h1>Moved Permanently</h1>\n<p>The document has moved <a href=\"https://leo.leung.xyz/wiki/api.php\">here</a>.</p>\n</body></html>\n","longMsg":"non-200 response:\n301\n<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>301 Moved Permanently</title>\n</head><body>\n<h1>Moved Permanently</h1>\n<p>The document has moved <a href=\"https://leo.leung.xyz/wiki/api.php\">here</a>.</p>\n</body></html>\n","levelPath":"warn","time":"2020-05-24T13:02:41.718Z","v":0}

If you are using the docker image I referenced above, the docker-compose.yml file should have the environment variable PARSOID_DOMAIN_domain reference the appropriate https version of the URL.

Error contacting the Parsoid/RESTBase server (HTTP 412)

A private wiki had this error. The error was caused by a traefik middleware used for authentication. I believe the script used for the middleware was generating output which somehow interfered with the rest.php request. Disabling (and later fixing) the middleware fixed this issue.

The DynamicPageList extension doesn't sort by lastedit properly

As mentioned in the DynamicPageList extension documentation, 'lastedit' sort method doesn't actually work as you'd expect:

It should be noted, that lastedit really sorts by the last time the page was touched. In some cases this is not equivalent to the last edit (for example, this includes permission changes, creation or deletion of linked pages, and alteration of contained templates).

Worse yet, editing LocalSettings.php would cause all pages to be 'touched' and render the generated list to be a random tossup of all pages within the Wiki and rendering the list to be completely useless.

A fix would be to tweak the query for 'lastedit' in DynamicPageListHooks.php:

// From:
case 'lastedit':
	$sqlSort = 'page_touched';

// To:
case 'lastedit':
	$fields['rc_timestamp'] = 'MAX(recentchanges.rc_timestamp)';
	$tables['recentchanges'] = 'recentchanges';
	$join['recentchanges'] = ['LEFT JOIN', 'recentchanges.rc_title = page_title'];
	$options['GROUP BY'] = "page_id";
	$sqlSort = 'rc_timestamp';

Updating from 1.34 to 1.35

Version 1.35 includes a built-in PHP based parser to replace Parsoid and bundles the VisualEditor extension.

I had issues getting the 1.34 Docker container to work after updating it to 1.35 because the wiki wasn't able to reach its REST API via localhost within the container. It was only after setting the servername to the public IP address in /etc/hosts that the API was able to reach its REST API and then have visual editor work properly. This is stupid however because REST API traffic will leave the container, out to my ISP, then come right back in to the same container via the reverse proxy.

Changes I had to make for VisualEditor to work:

  1. Add wiki.home.steamr.com to my external IP address in /etc/hosts.
  2. Add to nginx.conf after the location / section:
    location /rest.php/ {
            try_files $uri $uri/ /rest.php?$query_string;
    }
    

Updating from 1.35 to 1.37

I tried updating from 1.35 to 1.37 using the official Docker image and had issues getting Parsoid and the VisualEditor to work. Using my existing LocalSettings.php config, I ran into 404 errors when launching the VisualEditor. This was resolved by defining the URL in $wgVirtualRestConfig['modules']['parsoid'] to point to the Docker container itself:

$wgVirtualRestConfig['modules']['parsoid'] = array(
    // URL to the Parsoid instance
    'url' => 'http://localhost:8080/rest.php',
);

This however resulted in another error 400 which looking at the web browser developer console shows "The requested relative path (...) did not match any known handler". I did see the rest.php calls land on the web server of the container. This was eventually fixed by loading the Parsoid extension in LocalSettings.php. What I have in my LocalSettings.php includes the following lines:

wfLoadExtension('VisualEditor');
wfLoadExtension( 'Parsoid', 'vendor/wikimedia/parsoid/extension.json' );
$wgDefaultUserOptions['visualeditor-enable'] = 1;

Updating from 1.37 to 1.39

Skin deprecation issues. To hide deprecation notices, I added the following lines to LocalSettings.php:

$wgShowExceptionDetails = false;
$wgDeprecationReleaseLimit = '1.30';

Corrupt objectcache table

For some odd reason, my Wiki's objectcache table is randomly getting corrupted. This can be recovered from if you drop and re-create the objectcache table.

# mariadb -u root -p$MYSQL_ROOT_PASSWORD
MariaDB [(none)]> use wiki
MariaDB [wiki]>  DROP TABLE IF EXISTS `objectcache`;
MariaDB [wiki]>  CREATE TABLE `objectcache` (
  `keyname` varbinary(255) NOT NULL DEFAULT '',
  `value` mediumblob DEFAULT NULL,
  `exptime` binary(14) NOT NULL,
  `modtoken` varbinary(17) NOT NULL DEFAULT '00000000000000000',
  `flags` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`keyname`),
  KEY `exptime` (`exptime`)
) ENGINE=InnoDB DEFAULT CHARSET=binary;