diff --git a/classes/local/controllers/maintenance_static_page.php b/classes/local/controllers/maintenance_static_page.php new file mode 100644 index 0000000..eebe024 --- /dev/null +++ b/classes/local/controllers/maintenance_static_page.php @@ -0,0 +1,214 @@ +. + +/** + * maintenance_static_page class. + * + * @package auth_outage + * @author Daniel Thee Roperto + * @copyright 2016 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace auth_outage\local\controllers; + +use auth_outage\local\outage; +use coding_exception; +use DOMDocument; +use DOMElement; +use invalid_parameter_exception; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * maintenance_static_page class. + * + * @package auth_outage + * @author Daniel Thee Roperto + * @copyright 2016 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class maintenance_static_page { + /** @var int */ + private static $nextfile = 1; + + /** + * Gets the cli maintenance template file location. + * @return string + */ + public static function get_template_file() { + global $CFG; + return $CFG->dataroot.'/climaintenance.template.html'; + } + + /** + * Gets the resources folder in dataroot. + * + * Warning: this folder will be deleted every time the page is regenerated. + * + * @return string + */ + public static function get_resources_folder() { + global $CFG; + return $CFG->dataroot.'/auth_outage/climaintenance'; + } + + public static function create_from_outage(outage $outage) { + global $CFG; + $html = file_get_contents($CFG->wwwroot.'/auth/outage/info.php?auth_outage_hide_warning=1&id='.$outage->id); + self::create_from_html($html); + } + + public static function create_from_html($html) { + if (!is_string($html)) { + throw new coding_exception('$html is not valid.'); + } + + $dom = new DOMDocument(); + + // Let's assume we have no parsing errors as we cannot rely on a badly-formed page anyway. + libxml_use_internal_errors(true); + $dom->loadHTML($html); + libxml_clear_errors(); + + self::generate($dom); + } + + private static function generate(DOMDocument $dom) { + self::prepare_dataroot(); + self::remove_script_tags($dom); + self::update_link_stylesheet($dom); + self::update_link_favicon($dom); + self::update_images($dom); + file_put_contents(self::get_template_file(), $dom->saveHTML()); + } + + private static function remove_script_tags(DOMDocument $dom) { + $scripts = $dom->getElementsByTagName('script'); + // List items to remove without changing the DOM. + $remove = []; + foreach ($scripts as $node) { + $remove[] = $node; + } + // All listed, now remove them. + foreach ($remove as $node) { + $node->parentNode->removeChild($node); + } + } + + private static function prepare_dataroot() { + $dir = self::get_resources_folder(); + if (is_dir($dir)) { + self::delete_directory_recursively($dir); + } + mkdir($dir, 0775, true); + } + + private static function delete_directory_recursively($dir) { + // It should never come from user, but protect against possible attacks anyway. + $safedir = self::get_resources_folder(); + if (substr($dir, 0, strlen($safedir)) !== $safedir) { + throw new invalid_parameter_exception('Unsafe to delete: '.$dir); + } + + if (!is_dir($dir)) { + throw new coding_exception('Not a directory: '.$dir); + } + $files = scandir($dir); + foreach ($files as $file) { + if (($file == '.') || ($file == '..')) { + continue; + } + $file = $dir.'/'.$file; + if (is_file($file)) { + unlink($file); + continue; + } + if (is_dir($file)) { + self::delete_directory_recursively($file); + continue; + } + throw new coding_exception('Not a file or directory: '.$file); + } + rmdir($dir); + } + + private static function update_link_stylesheet(DOMDocument $dom) { + $links = $dom->getElementsByTagName('link'); + + foreach ($links as $link) { + $rel = $link->getAttribute("rel"); + $href = $link->getAttribute("href"); + if (($rel != 'stylesheet') || ($href == '')) { + continue; + } + $link->setAttribute('href', self::prepare_url($href, 'css')); + } + } + + private static function update_link_favicon(DOMDocument $dom) { + $links = $dom->getElementsByTagName('link'); + + foreach ($links as $link) { + $rel = $link->getAttribute("rel"); + $href = $link->getAttribute("href"); + if (($rel != 'shortcut icon') || ($href == '')) { + continue; + } + $link->setAttribute('href', self::prepare_url($href, 'png')); // Works for most image formats. + } + } + + private static function update_images(DOMDocument $dom) { + $links = $dom->getElementsByTagName('img'); + + foreach ($links as $link) { + $src = $link->getAttribute("src"); + if ($src == '') { + continue; + } + $link->setAttribute('src', self::prepare_url($src, 'png')); // Works for most image formats. + } + } + + private static function prepare_url($url, $type) { + global $CFG; + + if (!preg_match('#^http(s)?://#', $url)) { + debugging('Found a relative url ('.$url.') -- is it using moodle_url()?'); + return $url; // Leave hardcoded URLs as it is. + } + + if (substr($url, 0, strlen($CFG->wwwroot)) !== $CFG->wwwroot) { + return $url; // External URL, leave it. + } + + $file = self::$nextfile++; + if ($type != '') { + $file .= '.'.$type; + } + $path = self::get_resources_folder().'/'.$file; + + // PHPUnit will use www.example.com as wwwroot and we don't to copy the file. + if (!PHPUNIT_TEST) { + copy($url, $path); + } + + $url = (string)new moodle_url('/auth/outage/maintenance.php/'.$file); + return $url; + } +} diff --git a/lang/en/auth_outage.php b/lang/en/auth_outage.php index 0fd726d..a999efc 100644 --- a/lang/en/auth_outage.php +++ b/lang/en/auth_outage.php @@ -80,6 +80,7 @@ $string['finish'] = 'Finish'; $string['info15secondsbefore'] = '15 seconds before'; $string['infoendofoutage'] = 'end of outage'; $string['infofrom'] = 'From:'; +$string['infohidewarning'] = 'no warning bar'; $string['infountil'] = 'Until:'; $string['infostart'] = 'start'; $string['infostartofwarning'] = 'start of warning'; diff --git a/maintenance.php b/maintenance.php new file mode 100644 index 0000000..c84a093 --- /dev/null +++ b/maintenance.php @@ -0,0 +1,33 @@ +. + +/** + * This page is used to regenerate and preview a maintenance mode static page. + * + * @package auth_outage + * @author Daniel Thee Roperto + * @copyright 2016 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use auth_outage\dml\outagedb; +use auth_outage\local\controllers\maintenance_static_page; + +require_once(__DIR__.'/../../config.php'); + +$outage = outagedb::get_next_starting(); +maintenance_static_page::create_from_outage($outage); +readfile(maintenance_static_page::get_template_file()); diff --git a/tests/phpunit/local/controllers/maintenance_static_page_test.php b/tests/phpunit/local/controllers/maintenance_static_page_test.php new file mode 100644 index 0000000..e5a8d35 --- /dev/null +++ b/tests/phpunit/local/controllers/maintenance_static_page_test.php @@ -0,0 +1,124 @@ +. + +/** + * maintenance_static_page_test task class. + * + * @package auth_outage + * @author Daniel Thee Roperto + * @copyright 2016 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use auth_outage\local\controllers\maintenance_static_page; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__.'/../../base_testcase.php'); + +/** + * maintenance_static_page_test class. + * + * @package auth_outage + * @author Daniel Thee Roperto + * @copyright 2016 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @SuppressWarnings(public) Allow as many methods as needed. + */ +class maintenance_static_page_test extends auth_outage_base_testcase { + /** + * Ensures the template file does not exist when starting a test. + */ + public function setUp() { + $file = maintenance_static_page::get_template_file(); + if (file_exists($file)) { + if (is_file($file)) { + unlink($file); + } else { + self::fail('Invalid temp file: '.$file); + } + } + } + + public function test_templatefile() { + global $CFG; + self::assertSame($CFG->dataroot.'/climaintenance.template.html', maintenance_static_page::get_template_file()); + } + + public function test_createfromoutage() { + // How to fetch a page from PHPUnit environment? + } + + public function test_createfromhtml() { + $html = "\nTitleContent"; + maintenance_static_page::create_from_html($html); + $generated = trim(file_get_contents(maintenance_static_page::get_template_file())); + self::assertSame($html, $generated); + } + + public function test_removescripttags() { + $html = "\n". + 'Title'. + 'Content'; + maintenance_static_page::create_from_html($html); + + $generated = file_get_contents(maintenance_static_page::get_template_file()); + self::assertNotContains('Title'. + 'Content'; + maintenance_static_page::create_from_html($html); + $generated = file_get_contents(maintenance_static_page::get_template_file()); + + self::assertContains('http://www.example.com/moodle/auth/outage/maintenance.php/', $generated); + self::assertNotContains($link1, $generated); + self::assertNotContains($link2, $generated); + self::assertContains($link3, $generated); + } + + public function test_updateimages() { + $link1 = (string)new moodle_url('/example.png'); + $link2 = (string)new moodle_url('/auth/outage/imagefile'); + $link3 = 'http://google.com/coolstyle.css'; + $html = "\n". + 'an imageTitle'. + 'Content'; + maintenance_static_page::create_from_html($html); + $generated = file_get_contents(maintenance_static_page::get_template_file()); + + self::assertContains('http://www.example.com/moodle/auth/outage/maintenance.php/', $generated); + self::assertNotContains($link1, $generated); + self::assertNotContains($link2, $generated); + self::assertContains($link3, $generated); + } + + public function test_updatelinkfavicon() { + $link = (string)new moodle_url('/favicon.jpg'); + $html = "\n". + 'Title'. + 'Content'; + maintenance_static_page::create_from_html($html); + $generated = file_get_contents(maintenance_static_page::get_template_file()); + + self::assertNotContains($link, $generated); + self::assertContains('http://www.example.com/moodle/auth/outage/maintenance.php/', $generated); + } +}