. * * @package mahara * @subpackage export-html * @author Catalyst IT Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL * @copyright (C) 2006-2009 Catalyst IT Ltd http://catalyst.net.nz * */ defined('INTERNAL') || die(); /** * HTML export plugin */ class PluginExportHtml extends PluginExport { /** * name of resultant zipfile */ protected $zipfile; /** * The name of the directory under which all the other directories and * files will be placed in the export */ protected $rootdir; /** * List of directories of static files provided by artefact plugins */ private $pluginstaticdirs = array(); /** * List of stylesheets to include in the export. * * This is keyed by artefact plugin name, the empty string key contains * stylesheets that will be included on all pages. */ private $stylesheets = array('' => array()); /** * Whether the user requested to export just one view. In this case, * the generated export doesn't have the home page - just the View is * exported (plus artefacts of course) */ protected $exportingoneview = false; /** * constructor. overrides the parent class * to set up smarty and the attachment directory */ public function __construct(User $user, $views, $artefacts, $progresscallback=null) { global $THEME; parent::__construct($user, $views, $artefacts, $progresscallback); $this->rootdir = 'portfolio-for-' . self::text_to_path($user->get('username')); // Create basic required directories foreach (array('files', 'views', 'static', 'static/smilies', 'static/profileicons') as $directory) { $directory = "{$this->exportdir}/{$this->rootdir}/{$directory}/"; if (!check_dir_exists($directory)) { throw new SystemException("Couldn't create the temporary export directory $directory"); } } $this->zipfile = 'mahara-export-html-user' . $this->get('user')->get('id') . '-' . $this->exporttime . '.zip'; // Find what stylesheets need to be included $themedirs = $THEME->get_path('', true, 'export/html'); $stylesheets = array('style.css', 'print.css'); foreach ($themedirs as $theme => $themedir) { foreach ($stylesheets as $stylesheet) { if (is_readable($themedir . 'style/' . $stylesheet)) { array_unshift($this->stylesheets[''], 'theme/' . $theme . '/static/style/' . $stylesheet); } } } $this->exportingoneview = ( $this->viewexportmode == PluginExport::EXPORT_LIST_OF_VIEWS && $this->artefactexportmode == PluginExport::EXPORT_ARTEFACTS_FOR_VIEWS && count($this->views) == 1 ); $this->notify_progress_callback(15, 'Setup complete'); } public static function get_title() { return get_string('title', 'export.html'); } public static function get_description() { return get_string('description', 'export.html'); } /** * Main export routine */ public function export() { global $THEME; raise_memory_limit('128M'); $summaries = array(); $plugins = plugins_installed('artefact', true); $exportplugins = array(); $progressstart = 15; $progressend = 25; $plugincount = count($plugins); // First pass: find out which plugins are exporting like us, and ask // them about the static data they want to include in every template $i = 0; foreach ($plugins as $plugin) { $plugin = $plugin->name; $this->notify_progress_callback(intval($progressstart + (++$i / $plugincount) * ($progressend - $progressstart)), 'Preparing ' . $plugin); if (safe_require('export', 'html/' . $plugin, 'lib.php', 'require_once', true)) { $exportplugins[] = $plugin; $classname = 'HtmlExport' . ucfirst($plugin); if (!is_subclass_of($classname, 'HtmlExportArtefactPlugin')) { throw new SystemException("Class $classname does not extend HtmlExportArtefactPlugin as it should"); } safe_require('artefact', $plugin); // Find out whether the plugin has static data for us $themestaticdirs = array_reverse($THEME->get_path('', true, 'artefact/' . $plugin . '/export/html')); foreach ($themestaticdirs as $dir) { $staticdir = substr($dir, strlen(get_config('docroot') . 'artefact/')); $this->pluginstaticdirs[] = $staticdir; foreach (array('style.css', 'print.css') as $stylesheet) { if (is_readable($dir . 'style/' . $stylesheet)) { $this->stylesheets[$plugin][] = str_replace('export/html/', '', $staticdir) . 'style/' . $stylesheet; } } } } } // Second pass: actually dump data for active export plugins $progressstart = 25; $progressend = 50; $i = 0; foreach ($exportplugins as $plugin) { $this->notify_progress_callback(intval($progressstart + (++$i / $plugincount) * ($progressend - $progressstart)), 'Exporting data for ' . $plugin); $classname = 'HtmlExport' . ucfirst($plugin); $artefactexporter = new $classname($this); $artefactexporter->dump_export_data(); // If just exporting a list of views, we don't care about the summaries for each artefact plugin if (!($this->viewexportmode == PluginExport::EXPORT_LIST_OF_VIEWS && $this->artefactexportmode == PluginExport::EXPORT_ARTEFACTS_FOR_VIEWS)) { $summaries[$plugin] = array($artefactexporter->get_summary_weight(), $artefactexporter->get_summary()); } } // Get the view data $this->notify_progress_callback(55, 'Exporting Views'); $this->dump_view_export_data(); if (!$this->exportingoneview) { $summaries['view'] = array(100, $this->get_view_summary()); // Sort by weight (then drop the weight information) $this->notify_progress_callback(75, 'Building index page'); uasort($summaries, create_function('$a, $b', 'return $a[0] > $b[0];')); foreach ($summaries as &$summary) { $summary = $summary[1]; } // Build index.html $this->build_index_page($summaries); } // Copy all static files into the export $this->notify_progress_callback(80, 'Copying extra files'); $this->copy_static_files(); // Copy all resized images that were found while rewriting the HTML $copyproxy = HtmlExportCopyProxy::singleton(); $copydata = $copyproxy->get_copy_data(); foreach ($copydata as $from => $to) { if (!copy($from, $this->get('exportdir') . '/' . $this->get('rootdir') . $to)) { throw new SystemException("Could not copy static file $from"); } } // zip everything up $this->notify_progress_callback(90, 'Creating zipfile'); $cwd = getcwd(); $command = sprintf('%s %s %s %s', get_config('pathtozip'), get_config('ziprecursearg'), escapeshellarg($this->exportdir . $this->zipfile), escapeshellarg($this->rootdir) ); $output = array(); chdir($this->exportdir); exec($command, $output, $returnvar); chdir($cwd); if ($returnvar != 0) { throw new SystemException('Failed to zip the export file'); } $this->notify_progress_callback(100, 'Done'); return $this->zipfile; } public function cleanup() { // @todo remove temporary files and directories // @todo maybe move the zip file somewhere else - like to files/export or something } public function get_smarty($rootpath='', $section='') { if ($section && isset($this->stylesheets[$section])) { $stylesheets = array_merge($this->stylesheets[''], $this->stylesheets[$section]); } else { $stylesheets = $this->stylesheets['']; } $smarty = smarty_core(); $smarty->assign('user', $this->get('user')); $smarty->assign('rootpath', $rootpath); $smarty->assign('export_time', $this->exporttime); $smarty->assign('sitename', get_config('sitename')); $smarty->assign('stylesheets', $stylesheets); $smarty->assign('maharalogo', $rootpath . $this->theme_path('images/logo.png')); return $smarty; } /** * Converts a relative path to a static file that the HTML export theme * should have, to a path in the static export where the file will reside. * * This returns the path in the most appropriate theme. */ private function theme_path($path) { global $THEME; $themestaticdirs = $THEME->get_path('', true, 'export/html'); foreach ($themestaticdirs as $theme => $dir) { if (is_readable($dir . $path)) { return 'static/theme/' . $theme . '/static/' . $path; } } } /** * Converts the passed text into a a form that could be used in a URL. * * @param string $text The text to convert * @return string The converted text */ public static function text_to_path($text) { return substr(preg_replace('#[^a-zA-Z0-9_-]+#', '-', $text), 0, 255); } /** * Sanitises a string meant to be used as a filesystem path. * * Mahara allows file/folder artefact names to have slashes in them, which * aren't legal on most real filesystems. */ public static function sanitise_path($path) { return substr(str_replace('/', '_', $path), 0, 255); } private function build_index_page($summaries) { $smarty = $this->get_smarty(); $smarty->assign('page_heading', full_name($this->get('user'))); $smarty->assign('summaries', $summaries); $content = $smarty->fetch('export:html:index.tpl'); if (!file_put_contents($this->exportdir . '/' . $this->rootdir . '/index.html', $content)) { throw new SystemException("Could not create index.html for the export"); } } /** * Dumps all views into the HTML export */ private function dump_view_export_data() { $progressstart = 55; $progressend = 75; $i = 0; $viewcount = count($this->views); $rootpath = ($this->exportingoneview) ? './' : '../../'; $smarty = $this->get_smarty($rootpath); foreach ($this->views as $viewid => $view) { $this->notify_progress_callback(intval($progressstart + (++$i / $viewcount) * ($progressend - $progressstart)), "Exporting Views ($i/$viewcount)"); $smarty->assign('page_heading', $view->get('title')); $smarty->assign('viewdescription', $view->get('description')); if ($this->exportingoneview) { $smarty->assign('nobreadcrumbs', true); $directory = $this->exportdir . '/' . $this->rootdir; } else { $smarty->assign('breadcrumbs', array( array('text' => get_string('Views', 'view')), array('text' => $view->get('title'), 'path' => 'index.html'), )); $directory = $this->exportdir . '/' . $this->rootdir . '/views/' . self::text_to_path($view->get('title')); if (!check_dir_exists($directory)) { throw new SystemException("Could not create directory for view $viewid"); } } $outputfilter = new HtmlExportOutputFilter($rootpath); $smarty->assign('view', $outputfilter->filter($view->build_columns())); $content = $smarty->fetch('export:html:view.tpl'); if (!file_put_contents("$directory/index.html", $content)) { throw new SystemException("Could not write view page for view $viewid"); } } } private function get_view_summary() { $smarty = $this->get_smarty('../'); $views = array(); foreach ($this->views as $view) { if ($view->get('type') != 'profile') { $views[] = array( 'title' => $view->get('title'), 'folder' => self::text_to_path($view->get('title')), ); } } $smarty->assign('views', $views); if ($views) { $stryouhaveviews = (count($views) == 1) ? get_string('youhaveoneview', 'view') : get_string('youhaveviews', 'view', count($views)); } else { $stryouhaveviews = get_string('youhavenoviews', 'view'); } $smarty->assign('stryouhaveviews', $stryouhaveviews); return array( 'title' => get_string('Views', 'view'), 'description' => $smarty->fetch('export:html:viewsummary.tpl'), ); } /** * Copies the static files (stylesheets etc.) into the export */ private function copy_static_files() { global $THEME; require_once('file.php'); $staticdir = $this->get('exportdir') . '/' . $this->get('rootdir') . '/static/'; $directoriestocopy = array(); // Get static directories from each theme for HTML export $themestaticdirs = $THEME->get_path('', true, 'export/html'); foreach ($themestaticdirs as $theme => $dir) { $themedir = $staticdir . 'theme/' . $theme . '/static/'; $directoriestocopy[$dir] = $themedir; if (!check_dir_exists($themedir)) { throw new SystemException("Could not create theme directory for theme $theme"); } } // Smilies $directoriestocopy[get_config('docroot') . 'js/tinymce/plugins/emotions/img'] = $staticdir . 'smilies/'; $filestocopy = array( get_config('docroot') . 'theme/views.css' => $staticdir . 'views.css', ); foreach ($this->pluginstaticdirs as $dir) { $destinationdir = str_replace('export/html/', '', $dir); if (!check_dir_exists($staticdir . $destinationdir)) { throw new SystemException("Could not create static directory $destinationdir"); } $directoriestocopy[get_config('docroot') . 'artefact/' . $dir] = $staticdir . $destinationdir; } foreach ($directoriestocopy as $from => $to) { if (!copyr($from, $to)) { throw new SystemException("Could not copy $from to $to"); } } foreach ($filestocopy as $from => $to) { if (!copy($from, $to)) { throw new SystemException("Could not copy static file $from"); } } } } abstract class HtmlExportArtefactPlugin { protected $exporter; protected $fileroot; public function __construct(PluginExportHTML $exporter) { $this->exporter = $exporter; $pluginname = strtolower(substr(get_class($this), strlen('HtmlExport'))); $this->fileroot = $this->exporter->get('exportdir') . '/' . $this->exporter->get('rootdir') . '/files/' . $pluginname . '/'; if (!check_dir_exists($this->fileroot)) { throw new SystemException("Could not create the temporary export directory $this->fileroot"); } } abstract public function dump_export_data(); abstract public function get_summary(); abstract public function get_summary_weight(); } /** * Provides a mechanism for converting the HTML generated by views and * artefacts for the HTML export. * * Mostly, this means rewriting links to artefacts to point the correct place * in the export. */ class HtmlExportOutputFilter { /** * The relative path to the root of the generated export - used for link munging */ private $basepath = ''; /** * A cache of view titles. See replace_view_link() */ private $viewtitles = array(); /** * A cache of folder data. See get_folder_path_for_file() */ private $folderdata = null; /** */ private $htmlexportcopyproxy = null; /** * @param string $basepath The relative path to the root of the generated export */ public function __construct($basepath) { $this->basepath = preg_replace('#/$#', '', $basepath); $this->htmlexportcopyproxy = HtmlExportCopyProxy::singleton(); } /** * Filters the given HTML for HTML export purposes * * @param string $html The HTML to filter * @return string The filtered HTML */ public function filter($html) { $wwwroot = preg_quote(get_config('wwwroot')); $html = preg_replace( array( // We don't care about javascript '#]*>.*?#si', // Fix simlies from tinymce '#]*)src="(' . $wwwroot . ')?/?js/tinymce/plugins/emotions/img/([^"]+)"([^>]+)>#', // No forms '#]*>.*?#si', // Gratuitous hack for the RSS blocktype '#
[^<]*
#', ), array( '', '', '', '', ), $html ); // Links to views $html = preg_replace_callback( '#' . $wwwroot . 'view/view\.php\?id=(\d+)#', array($this, 'replace_view_link'), $html ); // Links to artefacts $html = preg_replace_callback( '#]+href="(' . preg_quote(get_config('wwwroot')) . ')?/?view/artefact\.php\?artefact=(\d+)(&view=\d+)?(&page=\d+)?"[^>]*>([^<]*)#', array($this, 'replace_artefact_link'), $html ); // Links to download files $html = preg_replace_callback( '#(' . preg_quote(get_config('wwwroot')) . ')?/?artefact/file/download\.php\?file=(\d+)((&[a-z]+=[x0-9]+)+)*#', array($this, 'replace_download_link'), $html ); // Thumbnails require_once('file.php'); $html = preg_replace_callback( '#(' . preg_quote(get_config('wwwroot')) . ')?/?thumb\.php\?type=([a-z]+)((&[a-z]+=[x0-9]+)+)*#', array($this, 'replace_thumbnail_link'), $html ); // Images out of the theme directory $html = preg_replace_callback( '#(' . preg_quote(get_config('wwwroot')) . ')?/?theme/' . get_config('theme') . '/static/images/([a-z0-9_.-]+)#', array($this, 'replace_theme_image_link'), $html ); return $html; } /** * Callback to replace links to views to point to the correct location in * the HTML export */ private function replace_view_link($matches) { $viewid = $matches[1]; if (!isset($this->viewtitles[$viewid])) { $this->viewtitles[$viewid] = PluginExportHtml::text_to_path(get_field('view', 'title', 'id', $viewid)); } return $this->basepath . '/views/' . $this->viewtitles[$viewid] . '/index.html'; } /** * Callback to replace links to artefact to point to the correct location * in the HTML export */ private function replace_artefact_link($matches) { $artefactid = $matches[2]; $artefact = artefact_instance_from_id($artefactid); switch ($artefact->get('artefacttype')) { case 'blog': $page = ($matches[4]) ? intval(substr($matches[4], strlen('&page='))) : 1; $page = ($page == 1) ? 'index' : $page; return '' . $matches[5] . ''; case 'file': case 'folder': case 'image': case 'archive': $folderpath = $this->get_folder_path_for_file($artefact); return '' . $matches[5] . ''; default: return $matches[5]; } } /** * Callback to replace links to artefact/file/download.php to point to the * correct file in the HTML export */ private function replace_download_link($matches) { $artefactid = $matches[2]; $artefact = artefact_instance_from_id($artefactid); // If artefact type not something that would be served by download.php, // replace link with nothing if ($artefact->get_plugin_name() != 'file') { return ''; } $options = array(); if (isset($matches[3])) { $parts = explode('&', substr($matches[3], 5)); foreach ($parts as $part) { list($key, $value) = explode('=', $part); $options[$key] = $value; } } $folderpath = $this->get_folder_path_for_file($artefact); return $this->get_export_path_for_file($artefact, $options, '/files/file/' . $folderpath); } /** * Callback to replace links to thumb.php to point to the correct file in * the HTML export */ private function replace_thumbnail_link($matches) { if (isset($matches[3])) { $type = $matches[2]; $parts = explode('&', substr($matches[3], 5)); foreach ($parts as $part) { list($key, $value) = explode('=', $part); $options[$key] = $value; } if (!isset($options['id'])) { return ''; } switch ($type) { case 'profileicon': // Convert the user ID to a profile icon ID if (!$options['id'] = get_field_sql('SELECT profileicon FROM {usr} WHERE id = ?', array($options['id']))) { // No profile icon, get the default one list($size, $prefix) = $this->get_size_from_options($options); if ($from = get_dataroot_image_path('artefact/file/profileicons/no_userphoto/' . get_config('theme'), 0, $size)) { $to = '/static/profileicons/0-' . $prefix . 'no_userphoto.png'; $this->htmlexportcopyproxy->add($from, $to); } return $this->basepath . $to; } case 'profileiconbyid': $icon = artefact_instance_from_id($options['id']); if ($icon->get_plugin_name() != 'file') { return ''; } $folderpath = $this->get_folder_path_for_file($icon); return $this->get_export_path_for_file($icon, $options, '/static/profileicons/'); default: return ''; } } return ''; } /** * Callback */ private function replace_theme_image_link($matches) { $file = '/theme/' . get_config('theme') . '/static/images/' . $matches[2]; $this->htmlexportcopyproxy->add( get_config('docroot') . $file, '/static/' . $file ); return $this->basepath . '/static/' . $file; } /** * Given a file, returns the folder path for it in the Mahara files area * * The path is pre-sanitised so it can be used when generating the export * * @param $file The file or folder to get the folder path for * @return string */ private function get_folder_path_for_file($file) { if ($this->folderdata === null) { $this->folderdata = get_records_select_assoc('artefact', "artefacttype = 'folder' AND owner = ?", array($file->get('owner'))); if ($this->folderdata) { foreach ($this->folderdata as &$folder) { $folder->title = PluginExportHtml::sanitise_path($folder->title); } } } $folderpath = ArtefactTypeFileBase::get_full_path($file->get('parent'), $this->folderdata); return $folderpath; } /** * Generates a path, relative to the root of the export, that the given * file will appear in the export. * * If the file is a thumbnail, the copy proxy is informed about it so that * the image can later be copied in to place. * * @param ArtefactTypeFileBase $file The file to get the exported path for * @param array $options Options from the URL that was linking * to the image - most importantly, size * related options about how the image * was thumbnailed, if it was. * @param string $basefolder What folder in the export to dump the * file in * @return string The relative path to where the file * will be placed */ private function get_export_path_for_file(ArtefactTypeFileBase $file, array $options, $basefolder) { unset($options['view']); $prefix = ''; if ($options) { list($size, $prefix) = $this->get_size_from_options($options); $from = $file->get_path($size); $to = $basefolder . $file->get('id') . '-' . $prefix . PluginExportHtml::sanitise_path($file->get('title')); $this->htmlexportcopyproxy->add($from, $to); } else { $to = $basefolder . PluginExportHtml::sanitise_path($file->get('title')); } return $this->basepath . $to; } /** * Helper method */ private function get_size_from_options($options) { $prefix = ''; foreach (array('size', 'width', 'height', 'maxsize', 'maxwidth', 'maxheight') as $param) { if (isset($options[$param])) { $$param = $options[$param]; $prefix .= $param . '-' . $options[$param] . '-'; } else { $$param = null; } } return array(imagesize_data_to_internal_form($size, $width, $height, $maxsize, $maxwidth, $maxheight), $prefix); } } /** * Gathers a list of files that need to be copied into the export, as they're * found by the HtmlExportOutputFilter */ class HtmlExportCopyProxy { private static $instance = null; private $copy = array(); private function __construct() { } public static function singleton() { if (is_null(self::$instance)) { self::$instance = new HtmlExportCopyProxy(); } return self::$instance; } public function add($from, $to) { $this->copy[$from] = $to; } public function get_copy_data() { return $this->copy; } } ?>