*/
class admin_plugin_archivegenerator extends DokuWiki_Admin_Plugin
{
protected $generateArchive = false;
protected $type = 'full';
protected $base = 'dokuwiki/';
/** @inheritdoc */
public function getMenuSort()
{
return 123;
}
/** @inheritdoc */
public function forAdminOnly()
{
return true;
}
/** @inheritdoc */
public function handle()
{
global $INPUT;
if ($INPUT->bool('isupdate')) $this->type = 'update';
if ($INPUT->bool('downloadArchive') && checkSecurityToken()) {
$this->sendArchiveAndExit();
}
if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') {
return;
}
$sectok = $INPUT->post->str('sectok');
if (!checkSecurityToken($sectok)) {
return;
}
// check for email and pass on full archives only
if ($this->type == 'full') {
$email = $INPUT->post->str('adminMail');
$pass = $INPUT->post->str('adminPass');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
msg(sprintf($this->getLang('message: email invalid'), hsc($email)), -1);
return;
}
if (empty($pass)) {
msg($this->getLang('message: password empty'), -1);
return;
}
}
$this->generateArchive = true;
}
/** @inheritdoc */
public function html()
{
if (!$this->generateArchive) {
$this->downloadView();
ptln('
' . $this->getLang('menu') . '
');
echo $this->locale_xhtml('intro');
} else {
ptln('' . $this->getLang('menu') . '
');
try {
if ($this->type == 'full') {
$this->generateArchive();
} else {
$this->generateUpdateArchive();
}
return;
} catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
msg(hsc($e->getMessage()), -1);
}
}
$this->showFullForm();
$this->showUpdateForm();
}
/**
* Send the existing wiki archive file and exit
*/
protected function sendArchiveAndExit()
{
global $conf;
if ($this->type == 'full') {
$persistentArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive.zip';
} else {
$persistentArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive-update.zip';
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="archive.zip"');
http_sendfile($persistentArchiveFN);
readfile($persistentArchiveFN);
exit();
}
/**
* Build the archive based on the existing wiki
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function generateArchive()
{
global $conf;
$this->log('info', $this->getLang('message: starting'));
$tmpArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive_new.zip';
$archive = $this->createZipArchive($tmpArchiveFN);
set_time_limit(0);
$this->addDirToArchive($archive, '.', false);
$this->addDirToArchive($archive, 'inc');
$this->addDirToArchive($archive, 'bin');
$this->addDirToArchive($archive, 'vendor');
$this->addDirToArchive($archive, 'conf', true, '^' . $this->base . 'conf/(users\.auth\.php|acl\.auth\.php)$');
$this->addUsersAuthToArchive($archive);
$this->addACLToArchive($archive);
$this->addDirToArchive($archive, 'lib', true, '^' . $this->base . 'lib/plugins$');
$this->addDirToArchive($archive, 'lib/plugins', true, $this->buildSkipPluginRegex());
$this->addDirToArchive($archive, 'data/pages');
$this->addDirToArchive($archive, 'data/meta', true, '\.changes(\.trimmed)?$');
$this->addDirToArchive($archive, 'data/media');
$this->addDirToArchive($archive, 'data/media_meta', true, '\.changes$');
$this->addDirToArchive($archive, 'data/index');
$this->addEmptyDirToArchive($archive, 'data/attic');
$this->addEmptyDirToArchive($archive, 'data/cache');
$this->addEmptyDirToArchive($archive, 'data/log');
$this->addEmptyDirToArchive($archive, 'data/locks');
$this->addEmptyDirToArchive($archive, 'data/tmp');
$this->addEmptyDirToArchive($archive, 'data/media_attic');
$archive->close();
$this->log('info', $this->getLang('message: adding data done'));
$persistentArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive.zip';
io_rename($tmpArchiveFN, $persistentArchiveFN);
$href = $this->getDownloadLinkHref();
$link = "" . $this->getLang('link: download now') . '';
$this->log('success', $this->getLang('message: done') . ' ' . $link);
// try a redirect to self
ptln('');
}
/**
* Build an update archive based on the existing wiki
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function generateUpdateArchive()
{
global $conf;
$this->log('info', $this->getLang('message: starting'));
$tmpArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive-update_new.zip';
$archive = $this->createZipArchive($tmpArchiveFN);
set_time_limit(0);
$this->addDirToArchive($archive, '.', false);
$this->addDirToArchive($archive, 'inc');
$this->addDirToArchive($archive, 'bin');
$this->addDirToArchive($archive, 'vendor');
$this->addDirToArchive($archive, 'conf', true, '^' . $this->base . 'conf/(users\.auth\.php|acl\.auth\.php|.*local\.(php|conf))$');
$this->addDirToArchive($archive, 'lib', true, '^' . $this->base . 'lib/plugins$');
$this->addDirToArchive($archive, 'lib/plugins', true, $this->buildSkipPluginRegex());
$this->addEmptyDirToArchive($archive, 'data/pages');
$this->addEmptyDirToArchive($archive, 'data/media');
$this->addEmptyDirToArchive($archive, 'data/index');
$this->addEmptyDirToArchive($archive, 'data/media_meta');
$this->addEmptyDirToArchive($archive, 'data/meta');
$this->addEmptyDirToArchive($archive, 'data/attic');
$this->addEmptyDirToArchive($archive, 'data/cache');
$this->addEmptyDirToArchive($archive, 'data/log');
$this->addEmptyDirToArchive($archive, 'data/locks');
$this->addEmptyDirToArchive($archive, 'data/tmp');
$this->addEmptyDirToArchive($archive, 'data/media_attic');
$archive->close();
$this->log('info', $this->getLang('message: adding data done'));
$persistentArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive-update.zip';
io_rename($tmpArchiveFN, $persistentArchiveFN);
$href = $this->getDownloadLinkHref('update');
$link = "" . $this->getLang('link: download now') . '';
$this->log('success', $this->getLang('message: done') . ' ' . $link);
// try a redirect to self
ptln('');
}
/**
* Build a regex for the plugins to skip, relative to the DokuWiki root
*
* @return string
*/
protected function buildSkipPluginRegex()
{
$list = array_map('trim', explode(',', $this->getConf('pluginsToIgnore')));
return '^' . $this->base . 'lib/plugins/(' . implode('|', $list) . ')$';
}
/**
* Generate a href for a link to download the archive
*
* @param string $type
* @return string
*/
protected function getDownloadLinkHref($type = 'full')
{
global $ID;
return wl($ID, [
'do' => 'admin',
'page' => 'archivegenerator',
'downloadArchive' => 1,
'sectok' => getSecurityToken(),
'isupdate' => (int)($type == 'update'),
]);
}
/**
* Generate the link to the admin page itself
*
* @return string
*/
protected function getSelfRedirect()
{
global $ID;
return wl($ID, [
'do' => 'admin',
'page' => 'archivegenerator',
], false, '&');
}
/**
* Add an empty directory to the archive.
*
* The directory will contain a dummy .keep file.
*
* @param Zip $archive
* @param string $directory path of the directory to add relative to the dokuwiki root
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function addEmptyDirToArchive(Zip $archive, $directory)
{
$this->log('info', sprintf($this->getLang('message: create empty dir'), $directory));
$dirPath = $this->base . $directory . '/.keep';
$archive->addData($dirPath, '');
}
/**
* Create a users.auth.php file with a single admin user
*
* @param Zip $archive
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function addUsersAuthToArchive(Zip $archive)
{
global $INPUT;
$email = $INPUT->post->str('adminMail');
$pass = $INPUT->post->str('adminPass');
$this->log('info', $this->getLang('message: create users'));
$authFile = '
# users.auth.php
#
# Don\'t modify the lines above
#
# Userfile
#
# Format:
#
# login:passwordhash:Real Name:email:groups,comma,separated
';
$pwHash = auth_cryptPassword($pass);
$adminLine = "admin:$pwHash:Administrator:$email:user,admin\n";
$archive->addData($this->base . 'conf/users.auth.php', $authFile . $adminLine);
}
/**
* Create an acl.auth.php file that allows reading only for logged-in users
*
* @param Zip $archive
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function addACLToArchive(Zip $archive)
{
$this->log('info', $this->getLang('message: create acl'));
$aclFileContents = '# acl.auth.php
#
* @ALL 0
* @users 1
';
$archive->addData($this->base . 'conf/acl.auth.php', $aclFileContents);
}
/**
* Create the archive file
*
* @return Zip
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function createZipArchive($archiveFN)
{
$this->log('info', sprintf($this->getLang('message: create zip archive'), hsc($archiveFN)));
io_makeFileDir($archiveFN);
$zip = new Zip();
$zip->create($archiveFN);
return $zip;
}
/**
* Add the contents of an directory to the archive
*
* @param Zip $archive
* @param string $srcDir the directory relative to the dokuwiki root
* @param bool $recursive whether to add subdirectories as well
* @param null|string $skipRegex files and directories matching this regex will be ignored. no delimiters
*
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function addDirToArchive(Zip $archive, $srcDir, $recursive = true, $skipRegex = null)
{
$message = [];
$message[] = sprintf($this->getLang('message: add files in dir'), hsc($srcDir . '/'));
if ($recursive) {
$message[] = $this->getLang('message: recursive');
}
if ($skipRegex) {
$message[] = sprintf($this->getLang('message: skipping files'), hsc($skipRegex));
}
$message[] .= '...';
$this->log('info', implode(' ', $message));
$this->addFilesToArchive(DOKU_INC . $srcDir, $archive, !$recursive, $skipRegex);
}
/**
* Recursive method to add files and directories to a archive
*
* It will report large files that might cause the process to fail.
*
* @param string $source
* @param Zip $archive
* @param bool $filesOnly
* @param null $skipRegex
*
* @return bool
* @throws \splitbrain\PHPArchive\ArchiveIOException
*/
protected function addFilesToArchive($source, Zip $archive, $filesOnly = false, $skipRegex = null)
{
// Simple copy for a file
if (is_file($source)) {
if (filesize($source) > 50 * 1024 * 1024) {
$this->log('warning', sprintf($this->getLang('message: file is large'),
hsc($source)) . ' ' . filesize_h(filesize($source)));
}
try {
$archive->addFile($source, $this->getDWPathName($source));
} catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
$this->log('error', hsc($e->getMessage()));
throw $e;
}
return true;
}
// Loop through the folder
$dir = dir($source);
while (false !== $entry = $dir->read()) {
if (in_array($entry, ['.', '..', '.git', 'node_modules'])) {
continue;
}
$srcFN = "$source/$entry";
if ($skipRegex && preg_match("#$skipRegex#", $this->getDWPathName($srcFN))) {
continue;
}
if (is_dir($srcFN) && $filesOnly) {
continue;
}
$copyOK = $this->addFilesToArchive($srcFN, $archive, $filesOnly, $skipRegex);
if ($copyOK === false) {
return false;
}
}
// Clean up
$dir->close();
return true;
}
/**
* Get the filepath relative to the dokuwiki root
*
* @param $filepath
*
* @return string
*/
protected function getDWPathName($filepath)
{
return $this->base . substr($filepath, strlen(DOKU_INC));
}
/**
* Display the download info
*/
protected function downloadView()
{
global $conf;
$fullArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive.zip';
$updateArchiveFN = $conf['tmpdir'] . '/archivegenerator/archive-update.zip';
if (!file_exists($fullArchiveFN) && !file_exists($updateArchiveFN)) return;
ptln('' . $this->getLang('label: download') . '
');
if (file_exists($fullArchiveFN)) {
$mtime = dformat(filemtime($fullArchiveFN));
$href = $this->getDownloadLinkHref();
ptln('');
ptln('' . $this->getLang('label: full archive') . '
');
ptln(sprintf($this->getLang('message: archive exists'), $mtime));
ptln("" . $this->getLang('link: download now') . '');
ptln('
');
}
if (file_exists($updateArchiveFN)) {
$mtime = dformat(filemtime($updateArchiveFN));
$href = $this->getDownloadLinkHref('update');
ptln('');
ptln('' . $this->getLang('label: update archive') . '
');
ptln(sprintf($this->getLang('message: archive exists'), $mtime));
ptln("" . $this->getLang('link: download now') . '');
ptln('
');
}
}
/**
* Show the form to create a full archive
*/
protected function showFullForm()
{
$form = new \dokuwiki\Form\Form();
$form->addFieldsetOpen($this->getLang('label: full archive'));
$adminMailInput = $form->addTextInput('adminMail', $this->getLang('label: admin mail'));
$adminMailInput->addClass('block');
$adminMailInput->attrs(['type' => 'email', 'required' => '1']);
$adminPassInput = $form->addPasswordInput('adminPass', $this->getLang('label: admin pass'));
$adminPassInput->addClass('block');
$adminPassInput->attr('required', 1);
$form->addButton('submit', $this->getLang('button: generate archive'));
$form->addFieldsetClose();
echo $form->toHTML();
}
/**
* Show the form to create a full archive
*/
protected function showUpdateForm()
{
$form = new \dokuwiki\Form\Form();
$form->addFieldsetOpen($this->getLang('label: update archive'));
$form->setHiddenField('isupdate', '1');
$form->addButton('submit', $this->getLang('button: generate archive'));
$form->addFieldsetClose();
echo $form->toHTML();
}
/**
* Print a message to the user, prefixes the time since the first message
*
* This adds whitespace padding to force the message being printed immediately.
*
* @param string $level can be 'error', 'warning' or 'info'
* @param string $message
*/
protected function log($level, $message)
{
static $startTime;
if (!$startTime) {
$startTime = microtime(true);
}
$time = round(microtime(true) - $startTime, 3);
$timedMessage = sprintf($this->getLang('seconds'), $time) . ': ' . $message;
switch ($level) {
case 'error':
$msgLVL = -1;
break;
case 'warning':
$msgLVL = 2;
break;
case 'success':
$msgLVL = 1;
break;
default:
$msgLVL = 0;
}
msg($timedMessage, $msgLVL);
echo str_repeat(' ', 16 * 1024);
/** @noinspection MissingOrEmptyGroupStatementInspection */
/** @noinspection LoopWhichDoesNotLoopInspection */
/** @noinspection PhpStatementHasEmptyBodyInspection */
while (@ob_end_flush()) {
};
flush();
}
}