Neulich wurde ich gefragt, wie man alle Artikel eines mediawikis in ein PDF packen könnte.
Ãœber Sinn und Zweck kann man streiten, da sich die Sache aber schnell als schwieriger herausstellte, als ich anfangs annahm, war mein Ehrgeiz geweckt.

Bei mediawiki.org sind zwar mehrere Extensions gelistet, die zum PDF-Export dienen, allerdings galt für alle: Entweder sie funktionieren mit der aktuellen Version 1.37 nicht oder die Installation ist derart komplex mit zig Abhängigkeiten und zu installierenden Serverkomponenten, dass ich darauf erst gar keine Lust hatte (wieso npm installieren, um ein PDF zu erstellen? Geht's noch?)...denn wer weiß, ob die Extension nach der umfangreichen Installation überhaupt funktioniert.

Am Ende hatte ich ein kleines PHP-Skript geschrieben, welches auf die mediawiki-API zurückgreift.
Einzige Abhängigkeiten: htmldoc auf dem Server installieren (dafür ist ein Kommandozeilenzugriff erforderlich) und einen Bot-Zugang in der Wiki erstellen.

Vorbereitungen

  1. htmldoc installieren. Dies geht auf einem Linux-System recht unkompliziert mit
    sudo apt-get install htmldoc
    htmldoc kann eine html-Seite in ein PDF konvertieren.
  2. Einen Bot-Zugang in der Wiki erstellen, damit wir über die API auf die Wiki zugreifen können. Dies geht über die Spezial-Seite Spezial:BotPasswords, die über die URL https://<wiki>/index.php/Spezial:BotPasswords aufzurufen sein sollte.
    Nach der Auswahl der Berechtigung (eigentlich ist nur ein Lesezugriff auf alles erforderlich) sollte ein Benutzername und ein Passwort von der Wiki generiert werden.

Skript

Folgendes Skript habe ich im Web-Verzeichnis des Webservers abgelegt. Es ist allerdings nur per Kommandozeile aufrufbar, damit auf dem öffentlichen Server keine unberechtigte Person die PDF-Generierung anstoßen kann.
Das Skript ist allerdings modifizierbar, so dass es auch vom Browser aus aufgerufen werden kann...so lange das PHP-Timeout nicht dazwischenkommt. (einfach die beiden Zeilen if (!defined('STDIN')) die(...) entfernen und aus $_ARG mach' $_GET)

In dem Verzeichnis, wo dieses Skript liegt, muss noch ein Ordner tmp mit Schreibrechten angelegt werden.
Hier wird erst temporär eine html-Seite angelegt, welche alle Artikel des Wikis enthält und später das PDF.

wiki2text.php
<?php

if (!defined('STDIN'))
  die("Dieses Skript kann nur über die Kommandozeile aufgerufen werden.");

// Kommandozeilen-Argumente  
parse_str(implode('&', array_slice($argv, 1)), $_ARG);
if (!isset($_ARG['url']) || !isset($_ARG['user']) || !isset($_ARG['pass'])) {
  echo("Verwendung:\n");
  echo("php wiki2pdf url=<Wiki-URL> user=<Bot-Benutzername> pass=<Bot-Passwort>\n\n");
  echo("Beispiel:\n");
  echo("php wiki2pdf url=https://meinwiki.de/\n\n");
  die();
  }
$_ARG['url'] = rtrim($_ARG['url'],"/");

$endPoint = $_ARG['url'] . "/api.php";
$args = "--left 1cm --right 1cm --top 1cm --bottom 1cm " . // Ränder
    "--header ... --footer .1. --headfootsize 8 --quiet --jpeg --color " .
    "--bodyfont Arial --fontsize 8 --fontspacing 1.5 --linkstyle plain --linkcolor 217A28 " .
    "--toclevels 2 --no-title yes --charset iso-8859-1 --firstpage toc";

$alle_seiten = getAllPages();

// Oder kommen noch Seiten spezieller Namensräume hinzu?
// Hier alle Standardseiten und Seiten der Namensräum 4000 und 4100 abfragen
// $std = getAllPages();
// $gdp = getAllPages(4000);
// $regularien = getAllPages(4100);
// $alle_seiten = $std + $gdp + $regularien;

file_put_contents("tmp/wiki.html", "<html><head></head><body>");
if (count($alle_seiten) > 0) {
  foreach($alle_seiten as $s) {
    $seite = getPage($s['pageid']);
    $artikel = $seite['text'];
//  $artikel = utf8_decode($artikel); // ggf. notwendig wegen zerpflückter Umlaute
    $artikel = str_replace(array('href="/', 'src="/'),
                          array('href="'.$_ARG['url'], 'src="'.$_ARG['url']),
                          $artikel); // Absolute URLs erstellen
    $html = "<h1>" . $seite['title'] . "</h1>" . $artikel;
    file_put_contents("tmp/wiki.html", $html, FILE_APPEND);
    }
  }
file_put_contents("tmp/wiki.html", "</body></html>", FILE_APPEND);
shell_exec("/usr/bin/htmldoc -t pdf --format pdf14 $args \"tmp/wiki.html\" > \"tmp/wiki.pdf\"" );
unlink("tmp/wiki.html");

echo("Fertig.\nPDF-Dokument angelegt unter tmp/wiki.pdf");

// Requesttoken abrufen, um Logintoken anzufragen
// (wie dämlich ist sowas denn...?!)
function getLoginToken() {
   global $endPoint;
   $params1 = [
       "action"  => "query",
       "meta"    => "tokens",
       "type"    => "login",
       "format"  => "json"
       ];

   $ch = curl_init($endPoint . "?" . http_build_query( $params1 ));
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/cookie.txt");
   $output = curl_exec($ch);
   curl_close($ch);

   $result = json_decode( $output, true );
   return $result["query"]["tokens"]["logintoken"];
   }

// POST-Request für eigentlichen Login
function loginRequest( $logintoken ) {
   global $endPoint, $_ARG;
   $params2 = [
       "action"     => "login",
       "lgname"     => $_ARG['user'],
       "lgpassword" => $_ARG['pass'],
       "lgtoken"    => $logintoken,
       "format"     => "json"
       ];

   $ch = curl_init();
   curl_setopt($ch, CURLOPT_URL, $endPoint);
   curl_setopt($ch, CURLOPT_POST, true);
   curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params2));
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/cookie.txt");

   $output = curl_exec($ch);
   curl_close($ch);
   $result = json_decode($output, true);
 
   return ($result['login']['result'] == "Success");
   }

// IDs und Namen aller Seiten in Erfahrung bringen
function getAllPages($namespace=null, $retry=false) {
   global $endPoint;
   $ch = curl_init($endPoint . "?action=query&format=json&list=allpages&aplimit=500".(($namespace==null)?"":"&apnamespace=$namespace"));
   curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   $output = curl_exec($ch);
   curl_close($ch);
   $result = json_decode($output, true);
 
   if (!$retry && isset($result['error']) && $result['error']['code'] == "readapidenied") {
     // Es scheint, wir müssen uns erst einloggen...
     $login_Token = getLoginToken();
     loginRequest($login_Token);
     return getAllPages($namespace, true);
     }
 
   return $result['query']['allpages'];
   }

// Name und Inhalt eines Artikels abrufen
function getPage($id, $retry=false) {
   global $endPoint;
   $ch = curl_init($endPoint . "?action=parse&pageid=$id&prop=text&formatversion=2&format=json");
   curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/cookie.txt");
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   $output = curl_exec($ch);
   curl_close($ch);
   $result = json_decode($output, true);

   if (!$retry && isset($result['error']) && $result['error']['code'] == "readapidenied") {
     // Es scheint, wir müssen uns erst einloggen...
     $login_Token = getLoginToken();
     loginRequest($login_Token);
     return getPage($id, true);
     }
 
   return $result['parse'];
   }
?>

Verwendung auf der Kommandozeile:

php wiki2pdf.php url=https://meinwiki.de/ user=Botuser@api-bot pass=geheimesbotpasswort

Zu beachten ist, dass die API auf die Anfrage, alle Seiten aufzulisten (getAllPages()), nur maximal 500 Ergebnisse mitteilt.
Sollte die Wiki mehr als 500 Seiten ihr eigen nennen, müsste man in diese Funktion noch etwas mehr Arbeit stecken, um durch die Ergebnisse zu "blättern".

Das erstellte PDF kann sich sehen lassen – dank htmldoc mit nummerierten Seiten und einem Inhaltsverzeichnis.
Sollten die Umlaute im PDF zerschossen sein, mal versuchen, die Kommentierung vor der Zeile $artikel = utf8_decode($artikel); zu entfernen. htmldoc hat ein Problem mit UTF-8.