Refactored maintenance_static_page.php due to high complexity (more than 50).

This commit is contained in:
Daniel Thee Roperto
2016-11-10 19:06:18 +11:00
parent af2e76e972
commit 33be3eef86
4 changed files with 524 additions and 332 deletions

View File

@@ -28,10 +28,6 @@ namespace auth_outage\local\controllers;
use auth_outage\local\outage;
use coding_exception;
use DOMDocument;
use finfo;
use invalid_parameter_exception;
use invalid_state_exception;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
@@ -62,7 +58,7 @@ class maintenance_static_page {
} else if (PHPUNIT_TEST) {
$html = '<html></html>';
} else {
$data = self::file_get_data($CFG->wwwroot.'/auth/outage/info.php?auth_outage_hide_warning=1&id='.$outage->id);
$data = maintenance_static_page_io::file_get_data($CFG->wwwroot.'/auth/outage/info.php?auth_outage_hide_warning=1&id='.$outage->id);
$html = $data['contents'];
}
@@ -94,331 +90,30 @@ class maintenance_static_page {
return new maintenance_static_page($dom);
}
/**
* Tries to get the contents of the file or URL.
* @param string $file File to get.
* @return string Contents of $file or an empty string if failed.
*/
private static function file_get_data($file) {
if (self::is_url($file)) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $file);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$contents = curl_exec($curl);
$mime = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
curl_close($curl);
} else {
$contents = @file_get_contents($file);
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($contents); // Not perfect, but try guessing it.
}
if ($contents === false) {
debugging('Cannot fetch: '.$file);
$contents = '';
$mime = 'unknown';
}
return ['contents' => $contents, 'mime' => $mime];
}
/**
* Checks if the given string starts with "http://" or "https://".
* @param $url
* @return bool
*/
private static function is_url($url) {
return (bool)preg_match('#^http(s)?://#', $url);
}
/** @var DOMDocument */
protected $dom;
/** @var bool */
protected $preview = false;
/** @var maintenance_static_page_generator */
protected $generator;
/**
* maintenance_static_page constructor.
* @param DOMDocument|null $dom
* @throws coding_exception
*/
public function __construct($dom) {
if (!is_null($dom) && !($dom instanceof DOMDocument)) {
throw new coding_exception('$dom must be null or an DOMDocument object.');
}
$this->dom = $dom;
protected function __construct($dom) {
$io = new maintenance_static_page_io();
$this->generator = new maintenance_static_page_generator($dom, $io);
}
/**
* Gets the cli maintenance template file location.
* @return string
*/
public function get_template_file() {
global $CFG;
if ($this->preview) {
return $this->get_resources_folder().'/climaintenance.html';
} else {
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 function get_resources_folder() {
global $CFG;
// If you change the path, also change file auth/outage/bootstrap.php as it does not use this reference.
$dir = $CFG->dataroot.'/auth_outage/climaintenance';
if ($this->preview) {
$dir = $dir.'/preview';
}
return $dir;
}
/**
* Generates the page.
* Requests to generate the static page.
*/
public function generate() {
$this->cleanup();
if (!is_null($this->dom)) {
$this->remove_script_tags();
$this->update_link_stylesheet();
$this->update_link_favicon();
$this->update_images();
$html = $this->dom->saveHTML();
if (trim($html) == '') {
// Should never happen, but just in case...
throw new invalid_state_exception('Sanity check failed, $html is empty.');
}
file_put_contents($this->get_template_file(), $html);
}
$this->generator->generate();
}
/**
* @param boolean $preview
* @return maintenance_static_page
* @return maintenance_static_page_io
*/
public function set_preview($preview) {
$this->preview = $preview;
return $this;
}
/**
* Remove script tags from DOM.
*/
private function remove_script_tags() {
$scripts = $this->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);
}
}
/**
* Clean up the dataroot as needed.
*/
private function cleanup() {
$resources = $this->get_resources_folder();
if (is_dir($resources)) {
$this->delete_directory_recursively($resources);
}
$template = $this->get_template_file();
if (is_file($template)) {
unlink($template);
}
if (!is_null($this->dom)) {
mkdir($resources, 0775, true);
}
}
/**
* Deletes the given directory with all its files and subdirectories.
* @param string $dir Directory to delete.
* @throws coding_exception
* @throws invalid_parameter_exception
*/
private function delete_directory_recursively($dir) {
// It should never come from user, but protect against possible attacks anyway.
$dir = realpath($dir);
$safedir = $this->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)) {
$this->delete_directory_recursively($file);
continue;
}
throw new coding_exception('Not a file or directory: '.$file);
}
rmdir($dir);
}
/**
* Fetch and fixes all link rel="stylesheet" tags.
*/
private function update_link_stylesheet() {
$links = $this->dom->getElementsByTagName('link');
foreach ($links as $link) {
$rel = $link->getAttribute("rel");
$href = $link->getAttribute("href");
if (($rel != 'stylesheet') || ($href == '')) {
continue;
}
$saved = $this->save_url_file($href);
if (is_null($saved['url'])) {
$url = $href; // Skipped, use original URL.
} else {
$this->update_link_stylesheet_parse($saved['file'], dirname($href));
$url = $this->get_url_for_file($saved['url']);
}
$link->setAttribute('href', $url);
}
}
/**
* Checks for urls inside filename.
* @param string $filename
*/
private function update_link_stylesheet_parse($filename, $baseref) {
global $CFG;
$contents = file_get_contents($filename);
if (!preg_match_all('#url\([\'"]?([^\'"\)]+)#', $contents, $matches)) {
return;
}
foreach ($matches[1] as $original_url) {
// Allow incomplete URLs in CSS, assume it is from moodle root.
if (self::is_url($original_url)) {
$full_url = $original_url;
} else if ($original_url[0] == '/') {
$full_url = $CFG->wwwroot.$original_url;
} else {
$full_url = $baseref.'/'.$original_url;
}
$saved = $this->save_url_file($full_url);
if (!is_null($saved)) {
$final_url = $this->get_url_for_file($saved['url']);
$contents = str_replace($original_url, $final_url, $contents);
}
}
file_put_contents($filename, $contents);
}
/**
* Fetch and fixes the favicon link tag.
*/
private function update_link_favicon() {
$links = $this->dom->getElementsByTagName('link');
foreach ($links as $link) {
$rel = $link->getAttribute("rel");
$href = $link->getAttribute("href");
if (($rel != 'shortcut icon') || ($href == '')) {
continue;
}
$link->setAttribute('href', $this->generate_file_url($href)); // Works for most image formats.
}
}
/**
* Fetch and fixes all img tags.
*/
private function update_images() {
$links = $this->dom->getElementsByTagName('img');
foreach ($links as $link) {
$src = $link->getAttribute("src");
if ($src == '') {
continue;
}
$link->setAttribute('src', $this->generate_file_url($src)); // Works for most image formats.
}
}
/**
* Saves the content of the URL into a file, returning the new URL.
* @param string $url Input URL.
* @return string Output URL.
*/
private function generate_file_url($url) {
$saved = $this->save_url_file($url);
if (is_null($saved)) {
return $url; // Skipped, use original URL.
}
return $this->get_url_for_file($saved['url']);
}
/**
* Creates a URL for a resource file.
* @param string $filename
* @return string
*/
private function get_url_for_file($filename) {
return (string)new moodle_url('/auth/outage/file.php', ['file' => $filename]);
}
/**
* Saves the content of the URL into a file, returning the local filename.
* @param string $url Input URL.
* @return string|null Output filename or null if skipped.
*/
private function save_url_file($url) {
global $CFG;
if (!self::is_url($url)) {
debugging('Found a relative url ('.$url.') -- is it using moodle_url()?');
return null; // Leave hardcoded URLs as it is.
}
if (substr($url, 0, strlen($CFG->wwwroot)) !== $CFG->wwwroot) {
return null; // External URL, leave it.
}
// PHPUnit does not expose a web interface to fetch, point to local file instead.
if (PHPUNIT_TEST) {
$url = str_replace($CFG->wwwroot, $CFG->dirroot, $url);
}
$data = self::file_get_data($url);
$mime = trim(base64_encode($data['mime']), '=');
$url = sha1($data['contents']).'.'.$mime;
$filepath = $this->get_resources_folder().'/'.$url;
file_put_contents($filepath, $data['contents']);
if ($this->preview) {
$url = 'preview/'.$url;
}
return ['file' => $filepath, 'url' => $url];
public function get_io() {
return $this->generator->get_io();
}
}

View File

@@ -0,0 +1,200 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* maintenance_static_page_generator class.
*
* @package auth_outage
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2016 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace auth_outage\local\controllers;
use coding_exception;
use DOMDocument;
use DOMElement;
use invalid_state_exception;
defined('MOODLE_INTERNAL') || die();
/**
* maintenance_static_page_generator class.
*
* @package auth_outage
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2016 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class maintenance_static_page_generator {
/** @var DOMDocument */
protected $dom;
/** @var maintenance_static_page_io */
protected $io;
/**
* maintenance_static_page_generator constructor.
* @param DOMDocument|null $dom
* @param maintenance_static_page_io $io
* @throws coding_exception
*/
public function __construct($dom, maintenance_static_page_io $io) {
if (!is_null($dom) && !($dom instanceof DOMDocument)) {
throw new coding_exception('$dom must be null or an DOMDocument object.');
}
$this->dom = $dom;
$this->io = $io;
}
/**
* Generates the page.
*/
public function generate() {
$this->io->cleanup();
if (!is_null($this->dom)) {
$this->io->create_resources_path();
$this->remove_script_tags();
$this->update_link_stylesheet();
$this->update_link_favicon();
$this->update_images();
$html = $this->dom->saveHTML();
if (trim($html) == '') {
// Should never happen, but just in case...
throw new invalid_state_exception('Sanity check failed, $html is empty.');
}
$this->io->save_template_file($html);
}
}
/**
* @return maintenance_static_page_io
*/
public function get_io() {
return $this->io;
}
/**
* Remove script tags from DOM.
*/
private function remove_script_tags() {
$scripts = $this->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);
}
}
/**
* Fetch and fixes all link rel="stylesheet" tags.
*/
private function update_link_stylesheet() {
$links = $this->dom->getElementsByTagName('link');
/** @var DOMElement $link */
foreach ($links as $link) {
$rel = $link->getAttribute("rel");
$href = $link->getAttribute("href");
if (($rel != 'stylesheet') || ($href == '')) {
continue;
}
$saved = $this->io->save_url_file($href);
if (is_null($saved['url'])) {
$url = $href; // Skipped, use original URL.
} else {
$this->update_link_stylesheet_parse($saved['file'], dirname($href));
$url = $this->io->get_url_for_file($saved['url']);
}
$link->setAttribute('href', $url);
}
}
/**
* Checks for urls inside filename.
* @param string $filename
*/
private function update_link_stylesheet_parse($filename, $baseref) {
global $CFG;
$contents = file_get_contents($filename);
if (!preg_match_all('#url\([\'"]?([^\'"\)]+)#', $contents, $matches)) {
return;
}
foreach ($matches[1] as $original_url) {
// Allow incomplete URLs in CSS, assume it is from moodle root.
if (maintenance_static_page_io::is_url($original_url)) {
$full_url = $original_url;
} else if ($original_url[0] == '/') {
$full_url = $CFG->wwwroot.$original_url;
} else {
$full_url = $baseref.'/'.$original_url;
}
$saved = $this->io->save_url_file($full_url);
if (!is_null($saved)) {
$final_url = $this->io->get_url_for_file($saved['url']);
$contents = str_replace($original_url, $final_url, $contents);
}
}
file_put_contents($filename, $contents);
}
/**
* Fetch and fixes the favicon link tag.
*/
private function update_link_favicon() {
$links = $this->dom->getElementsByTagName('link');
/** @var DOMElement $link */
foreach ($links as $link) {
$rel = $link->getAttribute("rel");
$href = $link->getAttribute("href");
if (($rel != 'shortcut icon') || ($href == '')) {
continue;
}
$link->setAttribute('href', $this->io->generate_file_url($href)); // Works for most image formats.
}
}
/**
* Fetch and fixes all img tags.
*/
private function update_images() {
$links = $this->dom->getElementsByTagName('img');
/** @var DOMElement $link */
foreach ($links as $link) {
$src = $link->getAttribute("src");
if ($src == '') {
continue;
}
$link->setAttribute('src', $this->io->generate_file_url($src)); // Works for most image formats.
}
}
}

View File

@@ -0,0 +1,252 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* maintenance_static_page_io class.
*
* @package auth_outage
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2016 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace auth_outage\local\controllers;
use coding_exception;
use finfo;
use invalid_parameter_exception;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
/**
* maintenance_static_page_io class.
*
* @package auth_outage
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2016 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class maintenance_static_page_io {
/**
* Checks if the given string starts with "http://" or "https://".
* @param $url
* @return bool
*/
public static function is_url($url) {
return (bool)preg_match('#^http(s)?://#', $url);
}
/**
* Tries to get the contents of the file or URL.
* @param string $file File to get.
* @return string Contents of $file or an empty string if failed.
* @throws coding_exception
*/
public static function file_get_data($file) {
if (!is_string($file)) {
throw new coding_exception('$file is not a string.');
}
if (self::is_url($file)) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $file);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$contents = curl_exec($curl);
$mime = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
curl_close($curl);
} else {
$contents = @file_get_contents($file);
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($contents); // Not perfect, but try guessing it.
}
if ($contents === false) {
debugging('Cannot fetch: '.$file);
$contents = '';
$mime = 'unknown';
}
return ['contents' => $contents, 'mime' => $mime];
}
/** @var bool */
protected $preview = false;
/**
* @param boolean $preview
*/
public function set_preview($preview) {
$this->preview = $preview;
}
/**
* Gets the cli maintenance template file location.
* @return string
*/
public function get_template_file() {
global $CFG;
if ($this->preview) {
return $this->get_resources_folder().'/climaintenance.html';
} else {
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 function get_resources_folder() {
global $CFG;
// If you change the path, also change file auth/outage/bootstrap.php as it does not use this reference.
$dir = $CFG->dataroot.'/auth_outage/climaintenance';
if ($this->preview) {
$dir = $dir.'/preview';
}
return $dir;
}
/**
* Clean up the dataroot as needed.
*/
public function cleanup() {
$resources = $this->get_resources_folder();
if (is_dir($resources)) {
$this->delete_directory_recursively($resources);
}
$template = $this->get_template_file();
if (is_file($template)) {
unlink($template);
}
}
/**
* Clean up the dataroot as needed.
*/
public function create_resources_path() {
mkdir($this->get_resources_folder(), 0775, true);
}
/**
* Saves the template file with the given string.
* @param string $data
* @throws coding_exception
*/
public function save_template_file($data) {
if (!is_string($data) || ($data === '')) {
throw new coding_exception('$data is not a valid string.');
}
file_put_contents($this->get_template_file(), $data);
}
/**
* Deletes the given directory with all its files and subdirectories.
* @param string $dir Directory to delete.
* @throws coding_exception
* @throws invalid_parameter_exception
*/
private function delete_directory_recursively($dir) {
// It should never come from user, but protect against possible attacks anyway.
$dir = realpath($dir);
$safedir = $this->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)) {
$this->delete_directory_recursively($file);
continue;
}
throw new coding_exception('Not a file or directory: '.$file);
}
rmdir($dir);
}
/**
* Saves the content of the URL into a file, returning the new URL.
* @param string $url Input URL.
* @return string Output URL.
*/
public function generate_file_url($url) {
$saved = $this->save_url_file($url);
if (is_null($saved)) {
return $url; // Skipped, use original URL.
}
return $this->get_url_for_file($saved['url']);
}
/**
* Creates a URL for a resource file.
* @param string $filename
* @return string
*/
public function get_url_for_file($filename) {
return (string)new moodle_url('/auth/outage/file.php', ['file' => $filename]);
}
/**
* Saves the content of the URL into a file, returning the local filename.
* @param string $url Input URL.
* @return string|null Output filename or null if skipped.
*/
public function save_url_file($url) {
global $CFG;
if (!self::is_url($url)) {
debugging('Found a relative url ('.$url.') -- is it using moodle_url()?');
return null; // Leave hardcoded URLs as it is.
}
if (substr($url, 0, strlen($CFG->wwwroot)) !== $CFG->wwwroot) {
return null; // External URL, leave it.
}
// PHPUnit does not expose a web interface to fetch, point to local file instead.
if (PHPUNIT_TEST) {
$url = str_replace($CFG->wwwroot, $CFG->dirroot, $url);
}
$data = self::file_get_data($url);
$mime = trim(base64_encode($data['mime']), '=');
$url = sha1($data['contents']).'.'.$mime;
$filepath = $this->get_resources_folder().'/'.$url;
file_put_contents($filepath, $data['contents']);
if ($this->preview) {
$url = 'preview/'.$url;
}
return ['file' => $filepath, 'url' => $url];
}
}