diff --git a/core/Controller/CssController.php b/core/Controller/CssController.php index 1206c95a5b..b467d386f9 100644 --- a/core/Controller/CssController.php +++ b/core/Controller/CssController.php @@ -28,6 +28,8 @@ use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\IAppData; use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IRequest; class CssController extends Controller { @@ -62,12 +64,16 @@ class CssController extends Controller { public function getCss($fileName, $appName) { try { $folder = $this->appData->getFolder($appName); - $cssFile = $folder->getFile($fileName); + $gzip = false; + $file = $this->getFile($folder, $fileName, $gzip); } catch(NotFoundException $e) { return new NotFoundResponse(); } - $response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']); + $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']); + if ($gzip) { + $response->addHeader('Content-Encoding', 'gzip'); + } $response->cacheFor(86400); $expires = new \DateTime(); $expires->setTimestamp($this->timeFactory->getTime()); @@ -76,4 +82,26 @@ class CssController extends Controller { $response->addHeader('Pragma', 'cache'); return $response; } + + /** + * @param ISimpleFolder $folder + * @param string $fileName + * @param bool $gzip is set to true if we use the gzip file + * @return ISimpleFile + */ + private function getFile(ISimpleFolder $folder, $fileName, &$gzip) { + $encoding = $this->request->getHeader('Accept-Encoding'); + + if ($encoding !== null && strpos($encoding, 'gzip') !== false) { + try { + $gzip = true; + return $folder->getFile($fileName . '.gz'); + } catch (NotFoundException $e) { + // continue + } + } + + $gzip = false; + return $folder->getFile($fileName); + } } diff --git a/core/Controller/JsController.php b/core/Controller/JsController.php index 0770974e7a..0b50abc158 100644 --- a/core/Controller/JsController.php +++ b/core/Controller/JsController.php @@ -29,6 +29,8 @@ use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\IAppData; use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IRequest; class JsController extends Controller { @@ -63,12 +65,16 @@ class JsController extends Controller { public function getJs($fileName, $appName) { try { $folder = $this->appData->getFolder($appName); - $jsFile = $folder->getFile($fileName); + $gzip = false; + $file = $this->getFile($folder, $fileName, $gzip); } catch(NotFoundException $e) { return new NotFoundResponse(); } - $response = new FileDisplayResponse($jsFile, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); + $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); + if ($gzip) { + $response->addHeader('Content-Encoding', 'gzip'); + } $response->cacheFor(86400); $expires = new \DateTime(); $expires->setTimestamp($this->timeFactory->getTime()); @@ -77,4 +83,26 @@ class JsController extends Controller { $response->addHeader('Pragma', 'cache'); return $response; } + + /** + * @param ISimpleFolder $folder + * @param string $fileName + * @param bool $gzip is set to true if we use the gzip file + * @return ISimpleFile + */ + private function getFile(ISimpleFolder $folder, $fileName, &$gzip) { + $encoding = $this->request->getHeader('Accept-Encoding'); + + if ($encoding !== null && strpos($encoding, 'gzip') !== false) { + try { + $gzip = true; + return $folder->getFile($fileName . '.gz'); + } catch (NotFoundException $e) { + // continue + } + } + + $gzip = false; + return $folder->getFile($fileName); + } } diff --git a/lib/private/Template/JSCombiner.php b/lib/private/Template/JSCombiner.php index 9f92813f90..0f30fb915f 100644 --- a/lib/private/Template/JSCombiner.php +++ b/lib/private/Template/JSCombiner.php @@ -154,9 +154,16 @@ class JSCombiner { $depFile = $folder->newFile($depFileName); } + try { + $gzipFile = $folder->getFile($fileName . '.gz'); + } catch (NotFoundException $e) { + $gzipFile = $folder->newFile($fileName . '.gz'); + } + try { $cachedfile->putContent($res); $depFile->putContent(json_encode($deps)); + $gzipFile->putContent(gzencode($res, 9)); return true; } catch (NotPermittedException $e) { return false; diff --git a/lib/private/Template/SCSSCacher.php b/lib/private/Template/SCSSCacher.php index c12d877151..df2e023250 100644 --- a/lib/private/Template/SCSSCacher.php +++ b/lib/private/Template/SCSSCacher.php @@ -186,9 +186,18 @@ class SCSSCacher { return false; } + // Gzip file try { - $cachedfile->putContent($this->rebaseUrls($compiledScss, $webDir)); + $gzipFile = $folder->getFile($fileNameCSS . '.gz'); + } catch (NotFoundException $e) { + $gzipFile = $folder->newFile($fileNameCSS . '.gz'); + } + + try { + $data = $this->rebaseUrls($compiledScss, $webDir); + $cachedfile->putContent($data); $depFile->putContent(json_encode($scss->getParsedFiles())); + $gzipFile->putContent(gzencode($data, 9)); $this->logger->debug($webDir.'/'.$fileNameSCSS.' compiled and successfully cached', ['app' => 'core']); return true; } catch(NotPermittedException $e) { diff --git a/tests/Core/Controller/CssControllerTest.php b/tests/Core/Controller/CssControllerTest.php index 60fef9ddda..7fa358e056 100644 --- a/tests/Core/Controller/CssControllerTest.php +++ b/tests/Core/Controller/CssControllerTest.php @@ -40,6 +40,9 @@ class CssControllerTest extends TestCase { /** @var IAppData|\PHPUnit_Framework_MockObject_MockObject */ private $appData; + /** @var IRequests|\PHPUnit_Framework_MockObject_MockObject */ + private $request; + /** @var CssController */ private $controller; @@ -52,9 +55,11 @@ class CssControllerTest extends TestCase { $timeFactory->method('getTime') ->willReturn(1337); + $this->request = $this->createMock(IRequest::class); + $this->controller = new CssController( 'core', - $this->createMock(IRequest::class), + $this->request, $this->appData, $timeFactory ); @@ -108,4 +113,65 @@ class CssControllerTest extends TestCase { $this->assertEquals($expected, $result); } + public function testGetGzipFile() { + $folder = $this->createMock(ISimpleFolder::class); + $gzipFile = $this->createMock(ISimpleFile::class); + $this->appData->method('getFolder') + ->with('myapp') + ->willReturn($folder); + + $folder->method('getFile') + ->with('file.css.gz') + ->willReturn($gzipFile); + + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip, deflate'); + + $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'text/css']); + $expected->addHeader('Content-Encoding', 'gzip'); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp(1337); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); + $expected->addHeader('Pragma', 'cache'); + + $result = $this->controller->getCss('file.css', 'myapp'); + $this->assertEquals($expected, $result); + } + + public function testGetGzipFileNotFound() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData->method('getFolder') + ->with('myapp') + ->willReturn($folder); + + $folder->method('getFile') + ->will($this->returnCallback( + function($fileName) use ($file) { + if ($fileName === 'file.css') { + return $file; + } + throw new NotFoundException(); + }) + ); + + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip, deflate'); + + $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp(1337); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); + $expected->addHeader('Pragma', 'cache'); + + $result = $this->controller->getCss('file.css', 'myapp'); + $this->assertEquals($expected, $result); + } + } diff --git a/tests/Core/Controller/JsControllerTest.php b/tests/Core/Controller/JsControllerTest.php index febb785f60..8f48a7c339 100644 --- a/tests/Core/Controller/JsControllerTest.php +++ b/tests/Core/Controller/JsControllerTest.php @@ -42,6 +42,9 @@ class JsControllerTest extends TestCase { /** @var JsController */ private $controller; + /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ + private $request; + public function setUp() { parent::setUp(); @@ -51,9 +54,11 @@ class JsControllerTest extends TestCase { $timeFactory->method('getTime') ->willReturn(1337); + $this->request = $this->createMock(IRequest::class); + $this->controller = new JsController( 'core', - $this->createMock(IRequest::class), + $this->request, $this->appData, $timeFactory ); @@ -107,4 +112,65 @@ class JsControllerTest extends TestCase { $this->assertEquals($expected, $result); } + public function testGetGzipFile() { + $folder = $this->createMock(ISimpleFolder::class); + $gzipFile = $this->createMock(ISimpleFile::class); + $this->appData->method('getFolder') + ->with('myapp') + ->willReturn($folder); + + $folder->method('getFile') + ->with('file.js.gz') + ->willReturn($gzipFile); + + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip, deflate'); + + $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); + $expected->addHeader('Content-Encoding', 'gzip'); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp(1337); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); + $expected->addHeader('Pragma', 'cache'); + + $result = $this->controller->getJs('file.js', 'myapp'); + $this->assertEquals($expected, $result); + } + + public function testGetGzipFileNotFound() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData->method('getFolder') + ->with('myapp') + ->willReturn($folder); + + $folder->method('getFile') + ->will($this->returnCallback( + function($fileName) use ($file) { + if ($fileName === 'file.js') { + return $file; + } + throw new NotFoundException(); + }) + ); + + $this->request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip, deflate'); + + $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp(1337); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123)); + $expected->addHeader('Pragma', 'cache'); + + $result = $this->controller->getJs('file.js', 'myapp'); + $this->assertEquals($expected, $result); + } + } diff --git a/tests/lib/Template/JSCombinerTest.php b/tests/lib/Template/JSCombinerTest.php index 7b09b87922..1e6234a062 100644 --- a/tests/lib/Template/JSCombinerTest.php +++ b/tests/lib/Template/JSCombinerTest.php @@ -22,6 +22,7 @@ */ namespace Test\Template; +use function foo\func; use OC\SystemConfig; use OC\Template\JSCombiner; use OCP\Files\IAppData; @@ -100,17 +101,18 @@ class JSCombinerTest extends \Test\TestCase { $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willThrowException(new NotFoundException()); $this->appData->expects($this->once())->method('newFolder')->with('awesomeapp')->willReturn($folder); $file = $this->createMock(ISimpleFile::class); + $gzfile = $this->createMock(ISimpleFile::class); $fileDeps = $this->createMock(ISimpleFile::class); $folder->method('getFile') - ->will($this->returnCallback(function($path) use ($file) { + ->will($this->returnCallback(function($path) use ($file, $gzfile) { if ($path === 'combine.js') { return $file; - } - - if ($path === 'combine.js.deps') { + } else if ($path === 'combine.js.deps') { throw new NotFoundException(); + } else if ($path === 'combine.js.gz') { + return $gzfile; } $this->fail(); })); @@ -137,17 +139,17 @@ class JSCombinerTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $this->appData->expects($this->once())->method('getFolder')->with('awesomeapp')->willReturn($folder); $file = $this->createMock(ISimpleFile::class); - $fileDeps = $this->createMock(ISimpleFile::class); + $gzfile = $this->createMock(ISimpleFile::class); $folder->method('getFile') - ->will($this->returnCallback(function($path) use ($file) { + ->will($this->returnCallback(function($path) use ($file, $gzfile) { if ($path === 'combine.js') { return $file; - } - - if ($path === 'combine.js.deps') { + } else if ($path === 'combine.js.deps') { throw new NotFoundException(); + } else if ($path === 'combine.js.gz') { + return $gzfile; } $this->fail(); })); @@ -288,16 +290,28 @@ class JSCombinerTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzFile = $this->createMock(ISimpleFile::class); $path = __DIR__ . '/data/'; - $folder->expects($this->at(0))->method('getFile')->with($fileName)->willThrowException(new NotFoundException()); - $folder->expects($this->at(1))->method('newFile')->with($fileName)->willReturn($file); - $folder->expects($this->at(2))->method('getFile')->with($fileName . '.deps')->willThrowException(new NotFoundException()); - $folder->expects($this->at(3))->method('newFile')->with($fileName . '.deps')->willReturn($depsFile); + $folder->method('getFile')->willThrowException(new NotFoundException()); + + $folder->method('newFile')->will($this->returnCallback( + function ($filename) use ($file, $depsFile, $gzFile) { + if ($filename === 'combine.js') { + return $file; + } else if ($filename === 'combine.js.deps') { + return $depsFile; + } else if ($filename === 'combine.js.gz') { + return $gzFile; + } + $this->fail(); + } + )); $file->expects($this->once())->method('putContent'); $depsFile->expects($this->once())->method('putContent'); + $gzFile->expects($this->once())->method('putContent'); $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); $this->assertTrue($actual); @@ -309,14 +323,26 @@ class JSCombinerTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzFile = $this->createMock(ISimpleFile::class); $path = __DIR__ . '/data/'; - $folder->expects($this->at(0))->method('getFile')->with($fileName)->willReturn($file); - $folder->expects($this->at(1))->method('getFile')->with($fileName . '.deps')->willReturn($depsFile); + $folder->method('getFile')->will($this->returnCallback( + function ($filename) use ($file, $depsFile, $gzFile) { + if ($filename === 'combine.js') { + return $file; + } else if ($filename === 'combine.js.deps') { + return $depsFile; + } else if ($filename === 'combine.js.gz') { + return $gzFile; + } + $this->fail(); + } + )); $file->expects($this->once())->method('putContent'); $depsFile->expects($this->once())->method('putContent'); + $gzFile->expects($this->once())->method('putContent'); $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); $this->assertTrue($actual); @@ -364,11 +390,23 @@ var b = \'world\'; $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzFile = $this->createMock(ISimpleFile::class); $path = __DIR__ . '/data/'; - $folder->expects($this->at(0))->method('getFile')->with($fileName)->willReturn($file); - $folder->expects($this->at(1))->method('getFile')->with($fileName . '.deps')->willReturn($depsFile); + + $folder->method('getFile')->will($this->returnCallback( + function ($filename) use ($file, $depsFile, $gzFile) { + if ($filename === 'combine.js') { + return $file; + } else if ($filename === 'combine.js.deps') { + return $depsFile; + } else if ($filename === 'combine.js.gz') { + return $gzFile; + } + $this->fail(); + } + )); $file->expects($this->at(0)) ->method('putContent') @@ -385,6 +423,17 @@ var b = \'world\'; return array_key_exists(__DIR__ . '/data//1.js', $deps) && array_key_exists(__DIR__ . '/data//2.js', $deps); })); + $gzFile->expects($this->at(0))->method('putContent')->with($this->callback( + function ($content) { + return gzdecode($content) === 'var a = \'hello\'; + + +var b = \'world\'; + + +'; + } + )); $actual = self::invokePrivate($this->jsCombiner, 'cache', [$path, 'combine.json', $folder]); $this->assertTrue($actual); diff --git a/tests/lib/Template/SCSSCacherTest.php b/tests/lib/Template/SCSSCacherTest.php index 898ea89cf4..887fa1ed60 100644 --- a/tests/lib/Template/SCSSCacherTest.php +++ b/tests/lib/Template/SCSSCacherTest.php @@ -73,13 +73,16 @@ class SCSSCacherTest extends \Test\TestCase { $file = $this->createMock(ISimpleFile::class); $file->expects($this->any())->method('getSize')->willReturn(1); $fileDeps = $this->createMock(ISimpleFile::class); + $gzfile = $this->createMock(ISimpleFile::class); $folder->method('getFile') - ->will($this->returnCallback(function($path) use ($file) { + ->will($this->returnCallback(function($path) use ($file, $gzfile) { if ($path === 'styles.css') { return $file; } else if ($path === 'styles.css.deps') { throw new NotFoundException(); + } else if ($path === 'styles.css.gz') { + return $gzfile; } else { $this->fail(); } @@ -99,14 +102,17 @@ class SCSSCacherTest extends \Test\TestCase { $file = $this->createMock(ISimpleFile::class); $file->expects($this->any())->method('getSize')->willReturn(1); $fileDeps = $this->createMock(ISimpleFile::class); + $gzfile = $this->createMock(ISimpleFile::class); $folder->method('getFile') - ->will($this->returnCallback(function($path) use ($file) { + ->will($this->returnCallback(function($path) use ($file, $gzfile) { if ($path === 'styles.css') { return $file; } else if ($path === 'styles.css.deps') { throw new NotFoundException(); - } else { + } else if ($path === 'styles.css.gz') { + return $gzfile; + }else { $this->fail(); } })); @@ -211,17 +217,26 @@ class SCSSCacherTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzipFile = $this->createMock(ISimpleFile::class); $webDir = "core/css"; $path = \OC::$SERVERROOT . '/core/css/'; - $folder->expects($this->at(0))->method('getFile')->with($fileNameCSS)->willThrowException(new NotFoundException()); - $folder->expects($this->at(1))->method('newFile')->with($fileNameCSS)->willReturn($file); - $folder->expects($this->at(2))->method('getFile')->with($fileNameCSS . '.deps')->willThrowException(new NotFoundException()); - $folder->expects($this->at(3))->method('newFile')->with($fileNameCSS . '.deps')->willReturn($depsFile); + $folder->method('getFile')->willThrowException(new NotFoundException()); + $folder->method('newFile')->will($this->returnCallback(function($fileName) use ($file, $depsFile, $gzipFile) { + if ($fileName === 'styles.css') { + return $file; + } else if ($fileName === 'styles.css.deps') { + return $depsFile; + } else if ($fileName === 'styles.css.gz') { + return $gzipFile; + } + throw new \Exception(); + })); $file->expects($this->once())->method('putContent'); $depsFile->expects($this->once())->method('putContent'); + $gzipFile->expects($this->once())->method('putContent'); $actual = self::invokePrivate($this->scssCacher, 'cache', [$path, $fileNameCSS, $fileNameSCSS, $folder, $webDir]); $this->assertTrue($actual); @@ -233,15 +248,25 @@ class SCSSCacherTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzipFile = $this->createMock(ISimpleFile::class); $webDir = "core/css"; $path = \OC::$SERVERROOT; - $folder->expects($this->at(0))->method('getFile')->with($fileNameCSS)->willReturn($file); - $folder->expects($this->at(1))->method('getFile')->with($fileNameCSS . '.deps')->willReturn($depsFile); + $folder->method('getFile')->will($this->returnCallback(function($fileName) use ($file, $depsFile, $gzipFile) { + if ($fileName === 'styles.css') { + return $file; + } else if ($fileName === 'styles.css.deps') { + return $depsFile; + } else if ($fileName === 'styles.css.gz') { + return $gzipFile; + } + throw new \Exception(); + })); $file->expects($this->once())->method('putContent'); $depsFile->expects($this->once())->method('putContent'); + $gzipFile->expects($this->once())->method('putContent'); $actual = self::invokePrivate($this->scssCacher, 'cache', [$path, $fileNameCSS, $fileNameSCSS, $folder, $webDir]); $this->assertTrue($actual); @@ -253,12 +278,21 @@ class SCSSCacherTest extends \Test\TestCase { $folder = $this->createMock(ISimpleFolder::class); $file = $this->createMock(ISimpleFile::class); $depsFile = $this->createMock(ISimpleFile::class); + $gzipFile = $this->createMock(ISimpleFile::class); $webDir = "tests/data/scss"; $path = \OC::$SERVERROOT . $webDir; - $folder->expects($this->at(0))->method('getFile')->with($fileNameCSS)->willReturn($file); - $folder->expects($this->at(1))->method('getFile')->with($fileNameCSS . '.deps')->willReturn($depsFile); + $folder->method('getFile')->will($this->returnCallback(function($fileName) use ($file, $depsFile, $gzipFile) { + if ($fileName === 'styles-success.css') { + return $file; + } else if ($fileName === 'styles-success.css.deps') { + return $depsFile; + } else if ($fileName === 'styles-success.css.gz') { + return $gzipFile; + } + throw new \Exception(); + })); $file->expects($this->at(0))->method('putContent')->with($this->callback( function ($content){ @@ -270,6 +304,11 @@ class SCSSCacherTest extends \Test\TestCase { return array_key_exists(\OC::$SERVERROOT . '/core/css/variables.scss', $deps) && array_key_exists(\OC::$SERVERROOT . '/tests/data/scss/styles-success.scss', $deps); })); + $gzipFile->expects($this->at(0))->method('putContent')->with($this->callback( + function ($content) { + return gzdecode($content) === 'body{background-color:#0082c9}'; + } + )); $actual = self::invokePrivate($this->scssCacher, 'cache', [$path, $fileNameCSS, $fileNameSCSS, $folder, $webDir]); $this->assertTrue($actual);