From dc23e3fbdb7371e0e317f24c9697161d4f93cd9f Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 26 Nov 2022 08:11:09 -0800 Subject: [PATCH] Unsecured FastPreview Implementation --- lib/AppInfo/Application.php | 2 + lib/FastPreview.php | 184 ++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 lib/FastPreview.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a889c471..ce5a9f5d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace OCA\Memories\AppInfo; +use OCA\Memories\FastPreview; use OCA\Memories\Listeners\PostDeleteListener; use OCA\Memories\Listeners\PostWriteListener; use OCP\AppFramework\App; @@ -67,6 +68,7 @@ class Application extends App implements IBootstrap public function register(IRegistrationContext $context): void { + FastPreview::intercept(); $context->registerEventListener(NodeWrittenEvent::class, PostWriteListener::class); $context->registerEventListener(NodeTouchedEvent::class, PostWriteListener::class); $context->registerEventListener(NodeDeletedEvent::class, PostDeleteListener::class); diff --git a/lib/FastPreview.php b/lib/FastPreview.php new file mode 100644 index 00000000..952fcb06 --- /dev/null +++ b/lib/FastPreview.php @@ -0,0 +1,184 @@ +getConfig(); + $root = $config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data'); + $instanceId = $config->getSystemValue('instanceid', ''); + + // Get paths + $appFolder = 'appdata_'.$instanceId.'/preview'; + $folderPath = \OC\Preview\Storage\Root::getInternalFolder($_GET['fileId']); + $absFolderPath = "{$root}/{$appFolder}/{$folderPath}"; + + // Get preview specs + $w = (int) $_GET['x']; + $h = (int) $_GET['y']; + $crop = '0' === $_GET['a']; + $mode = 'fill'; + if (!$w || !$h) { + return; + } + + // Get max preview specs and extension + [$maxWidth, $maxHeight, $ext] = self::getMaxPreview($absFolderPath); + + // Get size of the preview we want + [$w, $h] = self::calculateSize($w, $h, $crop, $mode, $maxWidth, $maxHeight); + + // Construct filename of preview + $filename = $w.'-'.$h; + if ($w === $maxWidth && $h === $maxHeight) { + $filename .= '-max'; + } + $filename .= '.'.$ext; + $absPath = "{$absFolderPath}/{$filename}"; + + // Check file + if (!file_exists($absPath)) { + return; + } + + // Send file + if ('jpg' === $ext || 'jpeg' === $ext) { + header('Content-Type: image/jpeg'); + } elseif ('png' === $ext) { + header('Content-Type: image/png'); + } elseif ('gif' === $ext) { + header('Content-Type: image/gif'); + } else { + return; // ? + } + + header('Content-Length: '.filesize($absPath)); + header('Content-Disposition: inline; filename="'.$filename.'"'); + header('Cache-Control: max-age=604800, private, immutable'); + header('X-Memories-FastPreview: HIT'); + header("Content-Security-Policy: default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'"); + readfile($absPath); + + // send to user + flush(); + ob_flush(); + + exit; + } catch (\Exception $e) { + } + } + + /** Get file with max preview and get size and extension */ + private static function getMaxPreview(string $absFolderPath) + { + $files = scandir($absFolderPath); + foreach ($files as $file) { + if (false !== strpos($file, '-max')) { + $parts = explode('-', $file); + if (3 !== \count($parts)) { + continue; + } + $maxWidth = (int) $parts[0]; + $maxHeight = (int) $parts[1]; + + $extParts = explode('.', $parts[2]); + if (2 !== \count($extParts)) { + continue; + } + $ext = $extParts[1]; + + return [$maxWidth, $maxHeight, $ext]; + } + } + + throw new \Exception('No max preview found'); + } + + // Taken from @nextcloud/server Generator.php + private static function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) + { + /* + * If we are not cropping we have to make sure the requested image + * respects the aspect ratio of the original. + */ + if (!$crop) { + $ratio = $maxHeight / $maxWidth; + + if (-1 === $width) { + $width = $height / $ratio; + } + if (-1 === $height) { + $height = $width * $ratio; + } + + $ratioH = $height / $maxHeight; + $ratioW = $width / $maxWidth; + + /* + * Fill means that the $height and $width are the max + * Cover means min. + */ + if ('fill' === $mode) { + if ($ratioH > $ratioW) { + $height = $width * $ratio; + } else { + $width = $height / $ratio; + } + } elseif ('cover' === $mode) { + if ($ratioH > $ratioW) { + $width = $height / $ratio; + } else { + $height = $width * $ratio; + } + } + } + + if ($height !== $maxHeight && $width !== $maxWidth) { + // Scale to the nearest power of four + $pow4height = 4 ** ceil(log($height) / log(4)); + $pow4width = 4 ** ceil(log($width) / log(4)); + + // Minimum size is 64 + $pow4height = max($pow4height, 64); + $pow4width = max($pow4width, 64); + + $ratioH = $height / $pow4height; + $ratioW = $width / $pow4width; + + if ($ratioH < $ratioW) { + $width = $pow4width; + $height /= $ratioW; + } else { + $height = $pow4height; + $width /= $ratioH; + } + } + + /* + * Make sure the requested height and width fall within the max + * of the preview. + */ + if ($height > $maxHeight) { + $ratio = $height / $maxHeight; + $height = $maxHeight; + $width /= $ratio; + } + if ($width > $maxWidth) { + $ratio = $width / $maxWidth; + $width = $maxWidth; + $height /= $ratio; + } + + return [(int) round($width), (int) round($height)]; + } +}