Files
forum/Sources/Packages.php

712 lines
25 KiB
PHP

<?php
/******************************************************************************
* Packages.php *
*******************************************************************************
* SMF: Simple Machines Forum *
* Open-Source Project Inspired by Zef Hemel (zef@zefhemel.com) *
* =========================================================================== *
* Software Version: SMF 1.0.1 *
* Software by: Simple Machines (http://www.simplemachines.org) *
* Copyright 2001-2005 by: Lewis Media (http://www.lewismedia.com) *
* Support, News, Updates at: http://www.simplemachines.org *
*******************************************************************************
* This program is free software; you may redistribute it and/or modify it *
* under the terms of the provided license as published by Lewis Media. *
* *
* This program is distributed in the hope that it is and will be useful, *
* but WITHOUT ANY WARRANTIES; without even any implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* See the "license.txt" file for details of the Simple Machines license. *
* The latest version can always be found at http://www.simplemachines.org. *
******************************************************************************/
if (!defined('SMF'))
die('Hacking attempt...');
// This is the notorious package manager..... :/.
function Packages()
{
global $txt, $scripturl, $sourcedir, $context;
isAllowedTo('admin_forum');
// Managing packages!
adminIndex('manage_packages');
// Load all the basic stuff.
require_once($sourcedir . '/Subs-Package.php');
loadLanguage('Packages');
loadTemplate('Packages');
// The package manager requires gzinflate (zlib) to work properly.
if (!function_exists('gzinflate'))
fatal_lang_error('package_no_zlib');
// Set up the linktree and title so it's already done.
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages',
'name' => &$txt['package1']
);
$context['page_title'] = $txt['package1'];
// Delegation makes the world... that is, the package manager go 'round.
$subActions = array(
'' => 'PackageBrowse',
'browse' => 'PackageBrowse',
'remove' => 'PackageRemove',
'list' => 'PackageList',
'install' => 'PackageInstallTest',
'install2' => 'PackageInstall',
'uninstall' => 'PackageInstallTest',
'uninstall2' => 'PackageInstall',
'installed' => 'InstalledList',
'options' => 'PackageOptions',
'flush' => 'FlushInstall',
'examine' => 'ExamineFile'
);
if (isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]))
$subActions[$_REQUEST['sa']]();
else
$subActions['']();
// Add the package above/below layer to the output.
$context['template_layers'][] = 'package';
}
// Test install a package.
function PackageInstallTest()
{
global $boarddir, $txt, $context, $scripturl, $sourcedir, $modSettings;
// You have to specify a file!!
if (!isset($_REQUEST['package']) || $_REQUEST['package'] == '')
redirectexit('action=packages');
$context['filename'] = $_REQUEST['package'];
require_once($sourcedir . '/Subs-Package.php');
$context['uninstalling'] = $_REQUEST['sa'] == 'uninstall';
// Set up the linktree...
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=browse',
'name' => $context['uninstalling'] ? $txt['package_uninstall_actions'] : $txt['package42']
);
$context['page_title'] .= ' - ' . ($context['uninstalling'] ? $txt['package_uninstall_actions'] : $txt['package42']);
$context['sub_template'] = 'view_package';
if (!file_exists($boarddir . '/Packages/' . $context['filename']))
fatal_lang_error('package_no_file', false);
// Make sure temp directory exists and is empty.
if (file_exists($boarddir . '/Packages/temp'))
deltree($boarddir . '/Packages/temp', false);
if (!mktree($boarddir . '/Packages/temp', 0777))
fatal_lang_error('package_cant_download', false);
// Extract the files so we can get things like the readme, etc.
$context['extracted_files'] = read_tgz_file($boarddir . '/Packages/' . $context['filename'], $boarddir . '/Packages/temp');
// Get the package info...
$packageInfo = getPackageInfo($context['filename']);
$packageInfo['filename'] = $context['filename'];
// Set the type of extraction...
$context['extract_type'] = isset($packageInfo['type']) ? $packageInfo['type'] : 'modification';
$instmods = loadInstalledPackages();
// The mod isn't installed.... unless proven otherwise.
$context['is_installed'] = false;
foreach ($instmods as $installed_mod)
if ($installed_mod['id'] == $packageInfo['id'])
$old_version = $installed_mod['version'];
// Wait, it's not installed yet!
if (!isset($old_version) && $context['uninstalling'])
{
deltree($boarddir . '/Packages/temp');
fatal_lang_error('package_cant_uninstall', false);
}
// Uninstalling?
elseif ($context['uninstalling'])
{
$actions = parsePackageInfo($packageInfo['xml'], true, 'uninstall');
// Gadzooks! There's no uninstaller at all!?
if (empty($actions))
fatal_lang_error('package_uninstall_cannot', false);
}
elseif (isset($old_version) && $old_version != $packageInfo['version'])
{
// Look for an upgrade...
$actions = parsePackageInfo($packageInfo['xml'], true, 'upgrade', $old_version);
// There was no upgrade....
if (empty($actions))
$context['is_installed'] = true;
}
elseif (isset($old_version) && $old_version == $packageInfo['version'])
$context['is_installed'] = true;
if (!isset($old_version) || $context['is_installed'])
$actions = parsePackageInfo($packageInfo['xml'], true, 'install');
$context['actions'] = array();
$context['ftp_needed'] = false;
$chmod_files = array();
if (empty($actions))
return;
foreach ($actions as $action)
{
if ($action['type'] == 'chmod')
{
$context['ftp_needed'] = true;
$chmod_files[] = $action['filename'];
continue;
}
elseif ($action['type'] == 'readme')
{
if (file_exists($boarddir . '/Packages/temp/' . $action['filename']))
$context['package_readme'] = nl2br(htmlspecialchars(implode('', file($boarddir . '/Packages/temp/' . $action['filename']))));
continue;
}
elseif ($action['type'] == 'modification')
{
if ($action['boardmod'])
$mod_actions = parseBoardMod(@implode('', @file($boarddir . '/Packages/temp/' . $action['filename'])), true, $action['reverse']);
else
$mod_actions = parseModification(@implode('', @file($boarddir . '/Packages/temp/' . $action['filename'])), true, $action['reverse']);
foreach ($mod_actions as $mod_action)
{
if ($mod_action['type'] == 'opened')
$failed = false;
elseif ($mod_action['type'] == 'failure')
$failed = true;
elseif ($mod_action['type'] == 'chmod')
{
$context['ftp_needed'] = true;
$chmod_files[] = $mod_action['filename'];
}
elseif ($mod_action['type'] == 'saved')
$context['actions'][] = array(
'type' => $txt['package56'],
'action' => strtr($mod_action['filename'], array($boarddir => '.')),
'description' => $failed ? $txt['package_action_failure'] : $txt['package_action_success']
);
elseif ($mod_action['type'] == 'missing')
$context['actions'][] = array(
'type' => $txt['package56'],
'action' => strtr($mod_action['filename'], array($boarddir => '.')),
'description' => $txt['package_action_missing']
);
elseif ($mod_action['type'] == 'error')
$context['actions'][] = array(
'type' => $txt['package56'],
'action' => strtr($mod_action['filename'], array($boarddir => '.')),
'description' => $txt['package_action_error']
);
}
// Don't add anything else.
$thisAction = array();
}
elseif ($action['type'] == 'code')
$thisAction = array(
'type' => $txt['package57'],
'action' => $action['filename']
);
elseif (in_array($action['type'], array('create-dir', 'create-file')))
$thisAction = array(
'type' => $txt['package50'] . ' ' . ($action['type'] == 'create-dir' ? $txt['package55'] : $txt['package54']),
'action' => strtr($action['destination'], array($boarddir => '.'))
);
elseif (in_array($action['type'], array('require-dir', 'require-file')))
$thisAction = array(
'type' => $txt['package53'] . ' ' . ($action['type'] == 'require-dir' ? $txt['package55'] : $txt['package54']),
'action' => strtr($action['destination'], array($boarddir => '.'))
);
elseif (in_array($action['type'], array('move-dir', 'move-file')))
$thisAction = array(
'type' => $txt['package51'] . ' ' . ($action['type'] == 'move-dir' ? $txt['package55'] : $txt['package54']),
'action' => strtr($action['source'], array($boarddir => '.')) . ' => ' . strtr($action['destination'], array($boarddir => '.'))
);
elseif (in_array($action['type'], array('remove-dir', 'remove-file')))
$thisAction = array(
'type' => $txt['package52'] . ' ' . ($action['type'] == 'remove-dir' ? $txt['package55'] : $txt['package54']),
'action' => strtr($action['filename'], array($boarddir => '.'))
);
if (empty($thisAction))
continue;
$thisAction['description'] = isset($action['description']) ? $action['description'] : '';
$context['actions'][] = $thisAction;
}
if (file_exists($boarddir . '/Packages/temp'))
deltree($boarddir . '/Packages/temp');
if ($context['ftp_needed'])
{
if (isset($_POST['ftp_username']))
{
$ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']);
if ($ftp->error === false)
{
// Common mistake, so let's try to remedy it...
if (!$ftp->chdir($_POST['ftp_path']))
$ftp->chdir(preg_replace('~^/home/[^/]+?~', '', $_POST['ftp_path']));
}
}
if (!isset($ftp) || $ftp->error !== false)
{
if (!isset($_POST['ftp_path']))
{
if (preg_match('~^/home/([^/]+?)/public_html~', $_SERVER['DOCUMENT_ROOT'], $match) == 1)
{
if (!isset($_POST['ftp_username']) && empty($modSettings['package_server']))
$_POST['ftp_username'] = $match[1];
$_POST['ftp_path'] = strtr($_SERVER['DOCUMENT_ROOT'], array('/home/' . $match[1] => ''));
if (substr($_POST['ftp_path'], -1) == '/')
$_POST['ftp_path'] = substr($_POST['ftp_path'], 0, -1);
if (strlen(dirname($_SERVER['PHP_SELF'])) > 1)
$_POST['ftp_path'] .= dirname($_SERVER['PHP_SELF']);
}
elseif (substr($boarddir, 0, 9) == '/var/www/')
$_POST['ftp_path'] = substr($boarddir, 8);
else
$_POST['ftp_path'] = strtr($boarddir, array($_SERVER['DOCUMENT_ROOT'] => ''));
}
$context['package_ftp'] = array(
'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'),
'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'),
'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''),
'path' => $_POST['ftp_path'],
);
}
else
{
$context['ftp_needed'] = false;
if (substr($_POST['ftp_path'], -1) == '/')
$_POST['ftp_path'] = substr($_POST['ftp_path'], 0, -1);
$ftp_root = strtr($boarddir, array($_POST['ftp_path'] => ''));
foreach ($chmod_files as $file)
{
$ftp->chmod(strtr($file, array($ftp_root => '')), 0777);
$ftp->chmod(dirname(strtr($file, array($ftp_root => ''))), 0777);
}
$ftp->close();
}
}
}
// Apply another type of (avatar, language, etc.) package.
function PackageInstall()
{
global $boarddir, $txt, $context, $scripturl, $sourcedir;
// If there's no file, what are we installing?
if (!isset($_REQUEST['package']) || $_REQUEST['package'] == '')
redirectexit('action=packages');
$context['filename'] = $_REQUEST['package'];
require_once($sourcedir . '/Subs-Package.php');
$context['uninstalling'] = $_REQUEST['sa'] == 'uninstall2';
// Set up the linktree for other.
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=browse',
'name' => $context['uninstalling'] ? $txt['smf198b'] : $txt['package37']
);
$context['page_title'] .= ' - ' . ($context['uninstalling'] ? $txt['smf198b'] : $txt['package37']);
$context['sub_template'] = 'extract_package';
if (!file_exists($boarddir . '/Packages/' . $context['filename']))
fatal_lang_error('package_no_file', false);
// Make sure temp directory exists and is empty!
if (file_exists($boarddir . '/Packages/temp'))
deltree($boarddir . '/Packages/temp', false);
else
mktree($boarddir . '/Packages/temp', 0777);
// Let the unpacker do the work.
$context['extracted_files'] = read_tgz_file($boarddir . '/Packages/' . $context['filename'], $boarddir . '/Packages/temp');
// Get the package info...
$packageInfo = getPackageInfo($context['filename']);
$packageInfo['filename'] = $context['filename'];
// Set the type of extraction...
$context['extract_type'] = isset($packageInfo['type']) ? $packageInfo['type'] : 'modification';
$instmods = loadInstalledPackages();
// The mod isn't installed.... unless proven otherwise.
$context['is_installed'] = false;
foreach ($instmods as $installed_mod)
if ($installed_mod['id'] == $packageInfo['id'])
$old_version = $installed_mod['version'];
// Wait, it's not installed yet!
if (!isset($old_version) && $context['uninstalling'])
{
deltree($boarddir . '/Packages/temp');
fatal_lang_error(1, false);
}
// Uninstalling?
elseif ($context['uninstalling'])
{
$install_log = parsePackageInfo($packageInfo['xml'], false, 'uninstall');
// Gadzooks! There's no uninstaller at all!?
if (empty($install_log))
fatal_lang_error('package_uninstall_cannot', false);
}
elseif (isset($old_version) && $old_version != $packageInfo['version'])
{
// Look for an upgrade...
$install_log = parsePackageInfo($packageInfo['xml'], false, 'upgrade', $old_version);
// There was no upgrade....
if (empty($install_log))
$context['is_installed'] = true;
}
elseif (isset($old_version) && $old_version == $packageInfo['version'])
$context['is_installed'] = true;
if (!isset($old_version) || $context['is_installed'])
$install_log = parsePackageInfo($packageInfo['xml'], false, 'install');
$context['install_finished'] = false;
if (!empty($install_log))
{
foreach ($install_log as $action)
{
if ($action['type'] == 'modification' && !empty($action['filename']))
{
if ($action['boardmod'])
parseBoardMod(implode('', file($boarddir . '/Packages/temp/' . $action['filename'])), false, $action['reverse']);
else
parseModification(implode('', file($boarddir . '/Packages/temp/' . $action['filename'])), false, $action['reverse']);
}
elseif ($action['type'] == 'code' && !empty($action['filename']))
{
// This is just here as reference for what is available.
global $txt, $boarddir, $sourcedir, $modSettings, $context, $settings, $db_prefix, $forum_version;
// Now include the file and be done with it ;).
require($boarddir . '/Packages/temp/' . $action['filename']);
}
}
// Check if the mod has been installed.
$seen = false;
// Look through the list of installed mods...
foreach ($instmods as $i => $installed_mod)
if ($installed_mod['id'] == $packageInfo['id'])
{
if ($context['uninstalling'])
$instmods[$i] = array();
else
{
$instmods[$i]['version'] = $packageInfo['version'];
$seen = true;
}
break;
}
// Hasn't.... make it show as installed.
if (!$seen && !$context['uninstalling'])
$instmods[] = $packageInfo;
saveInstalledPackages($instmods);
$context['install_finished'] = true;
}
// Clean house... get rid of the evidence ;).
if (file_exists($boarddir . '/Packages/temp'))
deltree($boarddir . '/Packages/temp');
}
// List the files in a package.
function PackageList()
{
global $txt, $scripturl, $boarddir, $context, $sourcedir;
require_once($sourcedir . '/Subs-Package.php');
// No package? Show him or her the door.
if (!isset($_REQUEST['package']) || $_REQUEST['package'] == '')
redirectexit('action=packages');
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=list;package=' . $_REQUEST['package'],
'name' => &$txt['smf180']
);
$context['page_title'] = $txt['smf180'];
$context['sub_template'] = 'list';
// The filename...
$context['filename'] = $_REQUEST['package'];
// Let the unpacker do the work.
$context['files'] = read_tgz_file($boarddir . '/Packages/' . $_REQUEST['package'], null);
}
// List the files in a package.
function ExamineFile()
{
global $txt, $scripturl, $boarddir, $context, $sourcedir;
require_once($sourcedir . '/Subs-Package.php');
// No package? Show him or her the door.
if (!isset($_REQUEST['package']) || $_REQUEST['package'] == '')
redirectexit('action=packages');
// No file? Show him or her the door.
if (!isset($_REQUEST['file']) || $_REQUEST['file'] == '')
redirectexit('action=packages');
if (isset($_REQUEST['raw']))
{
echo read_tgz_file($boarddir . '/Packages/' . $_REQUEST['package'], $_REQUEST['file'], true);
obExit(false);
}
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=list;package=' . $_REQUEST['package'],
'name' => &$txt['package_examine_file']
);
$context['page_title'] = $txt['package_examine_file'];
$context['sub_template'] = 'examine';
// The filename...
$context['package'] = $_REQUEST['package'];
$context['filename'] = $_REQUEST['file'];
// Let the unpacker do the work.... but make sure we handle images properly.
if (in_array(strtolower(strrchr($_REQUEST['file'], '.')), array('.bmp', '.gif', '.jpeg', '.jpg', '.png')))
$context['filedata'] = '<img src="' . $scripturl . '?action=packages;sa=examine;package=' . $_REQUEST['package'] . ';file=' . $_REQUEST['file'] . ';raw" alt="' . $_REQUEST['file'] . '" />';
elseif (strtolower(strrchr($_REQUEST['file'], '.')) == '.php')
$context['filedata'] = highlight_php_code(htmlspecialchars(read_tgz_file($boarddir . '/Packages/' . $_REQUEST['package'], $_REQUEST['file'], true)));
else
$context['filedata'] = htmlspecialchars(read_tgz_file($boarddir . '/Packages/' . $_REQUEST['package'], $_REQUEST['file'], true));
}
// List the installed packages.
function InstalledList()
{
global $txt, $scripturl, $context;
// Set up the linktree so things are purdy.
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=installed',
'name' => &$txt['package6']
);
$context['page_title'] .= ' - ' . $txt['package6'];
$context['sub_template'] = 'view_installed';
// Load the installed mods and send them to the template.
$context['installed_mods'] = loadInstalledPackages();
}
// Empty out the installed list.
function FlushInstall()
{
global $boarddir;
// Open the file and write nothing to it.
file_put_contents($boarddir . '/Packages/installed.list', '');
redirectexit('action=packages;sa=installed');
}
// Delete a package.
function PackageRemove()
{
global $scripturl, $boarddir;
// Ack, don't allow deletion of arbitrary files here, could become a security hole somehow!
if (!isset($_GET['package']) || $_GET['package'] == 'index.php' || $_GET['package'] == 'installed.list' || $_GET['package'] == 'server.list')
redirectexit('action=packages;sa=browse');
$_GET['package'] = strtr($_GET['package'], '/', '_');
// Can't delete what's not there.
if (file_exists($boarddir . '/Packages/' . $_GET['package']))
{
@chmod($boarddir . '/Packages/' . $_GET['package'], 0777);
unlink($boarddir . '/Packages/' . $_GET['package']);
}
redirectexit('action=packages;sa=browse');
}
// Browse a list of installed packages.
function PackageBrowse()
{
global $txt, $boarddir, $scripturl, $context, $forum_version;
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=browse',
'name' => &$txt['package3']
);
$context['page_title'] .= ' - ' . $txt['package3'];
$context['sub_template'] = 'browse';
$context['forum_version'] = $forum_version;
$instmods = loadInstalledPackages();
// Look through the list of installed mods...
$installed_mods = array();
foreach ($instmods as $installed_mod)
$installed_mods[$installed_mod['id']] = $installed_mod['version'];
$the_version = strtr($forum_version, array('SMF ' => ''));
// Get a list of all the ids installed, so the latest packages won't include already installed ones.
$context['installed_mods'] = array_keys($installed_mods);
// Empty lists for now.
$context['available_mods'] = array();
$context['available_avatars'] = array();
$context['available_languages'] = array();
$context['available_other'] = array();
$context['available_all'] = array();
if ($dir = @opendir($boarddir . '/Packages'))
{
while ($package = readdir($dir))
{
if (substr($package, -7) != '.tar.gz' && substr($package, -4) != '.zip')
continue;
$packageInfo = getPackageInfo($package);
if ($packageInfo === false)
continue;
$packageInfo['is_installed'] = isset($installed_mods[$packageInfo['id']]);
$packageInfo['is_current'] = $packageInfo['is_installed'] && ($installed_mods[$packageInfo['id']] == $packageInfo['version']);
$packageInfo['is_newer'] = $packageInfo['is_installed'] && ($installed_mods[$packageInfo['id']] > $packageInfo['version']);
$packageInfo['can_install'] = false;
$packageInfo['can_uninstall'] = false;
$packageInfo['can_upgrade'] = false;
// This package is currently NOT installed. Check if it can be.
if (!$packageInfo['is_installed'] && $packageInfo['xml']->exists('install'))
{
// Check if there's an install for *THIS* version of SMF.
$installs = $packageInfo['xml']->set('install');
foreach ($installs as $install)
if (!$install->exists('@for') || matchPackageVersion($the_version, $install->fetch('@for')))
{
// Okay, this one is good to go.
$packageInfo['can_install'] = true;
break;
}
}
// An already installed, but old, package. Can we upgrade it?
elseif ($packageInfo['is_installed'] && !$packageInfo['is_current'] && $packageInfo['xml']->exists('upgrade'))
{
$upgrades = $packageInfo['xml']->set('upgrade');
// First go through, and check against the current version of SMF.
foreach ($upgrades as $upgrade)
{
// Even if it is for this SMF, is it for the installed version of the mod?
if (!$upgrade->exists('@for') || matchPackageVersion($the_version, $upgrade->fetch('@for')))
if (!$upgrade->exists('@from') || matchPackageVersion($installed_mods[$packageInfo['id']], $upgrade->fetch('@from')))
{
$packageInfo['can_upgrade'] = true;
break;
}
}
}
// Note that it has to be the current version to be uninstallable. Shucks.
elseif ($packageInfo['is_installed'] && $packageInfo['is_current'] && $packageInfo['xml']->exists('uninstall'))
{
$uninstalls = $packageInfo['xml']->set('uninstall');
// Can we find any uninstallation methods that work for this SMF version?
foreach ($uninstalls as $uninstall)
if (!$uninstall->exists('@for') || matchPackageVersion($the_version, $uninstall->fetch('@for')))
{
$packageInfo['can_uninstall'] = true;
break;
}
}
// Store a complete list.
$context['available_all'][] = $packageInfo;
// Modification.
if ($packageInfo['type'] == 'modification' || $packageInfo['type'] == 'mod')
$context['available_mods'][] = $packageInfo;
// Avatar package.
elseif ($packageInfo['type'] == 'avatar')
$context['available_avatars'][] = $packageInfo;
// Language package.
elseif ($packageInfo['type'] == 'language')
$context['available_languages'][] = $packageInfo;
// Other stuff.
else
$context['available_other'][] = $packageInfo;
}
closedir($dir);
}
}
function PackageOptions()
{
global $txt, $scripturl, $context, $sourcedir, $modSettings;
if (isset($_POST['submit']))
{
updateSettings(array(
'package_server' => $_POST['pack_server'],
'package_port' => $_POST['pack_port'],
'package_username' => $_POST['pack_user'],
'package_make_backups' => !empty($_POST['package_make_backups'])
));
redirectexit('action=packages;sa=options');
}
if (preg_match('~^/home/([^/]+?)/public_html~', $_SERVER['DOCUMENT_ROOT'], $match))
$default_username = $match[1];
else
$default_username = '';
$context['linktree'][] = array(
'url' => $scripturl . '?action=packages;sa=options',
'name' => &$txt['package_install_options']
);
$context['page_title'] = $txt['package_install_options'];
$context['sub_template'] = 'install_options';
$context['package_ftp_server'] = isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost';
$context['package_ftp_port'] = isset($modSettings['package_port']) ? $modSettings['package_port'] : '21';
$context['package_ftp_username'] = isset($modSettings['package_username']) ? $modSettings['package_username'] : $default_username;
$context['package_make_backups'] = !empty($modSettings['package_make_backups']);
}
?>