/** show an introductory text for backup tool OR stream a ZIP-file to the browser * * If we arrive here via the tools menu, the parameter download is not set. * In that case we show an introductory text with a link that yields a ZIP-file * with the backup. * * If the user follows the download link, we arrive here too but with the download * parameter set. We then dump the database in a variable and subsequently * compress it in a ZIP-file which we stream to the browser. We do code some * things in the basename of the backup: * - the hostname * - the database name * - the database prefix * - the date and the time * which should be enough to distinguish nearly all backups if you happen to have a * lot of different ones. Note that the URL is also encoded as a comment in the .ZIP. * * The parameter download is currently set to 'zip'. However, we do attempt to send the * plain uncompressed data if that parameter is set to 'sql' (quick and dirty). Oh well. * hopefully there is enough memory to accomodate backups of moderate sized sites. * * Note that we need space to compress the data; a informal test yielded that we need * about 160% of the uncompressed size of the backup (tested with a small testset). * Rule of the thumb for memory: the more the merrier but at least twice the size of the * uncompressed backup. * * @param object &$output collects output to show to user * @return output displayed via $output OR ZIP-file streamed to the browser */ function task_backuptool(&$output) { global $DB, $CFG, $WAS_SCRIPT_NAME; $download = get_parameter_string('download', NULL); if (is_null($download)) { $output->set_helptopic('backuptool'); $output->add_content("<h2>" . t('backuptool_header', 'admin') . "</h2>"); $params = array('{DATADIRECTORY}' => htmlspecialchars($CFG->datadir)); $output->add_content(t('backuptool_intro', 'admin', $params)); $output->add_content('<p>'); $parameters = array('job' => JOB_TOOLS, 'task' => TASK_BACKUPTOOL, 'download' => 'zip'); $attributes = array('title' => t('backuptool_download_title', 'admin')); $anchor = t('backuptool_download', 'admin'); $output->add_content(html_a($WAS_SCRIPT_NAME, $parameters, $attributes, $anchor)); show_tools_menu($output, TASK_BACKUPTOOL); return; } // Try to construct a suitable hostname as part of the filename. $host = ''; if (($url = parse_url($CFG->www)) !== FALSE) { $host = isset($url['host']) ? trim(ereg_replace('[^a-zA-Z0-9.-]', '', $url['host']), '.') : ''; } if (empty($host) && isset($_SERVER['SERVER_NAME'])) { $host = trim(ereg_replace('[^a-zA-Z0-9.-]', '', $_SERVER['SERVER_NAME']), '.'); } if (empty($host)) { $host = 'localhost'; // last resort } // basename = host "-" database "-" prefix "-" timestamp (only acceptable chars for filename; strip fancy chars) $basename = $host . '-' . trim(ereg_replace('[^a-zA-Z0-9._-]', '', $CFG->db_name), '-_.') . '-' . trim(ereg_replace('[^a-zA-Z0-9._-]', '', $CFG->prefix), '-_.') . '-' . strftime('%Y%m%d-%H%M%S'); $archive = $basename . '.zip'; $backup = $basename . '.sql'; $data = ''; logger(sprintf('%s(): start creating/streaming backup %s', __FUNCTION__, $basename), WLOG_DEBUG); if ($DB->dump($data)) { $data_length = strlen($data); logger(sprintf('%s(): backup file %s is %d bytes', __FUNCTION__, $backup, $data_length), WLOG_DEBUG); if ($download == 'sql') { header('Content-Type: text/x-sql'); header(sprintf('Content-Disposition: attachment; filename="%s"', $backup)); logger(sprintf('%s(): about to stream %d bytes (backup = %s)', __FUNCTION__, $data_length, $backup), WLOG_DEBUG); echo $data; logger(sprintf('%s(): success streaming data %s (%d bytes uncompressed)', __FUNCTION__, $backup, $data_length)); exit; } else { include_once $CFG->progdir . '/lib/zip.class.php'; $zip = new Zip(); $comment = $CFG->www; if ($zip->OpenZipstream($archive, $comment)) { if ($zip->AddData($data, $backup)) { if ($zip->CloseZip()) { logger(sprintf('%s(): success streaming backup %s (%d bytes uncompressed)', __FUNCTION__, $archive, $data_length)); } else { logger(sprintf('%s(): cannot close zipstream %s: %s', __FUNCTION__, $archive, $zip->Error)); } } else { logger(sprintf('%s(): cannot add backup data to zipstream %s: %s', __FUNCTION__, $archive, $zip->Error)); $zip->CloseZip(); } // ZipStream is done, no point in adding garbage at the end of that file, quit completely exit; } else { logger(sprintf('%s(): cannot open zipstream %s for backup: %s', __FUNCTION__, $archive, $zip->Error)); } } } else { logger(sprintf('%s(): cannot open dump data for backup: %s', __FUNCTION__, db_errormessage())); } $output->add_message(t('backuptool_error', 'admin')); show_tools_menu($output); show_tools_intro($output); }
/** construct a zipfile with the current source and stream it to the visitor * * this routine streams a ZIP-archive to the visitor with either the current * websiteatschool program code or the selected manual. This routine is necessary * to comply with the provisions of the program license which basically says that the * source code of the running program must be made available. * * Note that it is not strictly necessary to also provide the manual, but this * routine can do that nevertheless. * * Note that we take special care not to download the (private) data directory * $CFG->datadir. Of course the datadirectory should live outside the document * root and certainly outside the /program directory tree, but accidents will * happen and we don't want to create a gaping security hole. * * If there are severe errors (e.g. no manual is available for download or an invalid * component was specified) the program exist immediately with a 404 not found error. * Otherwise the ZIP-archive is streamed to the user. If all goes well, we return TRUE, * if there were errors we immediately return TRUE (without finishing the task at hand * other than a perhasp futile attempt to properly close the ZIP-archive). The last * error message from the Zip is logged. * * @param string $component either 'program' or 'manual' or 'languages' * @return void|bool Exit with 404 not found, OR TRUE and generated ZIP-file sent to user OR FALSE on error * @uses download_source_tree() */ function download_source($component) { global $CFG; global $LANGUAGE; $time_start = microtime(); // // Step 0 -- decide what needs to be done // switch ($component) { case 'program': $files = array('index.php', 'admin.php', 'cron.php', 'file.php', 'config-example.php'); $directories = array('program' => $CFG->progdir); $excludes = array(realpath($CFG->datadir), realpath($CFG->progdir . '/manuals')); $archive = 'websiteatschool-program.zip'; break; case 'manual': $language = $LANGUAGE->get_current_language(); $manuals = $CFG->progdir . '/manuals'; if (!is_dir($manuals . '/' . $language)) { if (!is_dir($manuals . '/en')) { logger(sprintf("Failed download 'websiteatschool/manual/%s': 404 Not Found", $language)); error_exit404("websiteatschool/{$component}"); } else { $language = 'en'; } } $files = array(); $directories = array('program/manuals/' . $language => $manuals . '/' . $language); $excludes = array(realpath($CFG->datadir)); $archive = sprintf('websiteatschool-manual-%s.zip', $language); break; case 'languages': $files = array(); $directories = array(); $excludes = array(); $archive = 'websiteatschool-languages.zip'; $languages = $LANGUAGE->retrieve_languages(); foreach ($languages as $language_key => $language) { if (db_bool_is(TRUE, $language['is_active']) && db_bool_is(TRUE, $language['dialect_in_file'])) { $directories['languages/' . $language_key] = $CFG->datadir . '/languages/' . $language_key; } } if (sizeof($directories) < 1) { logger(sprintf("Failed download websiteatschool/%s': 404 Not Found", $component)); error_exit404('websiteatschool/' . $component); } break; default: logger(sprintf("Failed download websiteatschool/%s': 404 Not Found", $component)); error_exit404('websiteatschool/' . $component); break; } // // Step 1 -- setup Ziparchive // include_once $CFG->progdir . '/lib/zip.class.php'; $zip = new Zip(); $comment = $CFG->www; if (!$zip->OpenZipstream($archive, $comment)) { $elapsed = diff_microtime($time_start, microtime()); logger(sprintf("Failed download '%s' (%0.2f seconds): %s", $archive, $elapsed, $zip->Error)); return FALSE; } // // Step 2 -- add files in the root directory (if any) // if (sizeof($files) > 0) { foreach ($files as $file) { $path = $CFG->dir . '/' . $file; if (!file_exists($path)) { logger(sprintf("%s(): missing file '%s' in archive '%s'", __FUNCTION__, $path, $archive), WLOG_DEBUG); $data = sprintf('<' . '?' . 'php echo "%s: file was not found"; ?' . '>', $file); $comment = sprintf('%s: missing', $file); if (!$zip->AddData($data, $file, $comment)) { $elapsed = diff_microtime($time_start, microtime()); logger(sprintf("Failed download '%s' (%0.2f seconds): %s", $archive, $elapsed, $zip->Error)); $zip->CloseZip(); return FALSE; } } else { if (!$zip->AddFile($path, $file)) { $elapsed = diff_microtime($time_start, microtime()); logger(sprintf("Failed download '%s' (%0.2f seconds): %s", $archive, $elapsed, $zip->Error)); $zip->CloseZip(); return FALSE; } } } } // // Step 3 -- add directories to archive // foreach ($directories as $vpath => $path) { if (!download_source_tree($zip, $path, $vpath, $excludes)) { $elapsed = diff_microtime($time_start, microtime()); logger(sprintf("Failed download '%s' (%0.2f seconds): %s", $archive, $elapsed, $zip->Error)); $zip->CloseZip(); return FALSE; } } // // Step 4 -- we're done // if (!$zip->CloseZip()) { $elapsed = diff_microtime($time_start, microtime()); logger(sprintf("Failed download '%s' (%0.2f seconds): %s", $archive, $elapsed, $zip->Error)); return FALSE; } logger(sprintf("Download '%s' (%0.2f seconds): success", $archive, diff_microtime($time_start, microtime()))); return TRUE; }