diff --git a/public/css.php b/public/css.php index 7404e65c..6236c7e8 100644 --- a/public/css.php +++ b/public/css.php @@ -36,11 +36,6 @@ class CSSMin extends BaseMin { $this->group = $groups[$group]; $this->last_modified = $this->get_last_modified(); - $this->requested_time = $this->get_if_modified(); - } - - public function __destruct() - { $this->send(); } @@ -51,11 +46,10 @@ class CSSMin extends BaseMin { */ protected function send() { - /*if($this->last_modified >= $this->requested_time) + if($this->last_modified >= $this->get_if_modified() && $this->is_not_debug()) { - header('304 Not Modified'); - die(); - }*/ + throw new FileNotChangedException(); + } $css = ( ! array_key_exists('debug', $_GET)) ? $this->compress($this->get_css()) @@ -70,7 +64,7 @@ class CSSMin extends BaseMin { * @param string $buffer * @return string */ - public function compress($buffer) + protected function compress($buffer) { //Remove CSS comments $buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer); @@ -142,7 +136,7 @@ class CSSMin extends BaseMin { // Correct paths that have changed due to concatenation // based on rules in the config file - // $css = str_replace($this->path_from, $this->path_to, $css); + $css = str_replace($this->path_from, $this->path_to, $css); return $css; } @@ -152,20 +146,16 @@ class CSSMin extends BaseMin { * * @return void */ - public function output($css) + protected function output($css) { - //This GZIPs the CSS for transmission to the user - //making file size smaller and transfer rate quicker - ob_start("ob_gzhandler"); + $etag = md5($css); - header("Content-Type: text/css; charset=utf8"); - header("Cache-control: public, max-age=691200, must-revalidate"); - header("Last-Modified: ".gmdate('D, d M Y H:i:s', $this->last_modified)." GMT"); - header("Expires: ".gmdate('D, d M Y H:i:s', (filemtime(basename(__FILE__)) + 691200))." GMT"); + if ($etag === $this->get_if_none_match() && $this->is_not_debug()) + { + throw new FileNotChangedException(); + } - echo $css; - - ob_end_flush(); + $this->send_final_output($css, 'text/css', $this->last_modified, $etag); } } @@ -177,6 +167,18 @@ class CSSMin extends BaseMin { $config = require('../app/config/minify_config.php'); $groups = require($config['css_groups_file']); -new CSSMin($config, $groups); +if ( ! array_key_exists($_GET['g'], $groups)) +{ + throw new InvalidArgumentException('You must specify a css group that exists'); +} + +try +{ + new CSSMin($config, $groups); +} +catch (FileNotChangedException $e) +{ + BaseMin::send304(); +} //End of css.php \ No newline at end of file diff --git a/public/js.php b/public/js.php index 6fccc8ea..bfc18fc3 100644 --- a/public/js.php +++ b/public/js.php @@ -44,48 +44,42 @@ class JSMin extends BaseMin { $this->cache_file = "{$this->js_root}cache/{$group}"; $this->last_modified = $this->get_last_modified(); - $this->requested_time = $this->get_if_modified(); - $this->cache_modified = (is_file($this->cache_file)) ? filemtime($this->cache_file) : 0; - } - public function __destruct() - { // Output some JS! $this->send(); } protected function send() { - // If the browser's cached version is up to date, - // don't resend the file - /*if($this->last_modified === $this->requested_time) - { - header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified'); - exit(); - }*/ - - //Determine what to do: rebuild cache, send files as is, or send cache. - // If debug is set, just concatenate - if(array_key_exists('debug', $_GET)) + // Override caching if debug key is set + if($this->is_debug_call()) { return $this->output($this->get_files()); } - else if($this->cache_modified < $this->last_modified) + + // If the browser's cached version is up to date, + // don't resend the file + if($this->last_modified >= $this->get_if_modified() && $this->is_not_debug()) + { + throw new FileNotChangedException(); + } + + if($this->cache_modified < $this->last_modified) { $js = $this->minify($this->get_files()); //Make sure cache file gets created/updated - if(file_put_contents($this->cache_file, $js) === FALSE) + if (file_put_contents($this->cache_file, $js) === FALSE) { - die('Cache file was not created. Make sure you have the correct folder permissions.'); + echo 'Cache file was not created. Make sure you have the correct folder permissions.'; + return; } return $this->output($js); } - // Otherwise, send the cached file else { return $this->output(file_get_contents($this->cache_file)); @@ -185,7 +179,7 @@ class JSMin extends BaseMin { * @param string $js * @return string */ - public function minify($js) + protected function minify($js) { $options = [ 'output_info' => 'errors', @@ -218,19 +212,14 @@ class JSMin extends BaseMin { */ protected function output($js) { - //This GZIPs the js for transmission to the user - //making file size smaller and transfer rate quicker - ob_start('ob_gzhandler'); + $etag = md5($js); - // Set important caching headers - header('Content-Type: application/javascript; charset=utf8'); - header('Cache-control: public, max-age=691200, must-revalidate'); - header('Last-Modified: '.gmdate('D, d M Y H:i:s', $this->last_modified).' GMT'); - header('Expires: '.gmdate('D, d M Y H:i:s', (filemtime(__FILE__) + 691200)).' GMT'); + if (($etag === $this->get_if_none_match()) && ! $this->is_debug_call()) + { + throw new FileNotChangedException(); + } - echo $js; - - ob_end_flush(); + $this->send_final_output($js, 'application/javascript', $this->last_modified, $etag); } } @@ -249,11 +238,16 @@ if ( ! is_dir($cache_dir)) if ( ! array_key_exists($_GET['g'], $groups)) { - header('Content-Type: application/javascript; charset=utf8'); - echo '// You must specify a group that exists'; - die(); + throw new InvalidArgumentException('You must specify a js group that exists'); } -new JSMin($config, $groups); +try +{ + new JSMin($config, $groups); +} +catch (FileNotChangedException $e) +{ + BaseMin::send304(); +} //end of js.php \ No newline at end of file diff --git a/public/js/anime_collection.js b/public/js/anime_collection.js index 494c2e1c..9c03508f 100755 --- a/public/js/anime_collection.js +++ b/public/js/anime_collection.js @@ -18,7 +18,7 @@ }; $.get('/public/templates/anime-ajax-search-results.html', tempHtml => { - $('#search').on('keypress', AnimeClient.throttle(250, function(e) { + AnimeClient.on('#search', 'keypress', AnimeClient.throttle(250, function(e) { let query = encodeURIComponent($(this).val()); if (query === '') { return; diff --git a/public/js/anime_edit.js b/public/js/anime_edit.js index 70d5dfbc..201d4a26 100755 --- a/public/js/anime_edit.js +++ b/public/js/anime_edit.js @@ -34,12 +34,11 @@ } // okay, lets actually make some changes! - $.ajax({ + AnimeClient.ajax(AnimeClient.url('/anime/update'), { data: data, dataType: 'json', type: 'POST', mimeType: 'application/json', - url: AnimeClient.url('/anime/update'), success: (res) => { if (res.status === 'completed') { $(this).closest('article, tr').hide(); diff --git a/public/js/base/ajax.js b/public/js/base/ajax.js new file mode 100644 index 00000000..34ea577b --- /dev/null +++ b/public/js/base/ajax.js @@ -0,0 +1,70 @@ +AnimeClient = (function (ac) { + + /** + * Url encoding for non-get requests + * + * @param data + * @returns {string} + * @private + */ + function serialize(data) { + let pairs = []; + + Object.keys(data).forEach((name) => { + let value = data[name].toString(); + + name = encodeURIComponent(name); + value = encodeURIComponent(value); + + pairs.push(`${name}=${value}`); + }); + + return pairs.join("&"); + }; + + ac.ajax = function(url, config) { + // Set some sane defaults + config = config || {}; + config.data = config.data || {}; + config.type = config.type || 'GET'; + config.dataType = config.dataType || 'json'; + config.success = config.success || ac.noop; + config.error = config.error || ac.noop; + + let request = new XMLHttpRequest(); + let method = String(config.type).toUpperCase(); + + request.open(method, url); + + request.onreadystatechange = () => { + if (request.readyState === 4) { + if (request.status > 400) { + config.error.call(request.statusText, request.statusText, request.response); + } else { + config.success.call(request.responseText, request.response, request.responseText); + } + } + }; + + switch (method) { + case "GET": + request.send(null); + break; + + default: + request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + request.send(serialize(config.data)); + break; + } + }; + + ac.get = function(url, data, callback) { + return ac.ajax(url, { + data: data, + success: callback + }); + }; + + return ac; + +})(AnimeClient); \ No newline at end of file diff --git a/public/js/base/base.js b/public/js/base/base.js index ea74dd7f..9c006ba3 100644 --- a/public/js/base/base.js +++ b/public/js/base/base.js @@ -1,8 +1,39 @@ -const AnimeClient = (function($, w) { +var AnimeClient = (function(w) { 'use strict'; + const slice = Array.prototype.slice; + return { + /** + * Placeholder function + */ + noop: () => {}, + /** + * DOM selector + * + * @param {string} selector - The dom selector string + * @param {object} context + * @return {array} - arrau of dom elements + */ + $(selector, context) { + if (typeof selector != "string" || selector === undefined) { + return selector; + } + + context = (context != null && context.nodeType === 1) + ? context + : document; + + let elements = []; + if (selector.match(/^#([\w]+$)/)) { + elements.push(document.getElementById(selector.split('#')[1])); + } else { + elements = slice.apply(context.querySelectorAll(selector)); + } + + return elements; + }, /** * Scroll to the top of the Page * @@ -71,4 +102,4 @@ const AnimeClient = (function($, w) { }; }, }; -})(Zepto, window); \ No newline at end of file +})(window); \ No newline at end of file diff --git a/public/js/base/event.js b/public/js/base/event.js new file mode 100644 index 00000000..4307f20d --- /dev/null +++ b/public/js/base/event.js @@ -0,0 +1,41 @@ +AnimeClient = (function (ac) { + 'use strict'; + + function add(sel, event, listener) { + // Recurse! + if (! event.match(/^([\w\-]+)$/)) { + event.split(' ').forEach((evt) => { + add(sel, evt, listener); + }); + } + + sel.addEventListener(event, listener, false); + } + function delegate(sel, target, event, listener) { + // Attach the listener to the parent + add(sel, event, (e) => { + // Get live version of the target selector + ac.$(target, sel).forEach((element) => { + if(e.target == element) { + listener.call(element, e); + e.stopPropagation(); + } + }); + }); + } + + ac.on = function (sel, event, target, listener) { + if (arguments.length === 3) { + listener = target; + ac.$(sel).forEach((el) => { + add(el, event, listener); + }); + } else { + ac.$(sel).forEach((el) => { + delegate(el, target, event, listener); + }); + } + } + + return ac; +})(AnimeClient); \ No newline at end of file diff --git a/public/js/base/events.js b/public/js/base/events.js index 080855de..22fc3715 100644 --- a/public/js/base/events.js +++ b/public/js/base/events.js @@ -1,13 +1,13 @@ /** * Event handlers */ -(($) => { +((ac) => { 'use strict'; // Close event for messages - $('header').on('click', '.message', function() { - $(this).hide(); + ac.on('header', 'click', '.message', function () { + this.setAttribute('hidden', 'hidden'); }); -})(Zepto); \ No newline at end of file +})(AnimeClient); \ No newline at end of file diff --git a/public/js/manga_edit.js b/public/js/manga_edit.js index faa3617b..a6ff9640 100755 --- a/public/js/manga_edit.js +++ b/public/js/manga_edit.js @@ -25,13 +25,12 @@ // Update the total count data[type + "s_read"] = ++completed; - $.ajax({ + AnimeClient.ajax(AnimeClient.url('/manga/update'), { data: data, dataType: 'json', type: 'POST', mimeType: 'application/json', - url: AnimeClient.url('/manga/update'), - success: (res, status) => { + success: (res) => { parent_sel.find(`.${type}s_read`).text(completed); AnimeClient.showMessage('success', `Sucessfully updated ${manga_name}`); AnimeClient.scrollToTop(); diff --git a/public/min.php b/public/min.php index be3fc0da..48f32b11 100644 --- a/public/min.php +++ b/public/min.php @@ -30,18 +30,89 @@ while($i < $pia_len) $i = $j + 1; }; +class FileNotChangedException extends \Exception {} class BaseMin { - public function get_if_modified() + + /** + * Get value of the if-modified-since header + * + * @return int - timestamp to compare for cache control + */ + protected function get_if_modified() { return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : time(); } - public function get_if_none_match() + /** + * Get value of etag to compare to hash of output + * + * @return string - the etag to compare + */ + protected function get_if_none_match() { return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ? $_SERVER['HTTP_IF_NONE_MATCH'] : ''; } + + /** + * Determine whether or not to send debug version + * + * @return boolean + */ + protected function is_not_debug() + { + return ! $this->is_debug_call(); + } + + /** + * Determine whether or not to send debug version + * + * @return boolean + */ + protected function is_debug_call() + { + return array_key_exists('debug', $_GET); + } + + /** + * Send actual output to browser + * + * @param string $content - the body of the response + * @param string $mime_type - the content type + * @param int $last_modified - the last modified date + * @return void + */ + protected function send_final_output($content, $mime_type, $last_modified, $etag) + { + //This GZIPs the CSS for transmission to the user + //making file size smaller and transfer rate quicker + ob_start("ob_gzhandler"); + + $expires = $last_modified + 691200; + $last_modified_date = gmdate('D, d M Y H:i:s', $last_modified); + $expires_date = gmdate('D, d M Y H:i:s', $expires); + + header("Content-Type: {$mime_type}; charset=utf8"); + header("Cache-control: public, max-age=691200, must-revalidate"); + header("Etag: {$etag}"); + header("Last-Modified: {$last_modified_date} GMT"); + header("Expires: {$expires_date} GMT"); + + echo $content; + + ob_end_flush(); + } + + /** + * Send a 304 Not Modified header + * + * @return void + */ + public static function send304() + { + header("status: 304 Not Modified", true, 304); + } } \ No newline at end of file