Source of file FileIDHelperResolutionStrategy.php
Size: 18,801 Bytes - Last Modified: 2021-12-23T10:27:40+00:00
/var/www/docs.ssmods.com/process/src/src/FilenameParsing/FileIDHelperResolutionStrategy.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 | <?php namespace SilverStripe\Assets\FilenameParsing; use InvalidArgumentException; use League\Flysystem\Filesystem; use SilverStripe\Assets\Storage\FileHashingService; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; use SilverStripe\Versioned\Versioned; use SilverStripe\Assets\File; use SilverStripe\ORM\DB; /** * File resolution strategy that relies on a list of FileIDHelpers to find files. * * `DefaultFileIDHelper` is the default helper use to generate new file ID. * * `ResolutionFileIDHelpers` can contain a list of helpers that will be used to try to find existing file. * * This file resolution strategy can be helpful when the approach to resolving files has changed over time and you need * older file format to resolve. * * You may also provide a `VersionedStage` to only look at files that were published. * * @internal This is still an evolving API. It may change in the next minor release. */ class FileIDHelperResolutionStrategy implements FileResolutionStrategy { use Configurable; use Injectable; /** * The FileID helper that will be use to build FileID for this adapter. * @var FileIDHelper */ private $defaultFileIDHelper; /** * List of FileIDHelper that should be use to try to parse FileIDs on this adapter. * @var FileIDHelper[] */ private $resolutionFileIDHelpers; /** * Constrain this strategy to the a specific versioned stage. * @var string */ private $versionedStage = ''; /** @var FileHashingService */ private $hasher; private static $dependencies = [ 'FileHashingService' => '%$' . FileHashingService::class ]; public function __construct() { if (class_exists(Versioned::class)) { $this->versionedStage = Versioned::DRAFT; } } public function setFileHashingService($service) { $this->hasher = $service; return $this; } public function resolveFileID($fileID, Filesystem $filesystem) { foreach ($this->resolutionFileIDHelpers as $fileIDHelper) { $parsedFileID = $fileIDHelper->parseFileID($fileID); if ($parsedFileID) { $foundTuple = $this->searchForTuple($parsedFileID, $filesystem, true); if ($foundTuple) { return $foundTuple; } } } // If we couldn't resolve the file ID, we bail return null; } public function softResolveFileID($fileID, Filesystem $filesystem) { // If File is not versionable, let's bail if (!class_exists(Versioned::class) || !File::has_extension(Versioned::class)) { return null; } $parsedFileID = $this->parseFileID($fileID); if (!$parsedFileID) { return null; } $hash = $parsedFileID->getHash(); $tuple = $hash ? $this->resolveWithHash($parsedFileID) : $this->resolveHashless($parsedFileID); if ($tuple) { return $this->searchForTuple($tuple, $filesystem, false); } // If we couldn't resolve the file ID, we bail return null; } /** * Try to find a DB reference for this parsed file ID. Return a file tuple if a equivalent file is found. * @param ParsedFileID $parsedFileID * @return ParsedFileID|null */ private function resolveWithHash(ParsedFileID $parsedFileID) { // Try to find a version for a given stage /** @var File $file */ $file = Versioned::withVersionedMode(function () use ($parsedFileID) { Versioned::set_stage($this->getVersionedStage()); return File::get()->filter(['FileFilename:case' => $parsedFileID->getFilename()])->first(); }); // Could not find a valid file, let's bail. if (!$file) { return null; } $dbHash = $file->getHash(); if (strpos($dbHash, $parsedFileID->getHash()) === 0) { return $parsedFileID; } // If we found a matching live file, let's see if our hash was publish at any point // Build a version filter $versionFilters = [ ['"FileHash" like ?' => DB::get_conn()->escapeString($parsedFileID->getHash()) . '%'], ['not "FileHash" like ?' => DB::get_conn()->escapeString($file->getHash())], ]; if ($this->getVersionedStage() == Versioned::LIVE) { // If we a limited to the Live stage, let's only look at files that have bee published $versionFilters['"WasPublished"'] = true; } $oldVersionCount = $file->allVersions($versionFilters, "", 1)->count(); // Our hash was published at some other stage if ($oldVersionCount > 0) { return new ParsedFileID($file->getFilename(), $file->getHash(), $parsedFileID->getVariant()); } return null; } /** * Try to find a DB reference for this parsed file ID that doesn't have an hash. Return a file tuple if a * equivalent file is found. * @param ParsedFileID $parsedFileID * @return array|null */ private function resolveHashless(ParsedFileID $parsedFileID) { $filename = $parsedFileID->getFilename(); $variant = $parsedFileID->getVariant(); // Let's try to match the plain file name /** @var File $file */ $file = Versioned::withVersionedMode(function () use ($filename) { Versioned::set_stage($this->getVersionedStage()); return File::get()->filter(['FileFilename:case' => $filename])->first(); }); if ($file) { return [ 'Filename' => $filename, 'Hash' => $file->getHash(), 'Variant' => $variant ]; } return null; } public function generateVariantFileID($tuple, Filesystem $fs) { $parsedFileID = $this->preProcessTuple($tuple); if (empty($parsedFileID->getVariant())) { return $this->searchForTuple($parsedFileID, $fs); } // Let's try to find a helper who can understand our file ID foreach ($this->resolutionFileIDHelpers as $helper) { if ($this->validateHash($helper, $parsedFileID, $fs)) { return $parsedFileID->setFileID( $helper->buildFileID( $parsedFileID->getFilename(), $parsedFileID->getHash(), $parsedFileID->getVariant() ) ); } } return null; } public function searchForTuple($tuple, Filesystem $filesystem, $strict = true) { $parsedFileID = $this->preProcessTuple($tuple); $helpers = $this->getResolutionFileIDHelpers(); // Add default helper to list of resolvable helpers $defaultHelper = $this->getDefaultFileIDHelper(); $defaultHelperIndex = array_search($defaultHelper, $helpers); if ($defaultHelperIndex !== false) { unset($helpers[$defaultHelperIndex]); } array_unshift($helpers, $defaultHelper); $enforceHash = $strict && $parsedFileID->getHash(); // When trying to resolve a file ID it's possible that we don't know it's hash. // We'll try our best to get it from the DB if (empty($parsedFileID->getHash())) { $filename = $parsedFileID->getFilename(); if (class_exists(Versioned::class) && File::has_extension(Versioned::class)) { $hashList = Versioned::withVersionedMode(function () use ($filename) { Versioned::set_stage($this->getVersionedStage()); return File::get() ->filter(['FileFilename:case' => $filename, 'FileVariant' => null]) ->limit(1) ->column('FileHash'); }); } else { $hashList = File::get() ->filter(['FileFilename:case' => $filename]) ->limit(1) ->column('FileHash'); } // In theory, we could get more than one file with the same Filename. We wouldn't know how to tell // them apart any way so we'll just look at the first hash if (!empty($hashList)) { $parsedFileID = $parsedFileID->setHash($hashList[0]); } } foreach ($helpers as $helper) { try { $fileID = $helper->buildFileID($parsedFileID, null, null, false); } catch (InvalidArgumentException $ex) { // Some file ID helper will throw an exception if you ask them to build a file ID wihtout an hash continue; } if ($filesystem->has($fileID)) { if ($enforceHash && !$this->validateHash($helper, $parsedFileID, $filesystem)) { // We found a file, but its hash doesn't match the hash of our tuple. continue; } if (empty($parsedFileID->getHash()) && $fullHash = $this->findHashOf($helper, $parsedFileID, $filesystem) ) { $parsedFileID = $parsedFileID->setHash($fullHash); } return $parsedFileID->setFileID($fileID); } } return null; } /** * Try to validate the hash of a physical file against the expected hash from the parsed file ID. * @param FileIDHelper $helper * @param ParsedFileID $parsedFileID * @param Filesystem $filesystem * @return bool */ private function validateHash(FileIDHelper $helper, ParsedFileID $parsedFileID, Filesystem $filesystem) { // We assumme that hashless parsed file ID are always valid if (!$parsedFileID->getHash()) { return true; } // Check if the physical hash of the file starts with our parsed file ID hash $actualHash = $this->findHashOf($helper, $parsedFileID, $filesystem); if (!$actualHash) { return false; } return $this->hasher->compare($actualHash, $parsedFileID->getHash()); } /** * Get the full hash for the provided Parsed File ID, * @param FileIDHelper $helper * @param ParsedFileID $parsedFileID * @param Filesystem $filesystem * @return bool|string * @throws \League\Flysystem\FileNotFoundException */ private function findHashOf(FileIDHelper $helper, ParsedFileID $parsedFileID, Filesystem $filesystem) { // Re build the file ID but without the variant $fileID = $helper->buildFileID( $parsedFileID->getFilename(), $parsedFileID->getHash(), '', false ); // Couldn't find the original file, let's bail. if (!$filesystem->has($fileID)) { return false; } // Get hash from file $fullHash = $this->hasher->computeFromFile($fileID, $filesystem); return $fullHash; } /** * Receive a tuple under various formats and normalise it back to a ParsedFileID object. * @param $tuple * @return ParsedFileID * @throws \InvalidArgumentException */ private function preProcessTuple($tuple) { // Pre-format our tuple if ($tuple instanceof ParsedFileID) { return $tuple; } elseif (!is_array($tuple)) { throw new \InvalidArgumentException( 'AssetAdapter expect $tuples to be an array or a ParsedFileID' ); } return new ParsedFileID($tuple['Filename'], $tuple['Hash'], $tuple['Variant']); } /** * @return FileIDHelper */ public function getDefaultFileIDHelper() { return $this->defaultFileIDHelper; } /** * @param FileIDHelper $defaultFileIDHelper */ public function setDefaultFileIDHelper($defaultFileIDHelper) { $this->defaultFileIDHelper = $defaultFileIDHelper; } /** * @return FileIDHelper[] */ public function getResolutionFileIDHelpers() { return $this->resolutionFileIDHelpers; } /** * @param FileIDHelper[] $resolutionFileIDHelpers */ public function setResolutionFileIDHelpers(array $resolutionFileIDHelpers) { $this->resolutionFileIDHelpers = $resolutionFileIDHelpers; } /** * @return string */ public function getVersionedStage() { return $this->versionedStage; } /** * @param string $versionedStage */ public function setVersionedStage($versionedStage) { $this->versionedStage = $versionedStage; } public function buildFileID($tuple) { $parsedFileID = $this->preProcessTuple($tuple); return $this->getDefaultFileIDHelper()->buildFileID( $parsedFileID->getFilename(), $parsedFileID->getHash(), $parsedFileID->getVariant() ); } public function findVariants($tuple, Filesystem $filesystem) { $parsedFileID = $this->preProcessTuple($tuple); // Build a list of possible helperss to try $helpers = $this->getResolutionFileIDHelpers(); $defaultHelper = $this->getDefaultFileIDHelper(); if (!in_array($defaultHelper, $helpers)) { // If the default helper is not already in our list of resolution helpers, add it to the list array_unshift($helpers, $defaultHelper); } /** @var FileIDHelper[] $resolvableHelpers */ $resolvableHelpers = []; // Search for a helper that will allow us to find a file foreach ($helpers as $helper) { try { $fileID = $helper->buildFileID($parsedFileID->getFilename(), $parsedFileID->getHash()); if ($filesystem->has($fileID) && $this->validateHash($helper, $parsedFileID, $filesystem)) { $resolvableHelpers[] = $helper; } } catch (InvalidArgumentException $ex) { // Our helper couldn't build a FileID with the provided arguments, that means it's not a valid helper // for this file tuple. } } // Loop through the list of possible helpers foreach ($resolvableHelpers as $helper) { // Make sure our yield file has an hash $hash = $parsedFileID->getHash() ?: $this->findHashOf($helper, $parsedFileID, $filesystem); // Find the correct folder to search for possible variants in $folder = $helper->lookForVariantIn($parsedFileID); $possibleVariants = $filesystem->listContents($folder, $helper->lookForVariantRecursive()); // Flysystem returns array of meta data abouch each file, we remove directories and map it down to the path $possibleVariants = array_filter($possibleVariants, function ($possibleVariant) { return $possibleVariant['type'] !== 'dir'; }); $possibleVariants = array_map(function ($possibleVariant) { return $possibleVariant['path']; }, $possibleVariants); // Let's explicitely add the main variant to the list if need be $mainVariant = $this->stripVariantFromParsedFileID($parsedFileID, $helper)->getFileID(); if (!in_array($mainVariant, $possibleVariants) && $filesystem->has($mainVariant)) { $possibleVariants[] = $mainVariant; } // Loop through the possible variants and yield the ones that are actual variant. foreach ($possibleVariants as $possibleVariant) { if ($helper->isVariantOf($possibleVariant, $parsedFileID)) { yield $helper->parseFileID($possibleVariant)->setHash($hash); } } } } public function cleanFilename($filename) { return $this->getDefaultFileIDHelper()->cleanFilename($filename); } public function parseFileID($fileID) { foreach ($this->resolutionFileIDHelpers as $fileIDHelper) { $parsedFileID = $fileIDHelper->parseFileID($fileID); if ($parsedFileID) { return $parsedFileID; } } return null; } public function stripVariant($fileID) { $hash = ''; // File ID can be a string or a ParsedFileID // Normalise our parameters if ($fileID instanceof ParsedFileID) { // Let's get data out of our parsed file ID $parsedFileID = $fileID; $fileID = $parsedFileID->getFileID(); $hash = $parsedFileID->getHash(); // Our Parsed File ID has a blank FileID attached to it. This means we are dealing with a file that hasn't // been create yet. Let's used our default file ID helper if (empty($fileID)) { return $this->stripVariantFromParsedFileID($parsedFileID, $this->getDefaultFileIDHelper()); } } // We don't know what helper was use to build this file ID // Let's try to find a helper who can understand our file ID foreach ($this->resolutionFileIDHelpers as $fileIDHelper) { $parsedFileID = $fileIDHelper->parseFileID($fileID); if ($parsedFileID) { if ($hash && $parsedFileID->getHash() && !$this->hasher->compare($parsedFileID->getHash(), $hash)) { // Our file ID came bundled with an Hash, we got an hash from our helper, but that hash didn't // match what we were expecting. continue; } if ($hash) { $parsedFileID = $parsedFileID->setHash($hash); } return $this->stripVariantFromParsedFileID($parsedFileID, $fileIDHelper); } } return null; } /** * Convert the provided ParsedFileID to a its variantless equivalent. * * @param ParsedFileID $parsedFileID * @param FileIDHelper $helper * @return ParsedFileID */ private function stripVariantFromParsedFileID(ParsedFileID $parsedFileID, FileIDHelper $helper) { $parsedFileID = $parsedFileID->setVariant(''); try { return $parsedFileID->setFileID($helper->buildFileID($parsedFileID)); } catch (InvalidArgumentException $ex) { return null; } } } |