diff --git a/src/Bag.php b/src/Bag.php index b6943fc..4a4e3db 100644 --- a/src/Bag.php +++ b/src/Bag.php @@ -8,6 +8,9 @@ use Normalizer; use whikloj\BagItTools\Exceptions\BagItException; use whikloj\BagItTools\Exceptions\FilesystemException; +use whikloj\BagItTools\Exceptions\ProfileException; +use whikloj\BagItTools\Profiles\BagItProfile; +use whikloj\BagItTools\Profiles\ProfileFactory; use ZipArchive; /** @@ -134,14 +137,14 @@ class Bag /** * All the extensions in one array. * - * @var array + * @var array */ private array $packageExtensions; /** * Array of current bag version with keys 'major' and 'minor'. * - * @var array + * @var array */ private array $currentVersion = self::DEFAULT_BAGIT_VERSION; @@ -155,21 +158,21 @@ class Bag /** * Array of payload manifests. * - * @var array + * @var array */ private array $payloadManifests; /** * Array of tag manifests. * - * @var array + * @var array */ private array $tagManifests; /** * List of relative file paths for all files. * - * @var array + * @var array */ private array $payloadFiles; @@ -201,21 +204,21 @@ class Bag * supported by the BagIt specification. Stored to avoid extraneous calls * to hash_algos(). * - * @var array + * @var array */ private array $validHashAlgorithms; /** * Errors when validating a bag. * - * @var array + * @var array> */ private array $bagErrors; /** * Warnings when validating a bag. * - * @var array + * @var array> */ private array $bagWarnings; @@ -229,7 +232,7 @@ class Bag /** * Bag Info data. * - * @var array + * @var array> */ private array $bagInfoData = []; @@ -237,7 +240,7 @@ class Bag * Unique array of all Bag info tags/values. Tags are stored once in lower case with an array of all instances * of values. This index does not save order. * - * @var array + * @var array> */ private array $bagInfoTagIndex = []; @@ -254,6 +257,12 @@ class Bag */ private ?string $serialization = null; + /** + * Array of BagIt profiles. + * @var array + */ + private array $profiles = []; + /** * Bag constructor. * @@ -361,6 +370,16 @@ public function isValid(): bool $this->mergeErrors($manifest->getErrors()); $this->mergeWarnings($manifest->getWarnings()); } + foreach ($this->profiles as $profile) { + try { + $profile->validateBag($this); + } catch (ProfileException $e) { + $this->addBagError( + $profile->getProfileIdentifier(), + $e->getMessage() + ); + } + } return (count($this->bagErrors) == 0); } @@ -1276,10 +1295,74 @@ public function getSerializationMimeType(): ?string return $this->serialization; } + /** + * @return array The profiles that have been added to the bag. + */ + public function getBagProfiles(): array + { + return $this->profiles; + } + + /** + * Add a profile to the bag. + * @param string $url The URL to the profile. + * @throws Exceptions\ProfileException If the profile cannot be loaded or parsed. + */ + public function addBagProfileByURL(string $url): void + { + $this->addBagProfileInternal(ProfileFactory::generateProfileFromUri($url)); + } + + /** + * Add a profile to the bag. + * @param string $json The JSON representation of the profile. + * @throws Exceptions\ProfileException If the profile cannot be parsed. + */ + public function addBagProfileByJson(string $json): void + { + $this->addBagProfileInternal(BagItProfile::fromJson($json)); + } + + /** + * Remove a profile from the bag. + * @param string $profileId The identifier of the profile to remove. + */ + public function removeBagProfile(string $profileId): void + { + if (array_key_exists($profileId, $this->profiles)) { + unset($this->profiles[$profileId]); + $this->changed = true; + } + } + + /** + * Clear all profiles from the bag. + */ + public function clearAllProfiles(): void + { + if (count($this->profiles) > 0) { + $this->profiles = []; + $this->changed = true; + } + } + /* * XXX: Private functions */ + /** + * Add a profile to the bag if it doesn't already exist. + * @param BagItProfile $profile The profile to add. + */ + private function addBagProfileInternal(BagItProfile $profile): void + { + if (!array_key_exists($profile->getProfileIdentifier(), $this->profiles)) { + $this->setExtended(true); + $this->profiles[$profile->getProfileIdentifier()] = $profile; + $this->changed = true; + } + } + /** * Common checks for interactions with custom tag files. * @param string $tagFilePath The relative path to the tag file. @@ -1963,8 +2046,8 @@ function (&$item) { ); } else { $this->currentVersion = [ - 'major' => $match[1], - 'minor' => $match[2], + 'major' => (int)$match[1], + 'minor' => (int)$match[2], ]; } if ( @@ -2188,7 +2271,12 @@ private static function untarBag(string $filename): string private static function extensionTarCompression(string $filename): ?string { $filename = strtolower(basename($filename)); - return (str_ends_with($filename, '.bz2') ? 'bz2' : (str_ends_with($filename, 'gz') ? 'gz' : null)); + if (str_ends_with($filename, '.bz2')) { + return 'bz2'; + } elseif (str_ends_with($filename, '.gz') || str_ends_with($filename, '.tgz')) { + return 'gz'; + } + return null; } /** diff --git a/src/Profiles/BagItProfile.php b/src/Profiles/BagItProfile.php index 3c6ea7c..a6991b1 100644 --- a/src/Profiles/BagItProfile.php +++ b/src/Profiles/BagItProfile.php @@ -923,8 +923,7 @@ public function validateBag(Bag $bag): bool } if ($this->getManifestsRequired() !== []) { $manifests = array_keys($bag->getPayloadManifests()); - $diff = array_diff($manifests, $this->getManifestsRequired()) + - array_diff($this->getManifestsRequired(), $manifests); + $diff = array_diff($this->getManifestsRequired(), $manifests); if ($diff !== []) { $errors[] = "Profile requires payload manifest(s) which are missing from the bag (" . implode(", ", $diff) . ")"; @@ -940,8 +939,7 @@ public function validateBag(Bag $bag): bool } if ($this->getTagManifestsRequired() !== []) { $manifests = array_keys($bag->getTagManifests()); - $diff = array_diff($manifests, $this->getTagManifestsRequired()) + - array_diff($this->getTagManifestsRequired(), $manifests); + $diff = array_diff($this->getTagManifestsRequired(), $manifests); if ($diff !== []) { $errors[] = "Profile requires tag manifest(s) which are missing from the bag (" . implode(", ", $diff) . ")"; @@ -959,8 +957,7 @@ public function validateBag(Bag $bag): bool // Grab the first tag manifest, they should all be the same $manifests = $bag->getTagManifests()[0]; $tag_files = array_keys($manifests->getHashes()); - $diff = array_diff($this->getTagFilesRequired(), $tag_files) + - array_diff($tag_files, $this->getTagFilesRequired()); + $diff = array_diff($this->getTagFilesRequired(), $tag_files); if ($diff !== []) { $errors[] = "Profile requires tag files(s) which are missing from the bag (" . implode(", ", $diff) . ")"; @@ -980,8 +977,7 @@ public function validateBag(Bag $bag): bool // Grab the first tag manifest, they should all be the same $manifests = $bag->getPayloadManifests()[0]; $payload_files = array_keys($manifests->getHashes()); - $diff = array_diff($this->getPayloadFilesRequired(), $payload_files) + - array_diff($payload_files, $this->getPayloadFilesRequired()); + $diff = array_diff($this->getPayloadFilesRequired(), $payload_files); if ($diff !== []) { $errors[] = "Profile requires payload file(s) which are missing from the bag (" . implode(", ", $diff) . ")"; diff --git a/tests/BagItWebserverFramework.php b/tests/BagItWebserverFramework.php index 56ad14f..4532047 100644 --- a/tests/BagItWebserverFramework.php +++ b/tests/BagItWebserverFramework.php @@ -5,10 +5,25 @@ use donatj\MockWebServer\MockWebServer; use donatj\MockWebServer\Response; +/** + * Class to setup a mock webserver for testing remote file downloads. + * @package whikloj\BagItTools\Test + * @since 5.0.0 + * + * To use this abstract class, extend it and then implement the setupBeforeClass methods, define the webserver_files + * variable and then call the parent::setUpBeforeClass() method. + */ abstract class BagItWebserverFramework extends BagItTestFramework { /** * Array of remote files defined in mock webserver. + * Outside key is a unique identifier, keys for the inside array are: + * filename (string) - path to file with response contents + * headers (array) - headers to return in response + * status_code (int) - status code + * content (string) - string to return, used instead of filename + * path (path) - the path of the URL, used if filename not defined. Otherwise is basename(filename) + * @var array> */ protected static array $webserver_files = []; @@ -22,19 +37,20 @@ abstract class BagItWebserverFramework extends BagItTestFramework /** * Array of file contents for use with comparing against requests against the same index in self::$remote_urls * - * @var string|array|false + * @var array */ - protected static string|array|false $response_content = []; + protected static array $response_content = []; /** * Array of mock urls to get responses from. Match response bodies against matching key in self::$response_content * - * @var string|array + * @var array */ - protected static string|array $remote_urls = []; + protected static array $remote_urls = []; /** * {@inheritdoc} + * NOTE: You should override this in your class, define self::$webserver_files, then call parent::setUpBeforeClass */ public static function setUpBeforeClass(): void { @@ -42,14 +58,17 @@ public static function setUpBeforeClass(): void self::$webserver->start(); $counter = 0; foreach (self::$webserver_files as $file) { - self::$response_content[$counter] = file_get_contents($file['filename']); + self::$response_content[$counter] = $file['content'] ?? file_get_contents($file['filename']); // Add custom headers if defined. - $response_headers = [ 'Cache-Control' => 'no-cache', 'Content-Length' => stat($file['filename'])['size']] + - ($file['headers'] ?? []); + $response_headers = [ + 'Cache-Control' => 'no-cache', + 'Content-Length' => isset($file['content']) ? strlen($file['content']) : + stat($file['filename'])['size'] + ] + ($file['headers'] ?? []); // Use custom status code if defined. $status_code = $file['status_code'] ?? 200; self::$remote_urls[$counter] = self::$webserver->setResponseOfPath( - "/example/" . basename($file['filename']), + "/" . ($file['path'] ?? "example/" . basename($file['filename'])), new Response( self::$response_content[$counter], $response_headers, diff --git a/tests/Profiles/BagProfileTest.php b/tests/Profiles/BagProfileTest.php index 80018e9..639fca5 100644 --- a/tests/Profiles/BagProfileTest.php +++ b/tests/Profiles/BagProfileTest.php @@ -365,6 +365,11 @@ public function testTagManifestAllowed(): void $this->assertArrayEquals(["md5"], $profile->getTagManifestsAllowed()); } + /** + * @group Profiles + * @covers ::setTagFilesAllowed + * @covers ::getTagFilesRequired + */ public function testTagFilesMissingFromAllowed(): void { $profileJson = <<< JSON @@ -393,4 +398,92 @@ public function testTagFilesMissingFromAllowed(): void $profile = BagItProfile::fromJson($profileJson); $this->assertTrue($profile->isValid()); } + + /** + * @group Profiles + * @covers \whikloj\BagItTools\Bag::isValid + * @covers \whikloj\BagItTools\Bag::addBagProfileByJson + * @covers ::validateBag + */ + public function testAddProfileToBag(): void + { + $profileJson = <<assertTrue($profile->isValid()); + $bag = Bag::create($this->tmpdir); + $bag->addBagProfileByJson($profileJson); + $this->assertFalse($bag->isValid()); + $bag->addBagInfoTag("Source-Organization", "Simon Fraser University"); + $bag->addBagInfoTag("Contact-Phone", "555-555-5555"); + $this->assertFalse($bag->isValid()); + $this->assertCount(1, $bag->getErrors()); + $error = $bag->getErrors()[0]; + $this->assertEquals( + [ + "file" => "http://example.profile.org/bagit-test-profile.json", + "message" => "Profile requires payload manifest(s) which are missing from the bag (md5)" + ], + $error + ); + $bag->setAlgorithm("md5"); + $this->assertTrue($bag->isValid()); + } + + /** + * @group Profiles + * @covers \whikloj\BagItTools\Bag::addBagProfileByJson + * @covers \whikloj\BagItTools\Bag::addBagProfileInternal + * @covers \whikloj\BagItTools\Bag::removeBagProfile + */ + public function testAddSameProfileTwice(): void + { + $profileJson = file_get_contents(self::$profiles . "/bagProfileFoo.json"); + $bag = Bag::create($this->tmpdir); + $this->assertCount(0, $bag->getBagProfiles()); + $bag->addBagProfileByJson($profileJson); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->addBagProfileByJson($profileJson); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->removeBagProfile("http://some.incorrect.identifier"); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->removeBagProfile("http://www.library.yale.edu/mssa/bagitprofiles/disk_images.json"); + $this->assertCount(0, $bag->getBagProfiles()); + } } diff --git a/tests/Profiles/ProfileWebTests.php b/tests/Profiles/ProfileWebTests.php new file mode 100644 index 0000000..6167207 --- /dev/null +++ b/tests/Profiles/ProfileWebTests.php @@ -0,0 +1,103 @@ + [ + 'content' => $profileJson, + 'path' => 'bagit-test-profile.json', + ], + ]; + parent::setUpBeforeClass(); + } + + public function testAddProfileToBagUri(): void + { + $profile = ProfileFactory::generateProfileFromUri(self::$remote_urls[0]); + $this->assertTrue($profile->isValid()); + $bag = Bag::create($this->tmpdir); + $bag->addBagProfileByURL(self::$remote_urls[0]); + $this->assertFalse($bag->isValid()); + $bag->addBagInfoTag("Source-Organization", "Simon Fraser University"); + $bag->addBagInfoTag("Contact-Phone", "555-555-5555"); + $this->assertFalse($bag->isValid()); + $this->assertCount(1, $bag->getErrors()); + $error = $bag->getErrors()[0]; + $this->assertEquals( + [ + "file" => "http://example.profile.org/bagit-test-profile.json", + "message" => "Profile requires payload manifest(s) which are missing from the bag (md5)" + ], + $error + ); + $bag->setAlgorithm("md5"); + $this->assertTrue($bag->isValid()); + } + + /** + * @group Profiles + * @covers \whikloj\BagItTools\Bag::addBagProfileByJson + * @covers \whikloj\BagItTools\Bag::addBagProfileInternal + * @covers \whikloj\BagItTools\Bag::removeBagProfile + */ + public function testAddSameProfileTwiceByUri(): void + { + $bag = Bag::create($this->tmpdir); + $this->assertCount(0, $bag->getBagProfiles()); + $bag->addBagProfileByURL(self::$remote_urls[0]); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->addBagProfileByURL(self::$remote_urls[0]); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->removeBagProfile("http://some.incorrect.identifier"); + $this->assertCount(1, $bag->getBagProfiles()); + $bag->removeBagProfile("http://www.library.yale.edu/mssa/bagitprofiles/disk_images.json"); + $this->assertCount(0, $bag->getBagProfiles()); + } +}