Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/checksum media #139

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 169 additions & 64 deletions src/sprout/Controllers/MediaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,116 +13,170 @@

namespace Sprout\Controllers;

use BootstrapConfig;
use Exception;
use Kohana;
use Kohana_404_Exception;
use Kohana_Exception;
use Sprout\Exceptions\MediaException;
use Sprout\Helpers\AdminAuth;
use Sprout\Helpers\File;
use Sprout\Helpers\Modules;
use Sprout\Helpers\Media;
use Sprout\Helpers\Router;
use Sprout\Helpers\Modules;
use Sprout\Helpers\Subsites;
use Sprout\Helpers\SubsiteSelector;
use Sprout\Helpers\Url;
use Throwable;

/**
* Serving media assets.
*
* These are not files, {@see FileController}. These are JS/CSS assets
* typically loaded by the {@see Needs} helper.
* typically loaded by the {@see Needs} and {@see Media} helpers.
*/
class MediaController extends Controller
{

/**
* Serve the file immediately.
* Serve a file.
*
* Generated files should theoretically exist on the filesystem and be
* served directly from the webserver. However there are a fair few
* scenarios where this does not happen.
*
* - Empty caches on deploy
* - Stale documents in browsers (these may trigger requests to old checksums)
* - Stale middleware caches
* - Relative URLs from other media
*
* @param string $resource
* @param string $segments
* @return never
* @throws Kohana_404_Exception
* `_media/checksum/section/file`
*
* @return never serve the file
*/
public function serve(...$segments)
public function generate($checksum, ...$segments)
{
$resource = array_shift($segments);
$url = implode('/', $segments);

if ($resource === 'core') {
$root = COREPATH . 'media/';
$file = implode('/', $segments);
$media = Media::parse($file);

} elseif ($resource === 'sprout') {
$root = APPPATH . 'media/';
if (!file_exists($media->getPath())) {
throw new Kohana_404_Exception();
}

} elseif ($resource === 'skin') {
$root = DOCROOT . 'skin/';
$actual = $media->getChecksum();

} else if ($module = Modules::getModule($resource)) {
$root = $module->getPath() . 'media/';
}
else {
throw new Kohana_404_Exception($url);
if ($actual === null) {
throw new MediaException('Failed to read file: ' . $file);
}

$path = $root . $url;

if (!is_file($path)) {
throw new Kohana_404_Exception($url);
// We don't want the browser thinking this file belongs with the
// wrong checksum. Generate the correct checksum asset + redirect to it.
if ($checksum !== $actual) {
$url = $media->generateUrl();
Url::redirect($url);
}

// Shush. Drop any existing output.
Kohana::closeBuffers(false);
set_exception_handler(null);

// Caching for 7 days.
header('Cache-Control: cache, store, max-age=604800, must-revalidate');

// File types.
$mimetype = File::mimetypeExtended($path);
$mimetype = File::mimetypeExtended($media->getPath());
if ($mimetype) {
header("Content-Type: {$mimetype}");
}

// For debugging.
header('X-Media-Hit: true');

// Dump out the file.
$ok = readfile($path);
// Dump out the file immediately.
$ok = readfile($media->getPath());

if ($ok === false) {
throw new Exception('Failed to read file: ' . $url);
throw new MediaException("Failed to read file: '{$media->name}' ({$media->section})");
}

// Ok, really shush now.
// We don't want errors bleeding into the asset bodies.
set_exception_handler(null);
ini_set('display_errors', '0');

// Now copy it so this file doesn't hit the app again. This effectively
// 'shadows' the file in the same path as this controller. The
// web server (nginx, apache) should find and serve it before deferring
// to the PHP app.
// First check defined() so we don't break migrations.
if (defined('BootstrapConfig::ENABLE_MEDIA_CACHE') and constant('BootstrapConfig::ENABLE_MEDIA_CACHE')) {
try {
$dest = WEBROOT . Router::$current_uri;
$dir = dirname($dest);

// if (exists) mkdir() is not atomic. Another thread can always
// beat us to it. Do and ask for forgiveness later.
@mkdir($dir, 0777, true);

if (!is_dir($dir)) {
throw new Exception("Target directory is missing: {$dir}");
}

$ok = copy($path, $dest);
if (!$ok) throw new Exception("Failed to copy file: {$path} to {$dest}");
}
catch (Throwable $ex) {
Kohana::logException($ex, true);
// Generate the checksum asset for later.
try {
$media->generateUrl();
} catch (Throwable $ex) {
Kohana::logException($ex, true);
}

die;
}


/**
* Find an asset and redirect to its generated URL.
*
* This serves mostly as backwards compatibility.
*
* `_media/section/path/to/file`
*
* @return never redirect to generated checksum URL
*/
public function resolve(...$segments)
{
try {
// Backwards compat for naked modules.
if (!in_array($segments[0], ['core', 'sprout', 'skin', 'modules'])) {
array_unshift($segments, 'modules');
}

$file = implode('/', $segments);
$media = Media::parse($file);

$url = $media->generateUrl();
Url::redirect($url);

} catch (MediaException $ex) {
Kohana::logException($ex);
throw new Kohana_404_Exception();
}
}


/**
* Requests to the old media endpoints.
*
* - `media/` (core)
* - `sprout/media/`
* - `modules/{names}/media/`
* - `skin/{name}/`
*
* @param mixed ...$segments
* @return never redirect to generated checksum URL
*/
public function compat($section, ...$segments)
{
if ($section === 'media') {
$section = 'core';

} else if ($section === 'sprout') {
$section = 'sprout';

} else if ($section === 'skin') {
$name = array_shift($segments);
$section = 'skin/' . $name;

} else {
$section = 'modules/' . $section;
}

// Tidy up.
if ($segments[0] == 'media') {
array_shift($segments);
}

exit;
$file = $section . '/' . implode('/', $segments);

try {
$media = Media::parse($file);
$url = $media->generateUrl();
Url::redirect($url);

} catch (MediaException $ex) {
throw new Kohana_404_Exception();
}
}


Expand All @@ -141,4 +195,55 @@ public function clean()
Media::clean();
}


/**
* Copy all asset files into generated paths.
*
* @param string|null $skin specify null for all skins
*/
public function process($skin = null)
{
if (PHP_SAPI != 'cli') {
AdminAuth::checkLogin();
}

header('content-type: text/plain');

if ($skin === null) {
$subsite = Subsites::getDefaultSubsite();
} else {
$subsite = Subsites::getSubsiteByCode($skin);
}

if ($subsite === null) {
echo "Subsite not found: {$skin}\n";
exit(1);
}

SubsiteSelector::setSubsite($subsite);
echo "Selected skin: {$subsite['code']}\n";
echo "--------------------------------\n";

$paths = [
['core', COREPATH . 'media/'],
['sprout', APPPATH . 'media/'],
['skin/' . $subsite['code'], DOCROOT . 'skin/' . $subsite['code']],
];

foreach (Modules::getModules() as $module) {
$paths[] = [
'modules/' . $module->getName(),
$module->getPath() . 'media/',
];
}

Media::clean('silent');

foreach ($paths as [$name, $path]) {
$checksum = Media::generateChecksum($path, true);
$checksum ??= '--';
echo sprintf("Processed: %-12s %s\n", $name, $checksum);
}
}

}
21 changes: 21 additions & 0 deletions src/sprout/Exceptions/MediaException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS 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 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Exceptions;


/**
* An error when processing a media resources.
*/
class MediaException extends \Exception
{
}
Loading