html
##########plain', 'Subject'); + + $this->assertSame(0, $mailCallCount); + } + + public function testSendMessageCallsSendMailWhenMailIsConfigured(): void { + $mailCalls = []; + $errorLogMessages = []; + + \hashtopolis_set_test_mock('Hashtopolis\\inc\\is_file', static function ($path): bool { + return true; + }); + \hashtopolis_set_test_mock('Hashtopolis\\inc\\mail', static function ($to, $subject, $message, $additionalHeaders = null, $additionalParams = null) use (&$mailCalls): bool { + $mailCalls[] = [$to, $subject, $message, $additionalHeaders, $additionalParams]; + return true; + }); + + $notification = $this->createNotification(); + $notification->sendMessage('html
##########plain', 'Subject'); + + $this->assertCount(1, $mailCalls); + $this->assertSame('receiver@example.com', $mailCalls[0][0]); + $this->assertSame('Subject', $mailCalls[0][1]); + $this->assertStringContainsString('html
', $mailCalls[0][2]); + $this->assertStringContainsString('plain', $mailCalls[0][2]); + } + + public function testSendMessageThrowsWhenConfiguredSendMailFails(): void { + $mailCallCount = 0; + + \hashtopolis_set_test_mock('Hashtopolis\\inc\\is_file', static function ($path): bool { + return true; + }); + \hashtopolis_set_test_mock('Hashtopolis\\inc\\mail', static function () use (&$mailCallCount): bool { + $mailCallCount++; + return false; + }); + + $notification = $this->createNotification(); + $this->expectException(\Exception::class); + try { + $notification->sendMessage('html
##########plain', 'Subject'); + } + finally { + $this->assertSame(1, $mailCallCount); + } + } + + private function createNotification(): HashtopolisNotificationEmail { + $notification = new HashtopolisNotificationEmail(); + $receiverProperty = new \ReflectionProperty($notification, 'receiver'); + $receiverProperty->setValue($notification, 'receiver@example.com'); + return $notification; + } +} + diff --git a/ci/phpunit/inc/utils/AccessControlTest.php b/ci/phpunit/inc/utils/AccessControlTest.php new file mode 100644 index 000000000..32e06fdd4 --- /dev/null +++ b/ci/phpunit/inc/utils/AccessControlTest.php @@ -0,0 +1,231 @@ +resetAccessControlInstance(); + $this->resetLoginInstance(); + } + + protected function tearDown(): void { + parent::tearDown(); + } + + public function testGetInstanceWithoutArgsReusesSameObject(): void { + $first = AccessControl::getInstance(); + $second = AccessControl::getInstance(); + + $this->assertInstanceOf(AccessControl::class, $first); + $this->assertNull($first->getUser()); + + $this->assertSame($first, $second); + } + + public function testGetInstanceWithGroupIdOverwritesInstance(): void { + $first = AccessControl::getInstance(); + $second = AccessControl::getInstance(null, 1); + + $this->assertInstanceOf(AccessControl::class, $first); + $this->assertNull($first->getUser()); + + $this->assertInstanceOf(AccessControl::class, $second); + $this->assertNull($second->getUser()); + + $this->assertNotSame($first, $second); + } + + public function testGetInstanceWithUserOverwritesInstance(): void { + $first = AccessControl::getInstance(); + $second = AccessControl::getInstance($this->adminUser); + + $this->assertInstanceOf(AccessControl::class, $first); + $this->assertNull($first->getUser()); + + $this->assertInstanceOf(AccessControl::class, $second); + $this->assertEquals($this->adminUser, $second->getUser()); + + $this->assertNotSame($first, $second); + } + + public function testReloadReloadsTheRightsGroupForUser(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), '{}') + ); + + $user = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $group->getId(), '', '', '', '', '') + ); + + $accessControl = AccessControl::getInstance($user); + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + Factory::getRightGroupFactory()->set( + $group, + RightGroup::PERMISSIONS, + json_encode([DAccessControl::MANAGE_TASK_ACCESS => true]) + ); + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + + $accessControl->reload(); + $this->assertTrue($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + } + + public function testReloadDoesNotReloadTheRightsGroupWithoutUser(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), '{}') + ); + + $accessControl = AccessControl::getInstance(null, $group->getId()); + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + + Factory::getRightGroupFactory()->set( + $group, + RightGroup::PERMISSIONS, + json_encode([DAccessControl::MANAGE_TASK_ACCESS => true]) + ); + + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + + $accessControl->reload(); + + // TODO: Check if this is the desired behavour, ie not reloading if a groupId only. + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + } + + public function testPermissionPublicAccessAlwaysPermit(): void { + $accessControl = AccessControl::getInstance(); + $this->assertTrue($accessControl->hasPermission(DAccessControl::PUBLIC_ACCESS)); + } + + public function testPermissionLoginWithLoggedInUserPermit(): void { + $this->setLoginState(true, $this->adminUser); + $accessControl = AccessControl::getInstance(); + $this->assertTrue($accessControl->hasPermission(DAccessControl::LOGIN_ACCESS)); + } + + public function testPermissionLoginWithoutLoggedInUserDenies(): void { + $accessControl = AccessControl::getInstance(); + $this->assertFalse($accessControl->hasPermission(DAccessControl::LOGIN_ACCESS)); + } + + public function testUninitializedAccessControlDenies() { + $accessControl = AccessControl::getInstance(); + foreach(DAccessControl::getConstants() as $constant) { + $permission = is_array($constant) ? $constant[0] : $constant; + if ($permission != DAccessControl::PUBLIC_ACCESS) { + $this->assertFalse($accessControl->hasPermission($permission)); + } + } + } + + public function testRegularUserWithPermissionPermits(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), json_encode([DAccessControl::MANAGE_TASK_ACCESS => true])) + ); + + $user = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $group->getId(), '', '', '', '', '') + ); + + $accessControl = AccessControl::getInstance($user); + $this->assertTrue($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + } + + public function testRegularUserWithoutPermissionDenies(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), json_encode([DAccessControl::VIEW_HASHES_ACCESS => true])) + ); + + $user = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $group->getId(), '', '', '', '', '') + ); + + $accessControl = AccessControl::getInstance($user); + $this->assertFalse($accessControl->hasPermission(DAccessControl::MANAGE_TASK_ACCESS)); + } + + public function testALLUserPermissionPermitsAllPermissions(): void { + $accessControl = AccessControl::getInstance($this->adminUser); + foreach(DAccessControl::getConstants() as $constant) { + $permission = is_array($constant) ? $constant[0] : $constant; + $this->assertTrue($accessControl->hasPermission($permission)); + } + } + + public function testGivenByDependencyImplied(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), json_encode([DAccessControl::MANAGE_AGENT_ACCESS => true])) + ); + + $accessControl = AccessControl::getInstance(null, $group->getId()); + + $this->assertTrue($accessControl->givenByDependency(DAccessControl::VIEW_AGENT_ACCESS[0])); + } + + public function testGivenByDependencyDirect(): void { + $group = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup( + null, + 'phpunit-' . uniqid('', true), + json_encode([DAccessControl::MANAGE_TASK_ACCESS => true]) + ) + ); + + $accessControl = AccessControl::getInstance(null, $group->getId()); + + $this->assertTrue($accessControl->givenByDependency(DAccessControl::MANAGE_TASK_ACCESS)); + } + + /* + Local test helpers + */ + private function resetAccessControlInstance(): void { + $reflection = new ReflectionClass(AccessControl::class); + $instanceProperty = $reflection->getProperty('instance'); + $instanceProperty->setValue(null, null); + } + + private function setLoginState(bool $valid, ?User $user = null): void { + $reflection = new ReflectionClass(\Hashtopolis\inc\Login::class); + $instanceProperty = $reflection->getProperty('instance'); + $instance = $instanceProperty->getValue(); + + if ($instance === null) { + \Hashtopolis\inc\Login::getInstance(); + $instance = $instanceProperty->getValue(); + } + + $validProperty = $reflection->getProperty('valid'); + $validProperty->setValue($instance, $valid); + + $userProperty = $reflection->getProperty('user'); + $userProperty->setValue($instance, $user); + } + + private function resetLoginInstance(): void { + $reflection = new ReflectionClass(\Hashtopolis\inc\Login::class); + $instanceProperty = $reflection->getProperty('instance'); + $instanceProperty->setValue(null, null); + } +} \ No newline at end of file diff --git a/ci/phpunit/inc/utils/AccessControlUtilsTest.php b/ci/phpunit/inc/utils/AccessControlUtilsTest.php new file mode 100644 index 000000000..9491c80d2 --- /dev/null +++ b/ci/phpunit/inc/utils/AccessControlUtilsTest.php @@ -0,0 +1,269 @@ +createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), '[]') + ); + $this->assertTrue($group instanceof RightGroup); + $this->group = $group; + + $otherGroup = $this->createDatabaseObject( + Factory::getRightGroupFactory(), + new RightGroup(null, 'phpunit-' . uniqid('', true), '[]') + ); + $this->assertTrue($otherGroup instanceof RightGroup); + $this->otherGroup = $otherGroup; + } + + #[Override] + protected function tearDown(): void{ + parent::tearDown(); + } + + + + public function testGetMembersOfGroupReturnsOnlyMembersOfGroup(): void { + $firstMember = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $this->group->getId(), '', '', '', '', '') + ); + + $secondMember = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $this->group->getId(), '', '', '', '', '') + ); + + $otherMember = $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $this->otherGroup->getId(), '', '', '', '', '') + ); + + $members = AccessControlUtils::getMembers($this->group->getId()); + $memberIds = array_map(static fn (User $user): int => $user->getId(), $members); + + $this->assertCount(2, $members); + $this->assertContains($firstMember->getId(), $memberIds); + $this->assertContains($secondMember->getId(), $memberIds); + $this->assertNotContains($otherMember->getId(), $memberIds); + } + + public function testThatAdminGroupPermissionCanNotBeAltered(): void { + $this->expectException(HTException::class); + AccessControlUtils::addToPermissions( + $this->adminUser->getRightGroupId(), + [DAccessControl::MANAGE_TASK_ACCESS => true] + ); + } + + public function testAddPermissionsToNonExistentGroup(): void { + $this->expectException(HTException::class); + AccessControlUtils::addToPermissions( + -3, + [DAccessControl::MANAGE_TASK_ACCESS => true] + ); + } + + public function testGetGroupLoadsExistingGroup(): void { + $loadedGroup = AccessControlUtils::getGroup($this->group->getId()); + + $this->assertInstanceOf(RightGroup::class, $loadedGroup); + $this->assertSame($this->group->getId(), $loadedGroup->getId()); + $this->assertSame($this->group->getGroupName(), $loadedGroup->getGroupName()); + } + + public function testGetGroupThrowsForNonExistentGroup(): void { + $this->expectException(HTException::class); + AccessControlUtils::getGroup(-3); + } + + public function testAddToPermissionThrowsOnNonExistentGroup(): void { + $this->expectException(HTException::class); + AccessControlUtils::addToPermissions( + -3, + [DAccessControl::MANAGE_TASK_ACCESS => true], + ); + } + + public function testAddPermissionToGroup(): void { + AccessControlUtils::addToPermissions( + $this->group->getId(), + [DAccessControl::MANAGE_TASK_ACCESS => true], + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertIsArray($permissions); + $this->assertArrayHasKey(DAccessControl::MANAGE_TASK_ACCESS, $permissions); + $this->assertTrue($permissions[DAccessControl::MANAGE_TASK_ACCESS]); + } + + //Note: We do not enforce what to write in the permissions + public function testAddNonExistentPermissionToGroup(): void { + $nonexistentPermission = "nonexistent"; + AccessControlUtils::addToPermissions( + $this->group->getId(), + [$nonexistentPermission => true], + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertIsArray($permissions); + $this->assertArrayHasKey($nonexistentPermission, $permissions); + } + + public function testUpdateNonexistentGroupThrowsException() { + $this->expectException(HTException::class); + AccessControlUtils::updateGroupPermissions( + -3, + [DAccessControl::CRACKER_BINARY_ACCESS => true], + ); + } + + public function testUpdateAdminPermissionsIsNotAllowed(): void { + $this->expectException(HTException::class); + AccessControlUtils::updateGroupPermissions( + $this->adminUser->getRightGroupId(), + [DAccessControl::CRACKER_BINARY_ACCESS => true], + ); + } + + public function testUpdatePermission() { + $changed = AccessControlUtils::updateGroupPermissions( + $this->group->getId(), + [DAccessControl::MANAGE_TASK_ACCESS . '-1'] + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertFalse($changed); + $this->assertIsArray($permissions); + $this->assertArrayHasKey(DAccessControl::MANAGE_TASK_ACCESS, $permissions); + $this->assertTrue($permissions[DAccessControl::MANAGE_TASK_ACCESS]); + } + + public function testUpdatePermissionIgnoresValidPermissionWithInvalidInteger(): void { + $changed = AccessControlUtils::updateGroupPermissions( + $this->group->getId(), + [DAccessControl::MANAGE_TASK_ACCESS . '-2'] + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertFalse($changed); + $this->assertSame([], $permissions); + } + + public function testUpdatePermissionIgnoresInvalidPermissionWithValidInteger(): void { + $changed = AccessControlUtils::updateGroupPermissions( + $this->group->getId(), + ['nonexistentPermission-1'] + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertFalse($changed); + $this->assertSame([], $permissions); + } + + public function testUpdatePermissionAppliesDependencyOverride(): void { + $changed = AccessControlUtils::updateGroupPermissions( + $this->group->getId(), + [ + DAccessControl::MANAGE_AGENT_ACCESS . '-1', + DAccessControl::VIEW_AGENT_ACCESS[0] . '-0' + ] + ); + + $updatedGroup = Factory::getRightGroupFactory()->get($this->group->getId()); + $permissions = json_decode($updatedGroup->getPermissions(), true); + + $this->assertTrue($changed); + $this->assertIsArray($permissions); + $this->assertArrayHasKey(DAccessControl::MANAGE_AGENT_ACCESS, $permissions); + $this->assertTrue($permissions[DAccessControl::MANAGE_AGENT_ACCESS]); + $this->assertArrayHasKey(DAccessControl::VIEW_AGENT_ACCESS[0], $permissions); + $this->assertTrue($permissions[DAccessControl::VIEW_AGENT_ACCESS[0]]); + } + + public function testCreateGroupThrowsForEmptyName(): void { + $this->expectException(HttpError::class); + + AccessControlUtils::createGroup(''); + } + + public function testCreateGroupThrowsForNameLongerThanMaxLength(): void { + $this->expectException(HttpError::class); + + AccessControlUtils::createGroup(str_repeat('a', DLimits::ACCESS_GROUP_MAX_LENGTH + 1)); + } + + public function testCreateGroupAllowsNameAtMaxLength(): void { + $group = AccessControlUtils::createGroup(str_repeat('a', DLimits::ACCESS_GROUP_MAX_LENGTH)); + $this->registerDatabaseObject(Factory::getRightGroupFactory(), $group); + + $this->assertInstanceOf(RightGroup::class, $group); + $this->assertSame(DLimits::ACCESS_GROUP_MAX_LENGTH, strlen($group->getGroupName())); + } + + public function testCreateGroupThrowsForExistingGroupName(): void { + $this->expectException(HttpConflict::class); + + AccessControlUtils::createGroup($this->group->getGroupName()); + } + + public function testDeleteGroupThrowsForNonExistentGroup(): void { + $this->expectException(HTException::class); + + AccessControlUtils::deleteGroup(-3); + } + + public function testDeleteGroupThrowsWhenGroupHasUsers(): void { + $this->createDatabaseObject( + Factory::getUserFactory(), + new User(null, 'phpunit_' . uniqid(), 'phpunit_' . uniqid() . '@example.com', 'hash', 'salt', 1, 0, 0, time(), 3600, $this->group->getId(), '', '', '', '', '') + ); + + $this->expectException(HttpError::class); + + AccessControlUtils::deleteGroup($this->group->getId()); + } + + public function testDeleteGroupDeletesEmptyGroup(): void { + $groupId = $this->group->getId(); + + AccessControlUtils::deleteGroup($groupId); + + $this->expectException(HTException::class); + AccessControlUtils::getGroup($groupId); + } + +} \ No newline at end of file diff --git a/ci/phpunit/inc/utils/AccessGroupUtilsTest.php b/ci/phpunit/inc/utils/AccessGroupUtilsTest.php new file mode 100644 index 000000000..e91f3b6a2 --- /dev/null +++ b/ci/phpunit/inc/utils/AccessGroupUtilsTest.php @@ -0,0 +1,285 @@ +firstGroup = $this->createAccessGroup('group_one'); + $this->secondGroup = $this->createAccessGroup('group_two'); + $this->firstUser = $this->createUser('user_one'); + $this->secondUser = $this->createUser('user_two'); + $this->firstAgent = $this->createAgent('agent_one'); + $this->secondAgent = $this->createAgent('agent_two'); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $this->firstGroup->getId(), $this->firstUser->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $this->secondGroup->getId(), $this->secondUser->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $this->firstGroup->getId(), $this->firstAgent->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $this->secondGroup->getId(), $this->secondAgent->getId()) + ); + } + + #[Override] + protected function tearDown(): void { + parent::tearDown(); + } + + public function testGetUsersReturnsUsersAssignedToRequestedGroup(): void { + $users = AccessGroupUtils::getUsers($this->firstGroup->getId()); + + $this->assertCount(1, $users); + $this->assertSame($this->firstGroup->getId(), $users[0]->getAccessGroupId()); + $this->assertSame($this->firstUser->getId(), $users[0]->getUserId()); + } + + public function testGetAgentsReturnsAgentsAssignedToRequestedGroup(): void { + $agents = AccessGroupUtils::getAgents($this->firstGroup->getId()); + + $this->assertCount(1, $agents); + $this->assertSame($this->firstGroup->getId(), $agents[0]->getAccessGroupId()); + $this->assertSame($this->firstAgent->getId(), $agents[0]->getAgentId()); + } + + public function testGetGroupsReturnsAtLeastCreatedGroups(): void { + $groups = AccessGroupUtils::getGroups(); + $groupIds = array_map( + fn (AccessGroup $group) => $group->getId(), + $groups + ); + + $this->assertContains($this->firstGroup->getId(), $groupIds); + $this->assertContains($this->secondGroup->getId(), $groupIds); + } + + public function testCreateGroupThrowsForEmptyName(): void { + $this->expectException(HttpError::class); + AccessGroupUtils::createGroup(''); + } + + public function testCreateGroupThrowsForNameLongerThanMaxLength(): void { + $this->expectException(HttpError::class); + AccessGroupUtils::createGroup(str_repeat('a', DLimits::ACCESS_GROUP_MAX_LENGTH + 1)); + } + + public function testCreateGroupThrowsForExistingGroupName(): void { + $this->expectException(HttpConflict::class); + AccessGroupUtils::createGroup($this->firstGroup->getGroupName()); + } + + public function testCreateGroupCreatesGroupWithValidUniqueName(): void { + $groupName = 'created_group_' . uniqid(); + + $group = AccessGroupUtils::createGroup($groupName); + $this->registerDatabaseObject(Factory::getAccessGroupFactory(), $group); + + $this->assertInstanceOf(AccessGroup::class, $group); + $this->assertSame($groupName, $group->getGroupName()); + $this->assertNotNull($group->getId()); + $this->assertSame($groupName, Factory::getAccessGroupFactory()->get($group->getId())->getGroupName()); + } + + public function testRenameThrowsForNonExistentGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::rename(-1, 'renamed_group'); + } + + public function testRenameThrowsForEmptyName(): void { + $this->expectException(HTException::class); + AccessGroupUtils::rename($this->firstGroup->getId(), ''); + } + + public function testAbortChunksGroupThrowsForNonExistentGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::abortChunksGroup(-1, $this->firstUser); + } + + public function testAbortChunksGroupOnlyAbortsInitAndRunningChunks(): void { + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($this->firstGroup, $hashType); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $taskWrapper = $this->createTaskWrapper($this->firstGroup, $hashlist); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + $statusCases = [ + DHashcatStatus::INIT => DHashcatStatus::ABORTED, + DHashcatStatus::AUTOTUNE => DHashcatStatus::AUTOTUNE, + DHashcatStatus::RUNNING => DHashcatStatus::ABORTED, + DHashcatStatus::PAUSED => DHashcatStatus::PAUSED, + DHashcatStatus::EXHAUSTED => DHashcatStatus::EXHAUSTED, + DHashcatStatus::CRACKED => DHashcatStatus::CRACKED, + DHashcatStatus::ABORTED => DHashcatStatus::ABORTED, + DHashcatStatus::QUIT => DHashcatStatus::QUIT, + DHashcatStatus::BYPASS => DHashcatStatus::BYPASS, + DHashcatStatus::ABORTED_CHECKPOINT => DHashcatStatus::ABORTED_CHECKPOINT, + DHashcatStatus::STATUS_ABORTED_RUNTIME => DHashcatStatus::STATUS_ABORTED_RUNTIME, + ]; + + + $chunksByState = []; + foreach ($statusCases as $initialState => $expectedState) { + $chunksByState[$initialState] = $this->createChunk($task, $this->firstAgent, $initialState); + } + + AccessGroupUtils::abortChunksGroup($this->firstGroup->getId(), $this->firstUser); + + foreach ($statusCases as $initialState => $expectedState) { + $updatedChunk = Factory::getChunkFactory()->get($chunksByState[$initialState]->getId()); + + $this->assertInstanceOf(Chunk::class, $updatedChunk); + $this->assertSame($expectedState, $updatedChunk->getState()); + } + } + + public function testAddAgentAddsAgentToGroup(): void { + AccessGroupUtils::addAgent($this->secondAgent->getId(), $this->firstGroup->getId()); + + $qF1 = new QueryFilter(AccessGroupAgent::ACCESS_GROUP_ID, $this->firstGroup->getId(), '='); + $qF2 = new QueryFilter(AccessGroupAgent::AGENT_ID, $this->secondAgent->getId(), '='); + $addedMembership = Factory::getAccessGroupAgentFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(AccessGroupAgent::class, $addedMembership); + $this->assertSame($this->firstGroup->getId(), $addedMembership->getAccessGroupId()); + $this->assertSame($this->secondAgent->getId(), $addedMembership->getAgentId()); + $this->registerDatabaseObject(Factory::getAccessGroupAgentFactory(), $addedMembership); + } + + public function testAddAgentThrowsWhenAgentAlreadyInGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::addAgent($this->firstAgent->getId(), $this->firstGroup->getId()); + } + + public function testAddUserAddsUserToGroup(): void { + AccessGroupUtils::addUser($this->secondUser->getId(), $this->firstGroup->getId()); + + $qF1 = new QueryFilter(AccessGroupUser::ACCESS_GROUP_ID, $this->firstGroup->getId(), '='); + $qF2 = new QueryFilter(AccessGroupUser::USER_ID, $this->secondUser->getId(), '='); + $addedMembership = Factory::getAccessGroupUserFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(AccessGroupUser::class, $addedMembership); + $this->assertSame($this->firstGroup->getId(), $addedMembership->getAccessGroupId()); + $this->assertSame($this->secondUser->getId(), $addedMembership->getUserId()); + $this->registerDatabaseObject(Factory::getAccessGroupUserFactory(), $addedMembership); + } + + public function testAddUserThrowsWhenUserAlreadyInGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::addUser($this->firstUser->getId(), $this->firstGroup->getId()); + } + + public function testRemoveAgentRemovesAgentFromGroup(): void { + AccessGroupUtils::removeAgent($this->firstAgent->getId(), $this->firstGroup->getId()); + + $qF1 = new QueryFilter(AccessGroupAgent::ACCESS_GROUP_ID, $this->firstGroup->getId(), '='); + $qF2 = new QueryFilter(AccessGroupAgent::AGENT_ID, $this->firstAgent->getId(), '='); + $removedMembership = Factory::getAccessGroupAgentFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertNull($removedMembership); + } + + public function testRemoveAgentThrowsWhenAgentIsNotInGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::removeAgent($this->secondAgent->getId(), $this->firstGroup->getId()); + } + + public function testRemoveUserRemovesUserFromGroup(): void { + AccessGroupUtils::removeUser($this->firstUser->getId(), $this->firstGroup->getId()); + + $qF1 = new QueryFilter(AccessGroupUser::ACCESS_GROUP_ID, $this->firstGroup->getId(), '='); + $qF2 = new QueryFilter(AccessGroupUser::USER_ID, $this->firstUser->getId(), '='); + $removedMembership = Factory::getAccessGroupUserFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertNull($removedMembership); + } + + public function testRemoveUserThrowsWhenUserIsNotInGroup(): void { + $this->expectException(HTException::class); + AccessGroupUtils::removeUser($this->secondUser->getId(), $this->firstGroup->getId()); + } + + public function testDeleteGroupThrowsForDefaultGroup(): void { + $defaultGroup = AccessUtils::getOrCreateDefaultAccessGroup(); + + $this->expectException(HTException::class); + AccessGroupUtils::deleteGroup($defaultGroup->getId()); + } + + public function testDeleteGroupReassignsDependentEntitiesToDefaultGroup(): void { + $defaultGroup = AccessUtils::getOrCreateDefaultAccessGroup(); + $groupToDelete = $this->createAccessGroup('delete_group_'); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $groupToDelete->getId(), $this->firstUser->getId()), + ); + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $groupToDelete->getId(), $this->firstAgent->getId()), + ); + + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($groupToDelete, $hashType); + $taskWrapper = $this->createTaskWrapper($groupToDelete, $hashlist); + $file = $this->createFile($groupToDelete); + + AccessGroupUtils::deleteGroup($groupToDelete->getId()); + + $updatedHashlist = Factory::getHashlistFactory()->get($hashlist->getId()); + $updatedTaskWrapper = Factory::getTaskWrapperFactory()->get($taskWrapper->getId()); + $updatedFile = Factory::getFileFactory()->get($file->getId()); + $deletedGroup = Factory::getAccessGroupFactory()->get($groupToDelete->getId()); + $remainingUsers = AccessGroupUtils::getUsers($groupToDelete->getId()); + $remainingAgents = AccessGroupUtils::getAgents($groupToDelete->getId()); + + $this->assertInstanceOf(Hashlist::class, $updatedHashlist); + $this->assertSame($defaultGroup->getId(), $updatedHashlist->getAccessGroupId()); + $this->assertInstanceOf(TaskWrapper::class, $updatedTaskWrapper); + $this->assertSame($defaultGroup->getId(), $updatedTaskWrapper->getAccessGroupId()); + $this->assertInstanceOf(File::class, $updatedFile); + $this->assertSame($defaultGroup->getId(), $updatedFile->getAccessGroupId()); + $this->assertNull($deletedGroup); + $this->assertSame([], $remainingUsers); + $this->assertSame([], $remainingAgents); + } +} \ No newline at end of file diff --git a/ci/phpunit/inc/utils/AccessUtilsTest.php b/ci/phpunit/inc/utils/AccessUtilsTest.php new file mode 100644 index 000000000..d0ea3f8e5 --- /dev/null +++ b/ci/phpunit/inc/utils/AccessUtilsTest.php @@ -0,0 +1,429 @@ +createAccessGroup('hashlist_access_group'); + $user = $this->createUser('hashlist_access_user'); + $hashType = $this->createHashType(); + $firstHashlist = $this->createHashlist($group, $hashType); + $secondHashlist = $this->createHashlist($group, $hashType); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + + $this->assertTrue(AccessUtils::userCanAccessHashlists([$firstHashlist, $secondHashlist], $user)); + } + + public function testUserCanAccessSingleHashlistsWhenSharesHashlistAccessGroups(): void { + $group = $this->createAccessGroup('hashlist_access_group'); + $user = $this->createUser('hashlist_access_user'); + $hashType = $this->createHashType(); + $firstHashlist = $this->createHashlist($group, $hashType); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + + $this->assertTrue(AccessUtils::userCanAccessHashlists($firstHashlist, $user)); + } + + public function testUserCanAccessTheEmptyHashlist(): void { + $user = $this->createUser('hashlist_access_user'); + $this->assertTrue(AccessUtils::userCanAccessHashlists([], $user)); + } + + /* + TODO: Passing null in an array will dereference it and throw an error. + One can fix that by filtering the array before checking, or maybe throw an Illegal arg exception. + public function testUserCanAccessHashlistsThrowsWhenArrayContainsNull(): void { + $user = $this->createUser('hashlist_access_user'); + + //$this->expectException(\Error::class); + + AccessUtils::userCanAccessHashlists([null], $user); + } + */ + + public function testGetPermissionArrayConvertedReturnsAllPermissionsAsTrueForAdmin(): void { + $permissions = AccessUtils::getPermissionArrayConverted('ALL'); + $expectedPermissions = array_unique(array_merge(...array_values(AbstractBaseAPI::$acl_mapping))); + + sort($expectedPermissions); + + $this->assertSame($expectedPermissions, array_keys($permissions)); + $this->assertNotEmpty($permissions); + foreach ($permissions as $permission => $isAllowed) { + $this->assertIsString($permission); + $this->assertTrue($isAllowed); + } + } + + /* + Sampled some cases to verify the actual mapping function, not that every permission is mapped correctly + */ + public function testGetPermissionArrayConvertedForUserPermissions(): void { + $cases = [ + 'view hashlists' => [ + 'legacyPermission' => DAccessControl::VIEW_HASHLIST_ACCESS[0], + 'expectedTrue' => [Hashlist::PERM_READ], + 'expectedFalse' => [Hashlist::PERM_CREATE, Hashlist::PERM_UPDATE, Hashlist::PERM_DELETE], + ], + 'create hashlists' => [ + 'legacyPermission' => DAccessControl::CREATE_HASHLIST_ACCESS, + 'expectedTrue' => [Hashlist::PERM_CREATE, Hash::PERM_CREATE], + 'expectedFalse' => [Hashlist::PERM_READ, Hash::PERM_READ], + ], + 'manage files' => [ + 'legacyPermission' => DAccessControl::MANAGE_FILE_ACCESS, + 'expectedTrue' => [File::PERM_READ, File::PERM_UPDATE, File::PERM_DELETE], + 'expectedFalse' => [File::PERM_CREATE], + ], + 'public access' => [ + 'legacyPermission' => DAccessControl::PUBLIC_ACCESS, + 'expectedTrue' => [LogEntry::PERM_READ], + 'expectedFalse' => [LogEntry::PERM_CREATE, LogEntry::PERM_UPDATE, LogEntry::PERM_DELETE], + ], + ]; + + foreach ($cases as $label => $case) { + $permissions = AccessUtils::getPermissionArrayConverted(json_encode([$case['legacyPermission'] => true])); + + foreach ($case['expectedTrue'] as $crudPermission) { + $this->assertArrayHasKey($crudPermission, $permissions, $label); + $this->assertTrue($permissions[$crudPermission], sprintf('%s should enable %s.', $label, $crudPermission)); + } + + foreach ($case['expectedFalse'] as $crudPermission) { + $this->assertArrayHasKey($crudPermission, $permissions, $label); + $this->assertFalse($permissions[$crudPermission], sprintf('%s should not enable %s.', $label, $crudPermission)); + } + } + } + + public function testUserCannotAccessManyHashlistsWhenOneHashlistIsInDifferentAccessGroup(): void { + $allowedGroup = $this->createAccessGroup('hashlist_access_group_allowed'); + $deniedGroup = $this->createAccessGroup('hashlist_access_group_denied'); + $user = $this->createUser('hashlist_access_user'); + $hashType = $this->createHashType(); + $allowedHashlist = $this->createHashlist($allowedGroup, $hashType); + $deniedHashlist = $this->createHashlist($deniedGroup, $hashType); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $allowedGroup->getId(), $user->getId()) + ); + + $this->assertFalse(AccessUtils::userCanAccessHashlists([$allowedHashlist, $deniedHashlist], $user)); + } + + public function testUserCanAccessAgentWhenTheyShareAnAccessGroup(): void { + $group = $this->createAccessGroup('agent_access_group'); + $user = $this->createUser('agent_access_user'); + $agent = $this->createAgent('shared_access_agent'); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $group->getId(), $agent->getId()) + ); + + $this->assertTrue(AccessUtils::userCanAccessAgent($agent, $user)); + } + + public function testUserCannotAccessAgentWhenTheyDoNotShareAnAccessGroup(): void { + $userGroup = $this->createAccessGroup('user_access_group'); + $agentGroup = $this->createAccessGroup('agent_access_group'); + $user = $this->createUser('agent_access_user'); + $agent = $this->createAgent('isolated_access_agent'); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $userGroup->getId(), $user->getId()) + ); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $agentGroup->getId(), $agent->getId()) + ); + + $this->assertFalse(AccessUtils::userCanAccessAgent($agent, $user)); + } + + public function testUserCanAccessTaskWhenTheyShareAnAccessGroup(): void { + $group = $this->createAccessGroup('task_access_group'); + $user = $this->createUser('task_access_user'); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($group, $hashType); + $taskWrapper = $this->createTaskWrapper($group, $hashlist); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + + $this->assertTrue(AccessUtils::userCanAccessTask($taskWrapper, $user)); + } + + public function testUserCannotAccessTaskWhenTheyDoNotShareAnAccessGroup(): void { + $userGroup = $this->createAccessGroup('user_task_access_group'); + $taskGroup = $this->createAccessGroup('wrapper_access_group'); + $user = $this->createUser('task_access_user'); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($taskGroup, $hashType); + $taskWrapper = $this->createTaskWrapper($taskGroup, $hashlist); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $userGroup->getId(), $user->getId()) + ); + + $this->assertFalse(AccessUtils::userCanAccessTask($taskWrapper, $user)); + } + + public function testUserCanAccessFileWhenTheyShareAnAccessGroup(): void { + $group = $this->createAccessGroup('file_access_group'); + $user = $this->createUser('file_access_user'); + $file = $this->createFile($group); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $group->getId(), $user->getId()) + ); + + $this->assertTrue(AccessUtils::userCanAccessFile($file, $user)); + } + + public function testUserCannotAccessFileWhenTheyDoNotShareAnAccessGroup(): void { + $userGroup = $this->createAccessGroup('user_file_access_group'); + $fileGroup = $this->createAccessGroup('file_access_group'); + $user = $this->createUser('file_access_user'); + $file = $this->createFile($fileGroup); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $userGroup->getId(), $user->getId()) + ); + + $this->assertFalse(AccessUtils::userCanAccessFile($file, $user)); + } + + public function testIntersectionReturnsSharedAccessGroups(): void { + $firstOnlyGroup = $this->createAccessGroup('first_only_group'); + $sharedGroup = $this->createAccessGroup('shared_group'); + $secondOnlyGroup = $this->createAccessGroup('second_only_group'); + + $intersection = AccessUtils::intersection( + [$firstOnlyGroup, $sharedGroup], + [$sharedGroup, $secondOnlyGroup] + ); + + $this->assertSame([$sharedGroup], $intersection); + } + + public function testIntersectionReturnsEmptyArrayWhenOneSideIsEmpty(): void { + $group = $this->createAccessGroup('non_empty_group'); + + $this->assertSame([], AccessUtils::intersection([], [$group])); + $this->assertSame([], AccessUtils::intersection([$group], [])); + } + + public function testGetAccessGroupsOfUserReturnsAssignedGroups(): void { + $firstGroup = $this->createAccessGroup('user_group_one'); + $secondGroup = $this->createAccessGroup('user_group_two'); + $user = $this->createUser('grouped_user'); + $defaultGroup = AccessUtils::getOrCreateDefaultAccessGroup(); + + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $firstGroup->getId(), $user->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupUserFactory(), + new AccessGroupUser(null, $secondGroup->getId(), $user->getId()) + ); + + $groups = AccessUtils::getAccessGroupsOfUser($user); + + $this->assertEqualsCanonicalizing( + [$defaultGroup->getId(), $firstGroup->getId(), $secondGroup->getId()], + array_map(static fn (AccessGroup $group): ?int => $group->getId(), $groups) + ); + } + + public function testGetAccessGroupsOfUserReturnsDefaultGroupWhenUserHasNoAdditionalAssignments(): void { + $user = $this->createUser('ungrouped_user'); + $defaultGroup = AccessUtils::getOrCreateDefaultAccessGroup(); + $groups = AccessUtils::getAccessGroupsOfUser($user); + + $this->assertEqualsCanonicalizing( + [$defaultGroup->getId()], + array_map(static fn (AccessGroup $group): ?int => $group->getId(), $groups) + ); + } + + public function testGetAccessGroupsOfAgentReturnsAssignedGroups(): void { + $firstGroup = $this->createAccessGroup('agent_group_one'); + $secondGroup = $this->createAccessGroup('agent_group_two'); + $agent = $this->createAgent('grouped_agent'); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $firstGroup->getId(), $agent->getId()) + ); + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $secondGroup->getId(), $agent->getId()) + ); + + $groups = AccessUtils::getAccessGroupsOfAgent($agent); + + $this->assertEqualsCanonicalizing( + [$firstGroup->getId(), $secondGroup->getId()], + array_map(static fn (AccessGroup $group): ?int => $group->getId(), $groups) + ); + } + + public function testGetAccessGroupsOfAgentReturnsEmptyArrayWhenAgentHasNoAssignments(): void { + $agent = $this->createAgent('ungrouped_agent'); + $this->assertSame([], AccessUtils::getAccessGroupsOfAgent($agent)); + } + + public function testGetOrCreateDefaultAccessGroupReturnsExistingDefaultGroup(): void { + $defaultGroup = AccessUtils::getOrCreateDefaultAccessGroup(); + + $this->assertInstanceOf(AccessGroup::class, $defaultGroup); + $this->assertSame(1, $defaultGroup->getId()); + $this->assertNotNull(Factory::getAccessGroupFactory()->get(1)); + } + + public function testAgentCannotAccessTaskWhenItCannotAccessTaskWrapper(): void { + $agentGroup = $this->createAccessGroup('agent_task_group'); + $taskGroup = $this->createAccessGroup('task_wrapper_group'); + $agent = $this->createAgent('restricted_agent'); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($taskGroup, $hashType); + $taskWrapper = $this->createTaskWrapper($taskGroup, $hashlist); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $agentGroup->getId(), $agent->getId()) + ); + + $this->assertFalse(AccessUtils::agentCanAccessTask($agent, $task)); + } + + public function testAgentCannotAccessTaskWhenHashlistIsSecretAndAgentIsNotTrusted(): void { + $group = $this->createAccessGroup('secret_hashlist_group'); + $agent = $this->createAgent('untrusted_agent', 0); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($group, $hashType, 1); + $taskWrapper = $this->createTaskWrapper($group, $hashlist); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $group->getId(), $agent->getId()) + ); + + $this->assertFalse(AccessUtils::agentCanAccessTask($agent, $task)); + } + + public function testAgentCannotAccessTaskWhenHashlistIsInDifferentAccessGroup(): void { + $sharedTaskGroup = $this->createAccessGroup('shared_task_group'); + $otherHashlistGroup = $this->createAccessGroup('other_hashlist_group'); + $agent = $this->createAgent('untrusted_but_allowed_agent', 0); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($otherHashlistGroup, $hashType); + $taskWrapper = $this->createTaskWrapper($sharedTaskGroup, $hashlist); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $sharedTaskGroup->getId(), $agent->getId()) + ); + + $this->assertFalse(AccessUtils::agentCanAccessTask($agent, $task)); + } + + public function testAgentCannotAccessTaskWhenFileIsSecretAndAgentIsNotTrusted(): void { + $group = $this->createAccessGroup('secret_file_group'); + $agent = $this->createAgent('untrusted_file_agent', 0); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($group, $hashType); + $taskWrapper = $this->createTaskWrapper($group, $hashlist); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + $file = $this->createFile($group, 1); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $group->getId(), $agent->getId()) + ); + $this->createFileTask($file, $task); + + $this->assertFalse(AccessUtils::agentCanAccessTask($agent, $task)); + } + + public function testAgentCanAccessTaskWhenWrapperHashlistAndFilesAreAllowed(): void { + $group = $this->createAccessGroup('allowed_task_group'); + $agent = $this->createAgent('allowed_agent', 0); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($group, $hashType); + $taskWrapper = $this->createTaskWrapper($group, $hashlist); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + $file = $this->createFile($group); + + $this->createDatabaseObject( + Factory::getAccessGroupAgentFactory(), + new AccessGroupAgent(null, $group->getId(), $agent->getId()) + ); + $this->createFileTask($file, $task); + + $this->assertTrue(AccessUtils::agentCanAccessTask($agent, $task)); + } +} \ No newline at end of file diff --git a/ci/phpunit/inc/utils/AccountUtilsTest.php b/ci/phpunit/inc/utils/AccountUtilsTest.php new file mode 100644 index 000000000..3ff14c38c --- /dev/null +++ b/ci/phpunit/inc/utils/AccountUtilsTest.php @@ -0,0 +1,283 @@ +createUser('invalid_yubikey_user'); + $user->setYubikey(1); + $user->setOtp1('short'); + $user->setOtp2(''); + $user->setOtp3('12345678901'); + $user->setOtp4('1234567890123'); + + AccountUtils::checkOTP($user); + + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + $this->assertSame('0', $reloadedUser->getYubikey()); + $this->assertSame('short', $reloadedUser->getOtp1()); + $this->assertSame('', $reloadedUser->getOtp2()); + $this->assertSame('12345678901', $reloadedUser->getOtp3()); + $this->assertSame('1234567890123', $reloadedUser->getOtp4()); + } + + public function testCheckOTPKeepsYubikeyEnabledWhenOtp1HasAValidPrefix(): void { + $this->assertCheckOTPKeepsYubikeyEnabledForValidSlot(1); + } + + public function testCheckOTPKeepsYubikeyEnabledWhenOtp2HasAValidPrefix(): void { + $this->assertCheckOTPKeepsYubikeyEnabledForValidSlot(2); + } + + public function testCheckOTPKeepsYubikeyEnabledWhenOtp3HasAValidPrefix(): void { + $this->assertCheckOTPKeepsYubikeyEnabledForValidSlot(3); + } + + public function testCheckOTPKeepsYubikeyEnabledWhenOtp4HasAValidPrefix(): void { + $this->assertCheckOTPKeepsYubikeyEnabledForValidSlot(4); + } + + public function testSetOTPThrowsWhenEnablingWithoutAValidConfiguredKey(): void { + $user = $this->createUser('setotp_invalid_enable_user'); + $user->setOtp1('short'); + $user->setOtp2(''); + $user->setOtp3('12345678901'); + $user->setOtp4('1234567890123'); + + try { + AccountUtils::setOTP(0, DAccountAction::YUBIKEY_ENABLE, $user, ['', '', '', '']); + $this->fail('Expected setOTP to reject enabling Yubikey without a valid configured key.'); + } + catch (HTException $exception) { + $this->assertSame('Configure OTP KEY first!', $exception->getMessage()); + } + + $this->assertPersistedOtpState($user, '0', '', '', '', ''); + } + + public function testSetOTPDisableResetsYubikeyToZero(): void { + $user = $this->createUser('setotp_disable_user'); + $user->setYubikey(1); + $user->setOtp1('validyubikey'); + $user->setOtp2('backupyubico'); + $user->setOtp3('reservekey12'); + $user->setOtp4('lastresort12'); + + AccountUtils::setOTP(-1, DAccountAction::YUBIKEY_DISABLE, $user, ['', '', '', '']); + + $this->assertPersistedOtpState($user, '0', 'validyubikey', 'backupyubico', 'reservekey12', 'lastresort12'); + } + + public function testSetOTPYubikeyActivationWithoutValidKeysDisabledAfterCheckOTP(): void { + $user = $this->createUser('setotp_activate_without_valid_keys_user'); + $user->setYubikey(0); + $user->setOtp1('short'); + $user->setOtp2(''); + $user->setOtp3('12345678901'); + $user->setOtp4('1234567890123'); + + AccountUtils::setOTP(0, DAccountAction::SET_OTP1, $user, ['', '', '', '']); + + $this->assertPersistedOtpState($user, '0', 'short', '', '12345678901', '1234567890123'); + } + + public function testSetOTPStoresValidPrefixThenActivatesYubikey(): void { + foreach ([1, 2, 3, 4] as $slot) { + $this->assertSetOTPStoresValidPrefixThenActivatesYubikeyForSlot($slot); + } + } + + public function testSetEmailThrowsOnInvalidEmailFormat(): void { + $user = $this->createUser('invalid_email_user'); + $this->expectException(HTException::class); + AccountUtils::setEmail('invalid-email-address', $user); + } + + public function testSetEmailUpdatesEmailOnValidAddress(): void { + $user = $this->createUser('valid_email_user'); + $newEmail = 'updated_' . uniqid() . '@example.com'; + + AccountUtils::setEmail($newEmail, $user); + + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + $this->assertSame($newEmail, $reloadedUser->getEmail()); + } + + public function testUpdateSessionLifetimeThrowsWhenBelowMinimum(): void { + $user = $this->createUser('invalid_lifetime_user'); + $this->expectException(HTException::class); + + AccountUtils::updateSessionLifetime(59, $user); + } + + public function testUpdateSessionLifetimeUpdatesPersistedValue(): void { + $user = $this->createUser('valid_lifetime_user'); + $newLifetime = 60; + + AccountUtils::updateSessionLifetime($newLifetime, $user); + + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + $this->assertSame($newLifetime, $reloadedUser->getSessionLifetime()); + } + + public function testChangePasswordThrowsWhenOldPasswordIsWrong(): void { + $user = $this->createUserWithPassword('wrong_old_password_user', 'oldpass'); + $this->expectException(HTException::class); + $this->expectExceptionMessage('Your old password is wrong!'); + + AccountUtils::changePassword('wrongpass', 'newpass', 'newpass', $user); + } + + public function testChangePasswordThrowsWhenNewPasswordIsTooShort(): void { + $user = $this->createUserWithPassword('short_new_password_user', 'oldpass'); + $this->expectException(HTException::class); + $this->expectExceptionMessage('Your password is too short!'); + + AccountUtils::changePassword('oldpass', 'abc', 'abc', $user); + } + + public function testChangePasswordThrowsWhenNewPasswordsDoNotMatch(): void { + $user = $this->createUserWithPassword('mismatch_password_user', 'oldpass'); + $this->expectException(HTException::class); + $this->expectExceptionMessage('Your new passwords do not match!'); + + AccountUtils::changePassword('oldpass', 'newpass', 'otherpass', $user); + } + + public function testChangePasswordThrowsWhenNewPasswordMatchesOldPassword(): void { + $user = $this->createUserWithPassword('same_password_user', 'oldpass'); + $this->expectException(HTException::class); + $this->expectExceptionMessage('Your new password is the same as the old one!'); + + AccountUtils::changePassword('oldpass', 'oldpass', 'oldpass', $user); + } + + public function testChangePasswordUpdatesPersistedPasswordData(): void { + $user = $this->createUserWithPassword('happy_password_user', 'oldpass'); + $oldSalt = $user->getPasswordSalt(); + $oldHash = $user->getPasswordHash(); + + AccountUtils::changePassword('oldpass', 'newpass', 'newpass', $user); + + $reloadedUser = $this->reloadUser($user); + + $this->assertNotSame($oldSalt, $reloadedUser->getPasswordSalt()); + $this->assertNotSame($oldHash, $reloadedUser->getPasswordHash()); + $this->assertFalse(Encryption::passwordVerify('oldpass', $reloadedUser->getPasswordSalt(), $reloadedUser->getPasswordHash())); + $this->assertTrue(Encryption::passwordVerify('newpass', $reloadedUser->getPasswordSalt(), $reloadedUser->getPasswordHash())); + $this->assertSame(0, $reloadedUser->getIsComputedPassword()); + } + + private function assertCheckOTPKeepsYubikeyEnabledForValidSlot(int $validSlot): void { + $user = $this->createUser('valid_yubikey_user_' . $validSlot); + $user->setYubikey(1); + + $otpValues = [ + 1 => '', + 2 => '', + 3 => '', + 4 => '', + ]; + $otpValues[$validSlot] = 'validyubikey'; + + $user->setOtp1($otpValues[1]); + $user->setOtp2($otpValues[2]); + $user->setOtp3($otpValues[3]); + $user->setOtp4($otpValues[4]); + + AccountUtils::checkOTP($user); + + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + $this->assertSame('1', $reloadedUser->getYubikey()); + $this->assertSame($otpValues[1], $reloadedUser->getOtp1()); + $this->assertSame($otpValues[2], $reloadedUser->getOtp2()); + $this->assertSame($otpValues[3], $reloadedUser->getOtp3()); + $this->assertSame($otpValues[4], $reloadedUser->getOtp4()); + } + + private function assertPersistedOtpState(User $user, string $expectedYubikey, string $expectedOtp1, string $expectedOtp2, string $expectedOtp3, string $expectedOtp4): void { + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + $this->assertSame($expectedYubikey, $reloadedUser->getYubikey()); + $this->assertSame($expectedOtp1, $reloadedUser->getOtp1()); + $this->assertSame($expectedOtp2, $reloadedUser->getOtp2()); + $this->assertSame($expectedOtp3, $reloadedUser->getOtp3()); + $this->assertSame($expectedOtp4, $reloadedUser->getOtp4()); + } + + private function assertSetOTPStoresValidPrefixThenActivatesYubikeyForSlot(int $slot): void { + $user = $this->createUser('setotp_happy_path_user_' . $slot); + $fullOtp = 'ccccccdefghdefghdefghdefghdefghdefghdefghi'; + $actions = [ + 1 => DAccountAction::SET_OTP1, + 2 => DAccountAction::SET_OTP2, + 3 => DAccountAction::SET_OTP3, + 4 => DAccountAction::SET_OTP4, + ]; + $expectedOtpValues = [ + 1 => '', + 2 => '', + 3 => '', + 4 => '', + ]; + $expectedOtpValues[$slot] = 'ccccccdefghd'; + + $otpArr = ['', '', '', '']; + $otpArr[$slot - 1] = $fullOtp; + + AccountUtils::setOTP($slot, $actions[$slot], $user, $otpArr); + $this->assertPersistedOtpState($user, '0', $expectedOtpValues[1], $expectedOtpValues[2], $expectedOtpValues[3], $expectedOtpValues[4]); + + AccountUtils::setOTP(0, DAccountAction::YUBIKEY_ENABLE, $user, ['', '', '', '']); + $this->assertPersistedOtpState($user, '1', $expectedOtpValues[1], $expectedOtpValues[2], $expectedOtpValues[3], $expectedOtpValues[4]); + } + + /* + Local test helpers + */ + private function createUserWithPassword(string $prefix, string $password): User { + $user = $this->createUser($prefix); + UserUtils::setPassword($user->getId(), $password, $this->adminUser); + return $this->reloadUser($user); + } + + private function reloadUser(User $user): User { + $reloadedUser = Factory::getUserFactory()->filter([ + Factory::FILTER => new QueryFilter(User::USERNAME, $user->getUsername(), '=') + ], true); + + $this->assertInstanceOf(User::class, $reloadedUser); + return $reloadedUser; + } +} \ No newline at end of file diff --git a/ci/phpunit/inc/utils/ChunkUtilsTest.php b/ci/phpunit/inc/utils/ChunkUtilsTest.php new file mode 100644 index 000000000..946ba2215 --- /dev/null +++ b/ci/phpunit/inc/utils/ChunkUtilsTest.php @@ -0,0 +1,146 @@ +getProperty('instance'); + $p->setValue(null, new DataSet($v)); + } + + // Resets the SConfig singleton to null after every test so a mocked config + // from one test never leaks into the next. + protected function tearDown(): void { + $p = (new \ReflectionClass(SConfig::class))->getProperty('instance'); + $p->setValue(null, null); + } + + // Verifies that CHUNK_SIZE static mode bypasses all benchmark math and + // returns the configured chunk size value directly. + public function testStaticChunkSizeReturnsValueDirectly(): void { + $this->assertSame(25000, ChunkUtils::calculateChunkSize(1000000, '5000:1000', 60, 1.0, DTaskStaticChunking::CHUNK_SIZE, 25000)); + } + + // Verifies that NUM_CHUNKS static mode divides the keyspace evenly and + // rounds up (ceil) so no candidates are left out. + // Result is cast to int because PHP ceil() returns float. + public function testStaticNumChunksReturnsCeilDivision(): void { + $this->assertSame((int) ceil(1000000 / 3), (int) ChunkUtils::calculateChunkSize(1000000, '5000:1000', 60, 1.0, DTaskStaticChunking::NUM_CHUNKS, 3)); + } + + // Verifies that misconfigured static chunking inputs always throw HTException. + // Four cases via data provider: CHUNK_SIZE=0, NUM_CHUNKS=0, + // NUM_CHUNKS>10000 (flood protection), and an unknown mode constant. + // PHPUnit 12 requires the #[DataProvider] attribute — @dataProvider docblock no longer works. + #[DataProvider('staticExceptionCases')] + public function testStaticChunkingInvalidInputThrowsHTException(int $mode, int $size): void { + $this->expectException(HTException::class); + ChunkUtils::calculateChunkSize(1000000, '5000:1000', 60, 1.0, $mode, $size); + } + + public static function staticExceptionCases(): array { + return [ + 'CHUNK_SIZE zero' => [DTaskStaticChunking::CHUNK_SIZE, 0], + 'NUM_CHUNKS zero' => [DTaskStaticChunking::NUM_CHUNKS, 0], + 'NUM_CHUNKS too large' => [DTaskStaticChunking::NUM_CHUNKS, 10001], + 'unknown mode' => [99, 0], + ]; + } + + // Verifies the old benchmark special case: benchmark=0 means the agent + // reported no speed, so the entire keyspace is returned as one chunk. + public function testOldBenchmarkZeroValueReturnsFullKeyspace(): void { + $this->assertSame(500, ChunkUtils::calculateChunkSize(500, '0', 60)); + } + + // Verifies the old benchmark formula: floor(keyspace * benchmark * chunkTime / 100). + // Result is cast to int because PHP floor() returns float. + public function testOldBenchmarkNormalReturnsCorrectFormula(): void { + $this->assertSame((int) floor(1000000 * 50 * 60 / 100), (int) ChunkUtils::calculateChunkSize(1000000, '50', 60)); + } + + // Verifies the new benchmark formula using "speed:time" format. + // factor = chunkTime / time * 1000, size = floor(factor * speed). + // Result is cast to int because PHP floor() returns float. + public function testNewBenchmarkValidFormatReturnsCorrectFormula(): void { + $this->assertSame((int) floor(30.0 * 5000000), (int) ChunkUtils::calculateChunkSize(999999999, '5000000:1000', 30)); + } + + // Verifies that new-format benchmarks with zero speed or zero time return 0 + // instead of crashing — the guard in calculateChunkSize() catches both. + #[DataProvider('invalidBenchmarkCases')] + public function testNewBenchmarkInvalidInputReturnsZero(string $benchmark): void { + $this->assertSame(0, ChunkUtils::calculateChunkSize(1000000, $benchmark, 60)); + } + + public static function invalidBenchmarkCases(): array { + return [ + 'zero speed' => ['0:1000'], + 'zero time' => ['5000000:0'], + ]; + } + + // Verifies that a benchmark string with no colon routes to the old-benchmark + // path and PHP 8 throws TypeError on arithmetic with a non-numeric string. + public function testOldBenchmarkNonNumericStringThrowsTypeError(): void { + $this->expectException(\TypeError::class); + ChunkUtils::calculateChunkSize(1000000, 'invalid', 60); + } + + // Verifies the safety floor: when the formula produces a size <= 0 the result + // is clamped to 1 so dispatching never stalls on an infinite zero-size loop. + // $QUERY must be set because the clamp path calls Util::createLogEntry which + // reads $QUERY['token'] as a non-null TEXT value for the log entry. + public function testSizeClampedToOneWhenCalculationProducesZero(): void { + $GLOBALS['QUERY'] = ['token' => 'test']; + $this->assertSame(1, (int) ChunkUtils::calculateChunkSize(1000000, '1:999999999', 1)); + } + + // Verifies that the tolerance multiplier correctly scales the chunk size up. + // Both sides are cast to int because float arithmetic (30000000.0 * 1.1) + // produces 33000000.000000004 due to IEEE 754 precision — int cast aligns them. + public function testToleranceScalesChunkSizeUp(): void { + $base = (int) ChunkUtils::calculateChunkSize(1000000, '50', 60, 1.0); + $this->assertSame((int) ($base * 1.1), (int) ChunkUtils::calculateChunkSize(1000000, '50', 60, 1.1)); + } + + // Verifies that chunkTime=0 triggers the SConfig fallback: the server-wide + // CHUNK_DURATION value is used instead of the per-task setting. + // Result is cast to int because PHP floor() returns float. + public function testZeroChunkTimeFallsBackToSConfigValue(): void { + $this->mockSConfig([DConfig::CHUNK_DURATION => 120]); + $this->assertSame((int) floor(1000000 * 50 * 120 / 100), (int) ChunkUtils::calculateChunkSize(1000000, '50', 0)); + } + + // Verifies that createNewChunk() returns null when the full keyspace has been + // consumed (keyspace == keyspaceProgress). A mocked Task is used so no DB + // records are needed; the mock returns getKeyspace()=1000 and + // getKeyspaceProgress()=1000, making remaining=0 and triggering the null path. + public function testCreateNewChunkReturnsNullWhenKeyspaceExhausted(): void { + $this->mockSConfig([DConfig::DISP_TOLERANCE => 0, DConfig::CHUNK_DURATION => 600]); + $task = $this->createMock(Task::class); + $task->method('getSkipKeyspace')->willReturn(0); + $task->method('getKeyspaceProgress')->willReturn(1000); + $task->method('getKeyspace')->willReturn(1000); + $this->assertNull(ChunkUtils::createNewChunk($task, $this->createMock(Assignment::class))); + } + + // TODO: handleExistingChunk() and createNewChunk() require further test coverage. +} diff --git a/ci/phpunit/inc/utils/ConfigUtilsTest.php b/ci/phpunit/inc/utils/ConfigUtilsTest.php new file mode 100644 index 000000000..f71a66416 --- /dev/null +++ b/ci/phpunit/inc/utils/ConfigUtilsTest.php @@ -0,0 +1,69 @@ +existingConfig = ConfigUtils::get('chunktime'); + } + + // Verifies that get() returns the correct Config object when the item exists. + // Uses "chunktime" which is always present in the default database. + public function testGetKnownItemReturnsConfig(): void { + $config = ConfigUtils::get('chunktime'); + $this->assertSame('chunktime', $config->getItem()); + } + + // Verifies that get() throws HTException when the item does not exist. + // Uses a deliberately nonsensical key that will never be in the database. + public function testGetUnknownItemThrowsHTException(): void { + $this->expectException(HTException::class); + ConfigUtils::get('nonexistent_item_xyz_999'); + } + + // Verifies that updateSingleConfig() throws HTException when the attributes + // array contains no VALUE key, meaning no new value was provided. + public function testUpdateSingleConfigMissingValueThrowsHTException(): void { + $this->expectException(HTException::class); + // Empty attributes array — Config::VALUE key is absent, triggering the guard. + ConfigUtils::updateSingleConfig($this->existingConfig->getId(), []); + } + + // Verifies that updateSingleConfig() returns early without performing any + // database write when the provided value is identical to the stored value. + // This is the no-op path that avoids unnecessary DB updates. + public function testUpdateSingleConfigSameValueReturnsEarlyWithoutException(): void { + $this->expectNotToPerformAssertions(); + $sameValue = $this->existingConfig->getValue(); + // Passing the same value back — the method must detect no change and return. + ConfigUtils::updateSingleConfig($this->existingConfig->getId(), [Config::VALUE => $sameValue]); + + } + + // Verifies that updateSingleConfig() throws HTException when the given ID + // does not match any row in the Config table. + public function testUpdateSingleConfigInvalidIdThrowsHTException(): void { + $this->expectException(HTException::class); + ConfigUtils::updateSingleConfig(99999, [Config::VALUE => 'anything']); + } +} diff --git a/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php new file mode 100644 index 000000000..db7cb5b70 --- /dev/null +++ b/ci/phpunit/inc/utils/CrackerBinaryUtilsTest.php @@ -0,0 +1,78 @@ +type = $this->createDatabaseObject( + Factory::getCrackerBinaryTypeFactory(), + new CrackerBinaryType(null, 'test-crackerbinaryutils-type', 1) + ); + } + + // Helper: saves a CrackerBinary with the given version under the shared type + // and registers it for automatic cleanup via TestBase. + private function addBinary(string $version): AbstractModel { + return $this->createDatabaseObject( + Factory::getCrackerBinaryFactory(), + new CrackerBinary(null, $this->type->getId(), $version, 'http://example.com', 'testcracker') + ); + } + + // Verifies that getNewestVersion() throws HTException when no CrackerBinary + // rows exist for the given type — there is nothing to pick the newest from. + public function testGetNewestVersionNoBinariesThrowsHTException(): void { + $this->expectException(HTException::class); + CrackerBinaryUtils::getNewestVersion($this->type->getId()); + } + + // Verifies that getNewestVersion() returns the only available binary when + // exactly one version is registered under the type. + public function testGetNewestVersionSingleBinaryReturnsThatBinary(): void { + $binary = $this->addBinary('1.0.0'); + $result = CrackerBinaryUtils::getNewestVersion($this->type->getId()); + $this->assertSame($binary->getId(), $result->getId()); + } + + // Verifies that getNewestVersion() correctly picks the highest semantic version + // when multiple binaries are registered. The comparison uses Composer\Semver + // so "2.5.0" must beat "1.9.9" even though 1.9.9 was added after 2.5.0. + public function testGetNewestVersionMultipleBinariesReturnsHighestVersion(): void { + $this->addBinary('1.0.0'); + $newest = $this->addBinary('2.5.0'); + $this->addBinary('1.9.9'); + $result = CrackerBinaryUtils::getNewestVersion($this->type->getId()); + $this->assertSame($newest->getId(), $result->getId()); + } + + // Verifies that getNewestVersion() handles non-sequential insertion order + // correctly — the oldest version added last must not be chosen as newest. + public function testGetNewestVersionOutOfOrderInsertStillReturnsHighest(): void { + $newest = $this->addBinary('3.0.0'); + $this->addBinary('1.0.0'); + $this->addBinary('2.0.0'); + $result = CrackerBinaryUtils::getNewestVersion($this->type->getId()); + $this->assertSame($newest->getId(), $result->getId()); + } +} diff --git a/ci/phpunit/inc/utils/CrackerUtilsTest.php b/ci/phpunit/inc/utils/CrackerUtilsTest.php new file mode 100644 index 000000000..c2bc6497d --- /dev/null +++ b/ci/phpunit/inc/utils/CrackerUtilsTest.php @@ -0,0 +1,99 @@ +type = $this->createDatabaseObject( + Factory::getCrackerBinaryTypeFactory(), + new CrackerBinaryType(null, 'test-crackerutils-type', 1) + ); + $this->binary = $this->createDatabaseObject( + Factory::getCrackerBinaryFactory(), + new CrackerBinary(null, $this->type->getId(), '1.0.0', 'http://example.com', 'testcracker') + ); + } + + // Verifies that getBinary() throws HTException when the ID does not match + // any row — the caller must handle the "binary not found" case. + public function testGetBinaryInvalidIdThrowsHTException(): void { + $this->expectException(HTException::class); + CrackerUtils::getBinary(99999); + } + + // Verifies that getBinaryType() throws HTException when the ID does not match + // any row — the caller must handle the "type not found" case. + public function testGetBinaryTypeInvalidIdThrowsHTException(): void { + $this->expectException(HTException::class); + CrackerUtils::getBinaryType(99999); + } + + // Verifies that getBinary() returns the correct CrackerBinary when the ID + // matches the record created in setUp. + public function testGetBinaryValidIdReturnsBinary(): void { + $result = CrackerUtils::getBinary($this->binary->getId()); + $this->assertSame($this->binary->getId(), $result->getId()); + } + + // Verifies that getBinaryType() returns the correct CrackerBinaryType when + // the ID matches the record created in setUp. + public function testGetBinaryTypeValidIdReturnsBinaryType(): void { + $result = CrackerUtils::getBinaryType($this->type->getId()); + $this->assertSame($this->type->getId(), $result->getId()); + } + + // Verifies that createBinaryType() throws HttpError when an empty string is + // passed as the type name — an empty name is not a valid cracker identifier. + public function testCreateBinaryTypeEmptyNameThrowsHttpError(): void { + $this->expectException(HttpError::class); + CrackerUtils::createBinaryType(''); + } + + // Verifies that createBinaryType() throws HttpConflict when a type with the + // same name already exists in the database (setUp created "test-crackerutils-type"). + public function testCreateBinaryTypeDuplicateNameThrowsHttpConflict(): void { + $this->expectException(HttpConflict::class); + CrackerUtils::createBinaryType('test-crackerutils-type'); + } + + // Verifies that createBinary() throws HttpError when any required field is + // empty. Uses a valid type ID so the method reaches the field validation. + public function testCreateBinaryEmptyVersionThrowsHttpError(): void { + $this->expectException(HttpError::class); + CrackerUtils::createBinary('', 'testcracker', 'http://example.com', $this->type->getId()); + } + + // Verifies the full happy path: createBinary() creates and returns a new + // CrackerBinary when all fields are valid. + public function testCreateBinaryValidInputCreatesBinary(): void { + $b = CrackerUtils::createBinary('9.9.9', 'newcracker', 'http://example.com/dl', $this->type->getId()); + $this->registerDatabaseObject(Factory::getCrackerBinaryFactory(), $b); + $this->assertSame('9.9.9', $b->getVersion()); + } +} diff --git a/ci/phpunit/inc/utils/FileDownloadUtilsTest.php b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php new file mode 100644 index 000000000..13cf156ac --- /dev/null +++ b/ci/phpunit/inc/utils/FileDownloadUtilsTest.php @@ -0,0 +1,79 @@ +createAccessGroup('fdl_group'); + $this->file = $this->createFile($group); + $this->fileDownload = $this->createFileDownload($this->file->getId(), DFileDownloadStatus::DONE); + } + + public function testAddDownloadCreatesPendingDownload(): void { + $group = $this->createAccessGroup('fdl_new'); + $newFile = $this->createFile($group); + + FileDownloadUtils::addDownload($newFile->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $newFile->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $result = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(FileDownload::class, $result); + $this->assertSame($newFile->getId(), $result->getFileId()); + $this->assertSame(DFileDownloadStatus::PENDING, $result->getStatus()); + $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $result); + } + + public function testAddDownloadSkipsExistingPending(): void { + $this->createFileDownload($this->file->getId()); + + FileDownloadUtils::addDownload($this->file->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); + $this->assertCount(1, $pending); + } + + public function testAddDownloadCreatesNewForCompletedFile(): void { + FileDownloadUtils::addDownload($this->file->getId()); + + $qF1 = new QueryFilter(FileDownload::FILE_ID, $this->file->getId(), '='); + $qF2 = new QueryFilter(FileDownload::STATUS, DFileDownloadStatus::PENDING, '='); + $pending = Factory::getFileDownloadFactory()->filter([Factory::FILTER => [$qF1, $qF2]], true); + + $this->assertInstanceOf(FileDownload::class, $pending); + $this->assertSame($this->file->getId(), $pending->getFileId()); + $this->assertSame(DFileDownloadStatus::PENDING, $pending->getStatus()); + $this->registerDatabaseObject(Factory::getFileDownloadFactory(), $pending); + } + + public function testRemoveFileDeletesDownloads(): void { + FileDownloadUtils::removeFile($this->fileDownload->getFileId()); + + $qF = new QueryFilter(FileDownload::FILE_ID, $this->fileDownload->getFileId(), '='); + $remaining = Factory::getFileDownloadFactory()->filter([Factory::FILTER => $qF]); + $this->assertSame([], $remaining); + } + + public function testRemoveFileIsNoopForNonExistent(): void { + FileDownloadUtils::removeFile(-1); + } +} diff --git a/ci/phpunit/inc/utils/FileUtilsTest.php b/ci/phpunit/inc/utils/FileUtilsTest.php new file mode 100644 index 000000000..2c480090d --- /dev/null +++ b/ci/phpunit/inc/utils/FileUtilsTest.php @@ -0,0 +1,146 @@ +user = $this->createUser('fu_user'); + $this->group = $this->createAccessGroup('fu_group'); + $this->createAccessGroupUser($this->user, $this->group); + + $this->file = $this->createFile($this->group); + $this->ruleFile = $this->createFile($this->group, 0, 'test_rule_' . uniqid() . '.rule', 512, DFileType::RULE); + $this->wordlistFile = $this->createFile($this->group); + $this->otherFile = $this->createFile($this->group, 0, 'test_other_' . uniqid() . '.bin', 256, DFileType::OTHER); + } + + public function testGetFileReturnsFileForAuthorizedUser(): void { + $result = FileUtils::getFile($this->file->getId(), $this->user); + $this->assertInstanceOf(File::class, $result); + $this->assertSame($this->file->getId(), $result->getId()); + } + + public function testGetFileThrowsForInvalidId(): void { + $this->expectException(HTException::class); + FileUtils::getFile(-1, $this->user); + } + + public function testGetFileThrowsForUnauthorizedUser(): void { + $otherGroup = $this->createAccessGroup('fu_other'); + $otherFile = $this->createFile($otherGroup); + + $this->expectException(HTException::class); + FileUtils::getFile($otherFile->getId(), $this->user); + } + + public function testSetFileTypeUpdatesType(): void { + FileUtils::setFileType($this->file->getId(), DFileType::RULE, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(DFileType::RULE, $updated->getFileType()); + } + + public function testSetFileTypeThrowsForInvalidType(): void { + $this->expectException(HTException::class); + FileUtils::setFileType($this->file->getId(), 999, $this->user); + } + + public function testSwitchSecretTogglesSecret(): void { + FileUtils::switchSecret($this->file->getId(), 1, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(1, $updated->getIsSecret()); + + FileUtils::switchSecret($this->file->getId(), 0, $this->user); + + $updated = Factory::getFileFactory()->get($this->file->getId()); + $this->assertSame(0, $updated->getIsSecret()); + } + + public function testGetFilesReturnsFilesInUserAccessGroups(): void { + $files = FileUtils::getFiles($this->user); + $fileIds = array_map(fn(File $f) => $f->getId(), $files); + + $this->assertContains($this->file->getId(), $fileIds); + $this->assertContains($this->ruleFile->getId(), $fileIds); + $this->assertContains($this->wordlistFile->getId(), $fileIds); + $this->assertContains($this->otherFile->getId(), $fileIds); + } + + public function testGetFilesExcludesTemporaryFiles(): void { + $tempFile = $this->createFile($this->group, 0, 'temp_' . uniqid() . '.tmp', 0, DFileType::TEMPORARY); + + $files = FileUtils::getFiles($this->user); + $fileIds = array_map(fn(File $f) => $f->getId(), $files); + + $this->assertNotContains($tempFile->getId(), $fileIds); + } + + public function testLoadFilesByCategoryCategorizesFiles(): void { + [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, []); + + $ruleIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $rules); + $wlIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $wordlists); + $otherIds = array_map(fn($set) => $set->getAllValues()['file']->getId(), $other); + + $this->assertContains($this->ruleFile->getId(), $ruleIds); + $this->assertContains($this->file->getId(), $wlIds); + $this->assertContains($this->wordlistFile->getId(), $wlIds); + $this->assertContains($this->otherFile->getId(), $otherIds); + } + + public function testLoadFilesByCategoryMarksCheckedFiles(): void { + [$rules, $wordlists, $other] = FileUtils::loadFilesByCategory($this->user, [$this->file->getId()]); + + $checkedIds = []; + foreach (array_merge($rules, $wordlists, $other) as $set) { + $data = $set->getAllValues(); + if ($data['checked'] === '1') { + $checkedIds[] = $data['file']->getId(); + } + } + + $this->assertContains($this->file->getId(), $checkedIds); + } + + public function testDeleteThrowsForInvalidId(): void { + $this->expectException(HTException::class); + FileUtils::delete(-1, $this->user); + } + + public function testDeleteThrowsWhenFileInUseByTask(): void { + $this->expectException(HTException::class); + + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($this->group, $hashType); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $taskWrapper = $this->createTaskWrapper($this->group, $hashlist); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + $this->createFileTask($this->file, $task); + + FileUtils::delete($this->file->getId(), $this->user); + } +} diff --git a/ci/phpunit/inc/utils/HashtypeUtilsTest.php b/ci/phpunit/inc/utils/HashtypeUtilsTest.php new file mode 100644 index 000000000..6f07e6c24 --- /dev/null +++ b/ci/phpunit/inc/utils/HashtypeUtilsTest.php @@ -0,0 +1,74 @@ +user = $this->createUser('ht_user'); + } + + public function testAddHashtypeCreatesNewHashtype(): void { + $hashtypeId = 999001; + $description = 'test_hashtype_' . uniqid(); + + $hashtype = HashtypeUtils::addHashtype($hashtypeId, $description, 0, false, $this->user); + + $this->assertSame($hashtypeId, $hashtype->getId()); + $this->assertStringContainsString($description, $hashtype->getDescription()); + + Factory::getHashTypeFactory()->delete($hashtype); + } + + public function testAddHashtypeThrowsForDuplicateId(): void { + $existing = $this->createHashType(); + + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype($existing->getId(), 'new_desc', 0, false, $this->user); + } + + public function testAddHashtypeThrowsForEmptyDescription(): void { + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype(999003, '', 0, false, $this->user); + } + + public function testAddHashtypeThrowsForNegativeId(): void { + $this->expectException(HttpError::class); + HashtypeUtils::addHashtype(-1, 'desc', 0, false, $this->user); + } + + public function testDeleteHashtypeRemovesHashtype(): void { + $hashtype = $this->createHashType(); + + HashtypeUtils::deleteHashtype($hashtype->getId()); + + $this->assertNull(Factory::getHashTypeFactory()->get($hashtype->getId())); + } + + public function testDeleteHashtypeThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HashtypeUtils::deleteHashtype(-1); + } + + public function testDeleteHashtypeThrowsWhenHashlistsExist(): void { + $hashtype = $this->createHashType(); + $accessGroup = $this->createAccessGroup('ht_del'); + $this->createHashlist($accessGroup, $hashtype); + + $this->expectException(HTException::class); + HashtypeUtils::deleteHashtype($hashtype->getId()); + } +} diff --git a/ci/phpunit/inc/utils/HealthUtilsTest.php b/ci/phpunit/inc/utils/HealthUtilsTest.php new file mode 100644 index 000000000..589d93cda --- /dev/null +++ b/ci/phpunit/inc/utils/HealthUtilsTest.php @@ -0,0 +1,212 @@ +createCrackerBinaryType(); + $this->crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $this->agent = $this->createAgent('hc_agent'); + $this->otherAgent = $this->createAgent('hc_other'); + + $this->healthCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::PENDING, DHealthCheckType::BRUTE_FORCE, DHealthCheckMode::MD5, 50, '-a 3 -1 ?l?u?d ?1?1?1?1?1'); + + $this->healthCheckAgent = $this->createHealthCheckAgent($this->healthCheck, $this->agent); + + $this->completedAgent = $this->createHealthCheckAgent($this->healthCheck, $this->otherAgent, DHealthCheckAgentStatus::COMPLETED, 10, 2, 100, 200); + } + + #[Override] + protected function tearDown(): void { + $tmpFile = '/tmp/health-check-' . ($this->healthCheck->getId() ?? 0) . '.txt'; + if (file_exists($tmpFile)) { + unlink($tmpFile); + } + parent::tearDown(); + } + + public function testGenerateHashMd5(): void { + $plain = 'testplain'; + $hash = HealthUtils::generateHash(DHealthCheckMode::MD5, $plain); + $this->assertSame(md5($plain), $hash); + } + + public function testGenerateHashBcrypt(): void { + $plain = 'abc'; + $hash = HealthUtils::generateHash(DHealthCheckMode::BCRYPT, $plain); + $this->assertNotFalse(password_verify($plain, $hash)); + } + + public function testGenerateHashThrowsForUnknownHashtype(): void { + $this->expectException(HTException::class); + HealthUtils::generateHash(999999, 'plain'); + } + + public function testGetAttackModeBruteForce(): void { + $mode = $this->callPrivateMethod('getAttackMode', DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' -a 3', $mode); + } + + public function testGetAttackInputMd5BruteForce(): void { + $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::MD5, DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' -1 ?l?u?d ?1?1?1?1?1', $input); + } + + public function testGetAttackInputBcryptBruteForce(): void { + $input = $this->callPrivateMethod('getAttackInput', DHealthCheckMode::BCRYPT, DHealthCheckType::BRUTE_FORCE); + $this->assertSame(' ?l?l?l', $input); + } + + public function testGetAttackNumHashesMd5(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::MD5); + $this->assertSame(100, $num); + } + + public function testGetAttackNumHashesBcrypt(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', DHealthCheckMode::BCRYPT); + $this->assertSame(10, $num); + } + + public function testGetAttackNumHashesUnknown(): void { + $num = $this->callPrivateMethod('getAttackNumHashes', 999); + $this->assertSame(100, $num); + } + + public function testCheckNeededReturnsPendingAgentCheck(): void { + $result = HealthUtils::checkNeeded($this->agent); + $this->assertInstanceOf(HealthCheckAgent::class, $result); + $this->assertSame($this->healthCheckAgent->getId(), $result->getId()); + } + + public function testCheckNeededReturnsFalseWhenAgentHasNoPending(): void { + $freshAgent = $this->createAgent('hc_fresh'); + $result = HealthUtils::checkNeeded($freshAgent); + $this->assertFalse($result); + } + + public function testCheckNeededReturnsFalseWhenHealthCheckIsAborted(): void { + $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); + $isolatedAgent = $this->createAgent('hc_isolated'); + $pendingAgent = $this->createHealthCheckAgent($abortedCheck, $isolatedAgent); + + $result = HealthUtils::checkNeeded($isolatedAgent); + $this->assertFalse($result); + } + + public function testCheckCompletionMarksCompleteWhenAllAgentsDone(): void { + $allDoneCheck = $this->createHealthCheck($this->crackerBinary); + $this->createHealthCheckAgent($allDoneCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); + $this->createHealthCheckAgent($allDoneCheck, $this->otherAgent, DHealthCheckAgentStatus::FAILED, 0, 0, 0, 0, 'error'); + + HealthUtils::checkCompletion($allDoneCheck); + + $updated = Factory::getHealthCheckFactory()->get($allDoneCheck->getId()); + $this->assertSame(DHealthCheckStatus::COMPLETED, $updated->getStatus()); + } + + public function testCheckCompletionDoesNotCompleteWhenAgentPending(): void { + HealthUtils::checkCompletion($this->healthCheck); + + $updated = Factory::getHealthCheckFactory()->get($this->healthCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $updated->getStatus()); + } + + public function testResetAgentCheckResetsPendingAgent(): void { + HealthUtils::resetAgentCheck($this->healthCheckAgent->getId()); + + $updated = Factory::getHealthCheckAgentFactory()->get($this->healthCheckAgent->getId()); + $this->assertSame(DHealthCheckAgentStatus::PENDING, $updated->getStatus()); + $this->assertSame(0, $updated->getStart()); + $this->assertSame(0, $updated->getEnd()); + $this->assertSame('', $updated->getErrors()); + $this->assertSame(0, $updated->getCracked()); + $this->assertSame(0, $updated->getNumGpus()); + } + + public function testResetAgentCheckResetsCompletedAgentUnderPendingCheck(): void { + HealthUtils::resetAgentCheck($this->completedAgent->getId()); + + $updated = Factory::getHealthCheckAgentFactory()->get($this->completedAgent->getId()); + $this->assertSame(DHealthCheckAgentStatus::PENDING, $updated->getStatus()); + $this->assertSame(0, $updated->getCracked()); + $this->assertSame(0, $updated->getNumGpus()); + $this->assertSame(0, $updated->getStart()); + $this->assertSame(0, $updated->getEnd()); + $this->assertSame('', $updated->getErrors()); + + $parentCheck = Factory::getHealthCheckFactory()->get($this->healthCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $parentCheck->getStatus()); + } + + public function testResetAgentCheckReopensCompletedHealthCheck(): void { + $completedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::COMPLETED); + $agentCheck = $this->createHealthCheckAgent($completedCheck, $this->agent, DHealthCheckAgentStatus::COMPLETED, 5, 1, 0, 10); + + HealthUtils::resetAgentCheck($agentCheck->getId()); + + $updatedCheck = Factory::getHealthCheckFactory()->get($completedCheck->getId()); + $this->assertSame(DHealthCheckStatus::PENDING, $updatedCheck->getStatus()); + } + + public function testResetAgentCheckThrowsForAbortedHealthCheck(): void { + $abortedCheck = $this->createHealthCheck($this->crackerBinary, DHealthCheckStatus::ABORTED); + $agentCheck = $this->createHealthCheckAgent($abortedCheck, $this->agent, DHealthCheckAgentStatus::FAILED, 5, 1, 0, 10); + + $this->expectException(HTException::class); + HealthUtils::resetAgentCheck($agentCheck->getId()); + } + + public function testResetAgentCheckThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HealthUtils::resetAgentCheck(-1); + } + + public function testDeleteHealthCheckRemovesCheckAndAgents(): void { + HealthUtils::deleteHealthCheck($this->healthCheck->getId()); + + $this->assertNull(Factory::getHealthCheckFactory()->get($this->healthCheck->getId())); + + $qF = new QueryFilter(HealthCheckAgent::HEALTH_CHECK_ID, $this->healthCheck->getId(), '='); + $remaining = Factory::getHealthCheckAgentFactory()->filter([Factory::FILTER => $qF]); + $this->assertSame([], $remaining); + } + + public function testDeleteHealthCheckThrowsForInvalidId(): void { + $this->expectException(HTException::class); + HealthUtils::deleteHealthCheck(-1); + } + + private function callPrivateMethod(string $name, ...$args): mixed { + $ref = new ReflectionClass(HealthUtils::class); + $method = $ref->getMethod($name); + return $method->invoke(null, ...$args); + } +} diff --git a/ci/phpunit/inc/utils/JwtTokenUtilsTest.php b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php new file mode 100644 index 000000000..cdd0201bc --- /dev/null +++ b/ci/phpunit/inc/utils/JwtTokenUtilsTest.php @@ -0,0 +1,60 @@ +user = $this->createUser('jwt_user'); + } + + public function testCreateKeyCreatesValidKey(): void { + $start = time(); + $end = $start + 3600; + + $key = JwtTokenUtils::createKey($this->user->getId(), $start, $end); + + $this->assertInstanceOf(JwtApiKey::class, $key); + $this->assertSame($start, $key->getStartValid()); + $this->assertSame($end, $key->getEndValid()); + $this->assertSame($this->user->getId(), $key->getUserId()); + $this->assertNotNull($key->getId()); + $this->registerDatabaseObject(Factory::getJwtApiKeyFactory(), $key); + } + + public function testCreateKeyThrowsForInvalidUser(): void { + $this->expectException(HttpError::class); + JwtTokenUtils::createKey(-1, time(), time() + 3600); + } + + public function testDeleteKeyDeletesExpiredKey(): void { + $start = time() - 7200; + $end = time() - 3600; + $key = $this->createJwtApiKey($this->user, $start, $end); + + JwtTokenUtils::deleteKey($key); + + $this->assertNull(Factory::getJwtApiKeyFactory()->get($key->getId())); + } + + public function testDeleteKeyThrowsForUnexpiredKey(): void { + $key = $this->createJwtApiKey($this->user); + + $this->expectException(HttpForbidden::class); + JwtTokenUtils::deleteKey($key); + } +} diff --git a/ci/phpunit/inc/utils/LockUtilsTest.php b/ci/phpunit/inc/utils/LockUtilsTest.php new file mode 100644 index 000000000..5aa4b04ac --- /dev/null +++ b/ci/phpunit/inc/utils/LockUtilsTest.php @@ -0,0 +1,107 @@ +releaseTestLock(); + $this->cleanupLockFiles(); + $this->lockFile = self::LOCK_DIR . '/' . self::TEST_LOCK; + } + + #[Override] + protected function tearDown(): void { + $this->releaseTestLock(); + $this->cleanupLockFiles(); + parent::tearDown(); + } + + private function releaseTestLock(): void { + LockUtils::release(self::TEST_LOCK); + } + + private function cleanupLockFiles(): void { + $prefixes = [Lock::CHUNKING, self::TEST_LOCK]; + foreach ($prefixes as $prefix) { + $path = self::LOCK_DIR . '/' . $prefix; + if (is_file($path)) { + unlink($path); + } + } + } + + public function testGetCreatesAndAcquiresLock(): void { + LockUtils::get(self::TEST_LOCK); + $this->assertFileExists($this->lockFile); + LockUtils::release(self::TEST_LOCK); + } + + public function testGetReturnsCachedInstance(): void { + LockUtils::get(self::TEST_LOCK); + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + $this->assertFileDoesNotExist($this->lockFile); + } + + public function testReleaseReleasesLockForReacquisition(): void { + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + + LockUtils::get(self::TEST_LOCK); + LockUtils::release(self::TEST_LOCK); + $this->assertFileDoesNotExist($this->lockFile); + } + + public function testReleaseIsNoopForUnknownLock(): void { + LockUtils::release('nonexistent.lock'); + $this->assertFileDoesNotExist(self::LOCK_DIR . '/nonexistent.lock'); + } + + public function testDeleteLockFileRemovesExistingLockFile(): void { + $taskId = 999001; + $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; + + touch($lockFilePath); + $this->assertFileExists($lockFilePath); + + LockUtils::deleteLockFile($taskId); + + $this->assertFileDoesNotExist($lockFilePath); + } + + public function testDeleteLockFileDoesNotThrowForMissingFile(): void { + $taskId = 999002; + LockUtils::deleteLockFile($taskId); + $lockFilePath = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskId; + $this->assertFileDoesNotExist($lockFilePath); + } + + public function testDeleteLockFileCleansUpOnlySpecifiedTask(): void { + $taskIdA = 999003; + $taskIdB = 999004; + $pathA = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdA; + $pathB = self::LOCK_DIR . '/' . Lock::CHUNKING . $taskIdB; + + touch($pathA); + touch($pathB); + + LockUtils::deleteLockFile($taskIdA); + + $this->assertFileDoesNotExist($pathA); + $this->assertFileExists($pathB); + + unlink($pathB); + } +} diff --git a/ci/phpunit/inc/utils/MigrationUtilsTest.php b/ci/phpunit/inc/utils/MigrationUtilsTest.php new file mode 100644 index 000000000..9a3930b0d --- /dev/null +++ b/ci/phpunit/inc/utils/MigrationUtilsTest.php @@ -0,0 +1,67 @@ +assertArrayHasKey(0, $result); + $this->assertArrayHasKey(1, $result); + $this->assertStringEndsWith('.sql', $result[0][0]); + } + + public function testGetAllGenerationsPostgresHasExpectedGenerations(): void { + $result = MigrationUtils::getAllGenerations('postgres'); + $this->assertArrayHasKey(0, $result); + $this->assertArrayHasKey(1, $result); + $this->assertStringEndsWith('.sql', $result[0][0]); + } + + public function testGetAllGenerationsUnknownTypeReturnsEmptyArray(): void { + $result = MigrationUtils::getAllGenerations('nonexistent'); + $this->assertSame([], $result); + } + + public function testGetMigrationStartEntryGeneration0ReturnsModel(): void { + putenv('HASHTOPOLIS_DB_TYPE=mysql'); + StartupConfig::getInstance(true); + $result = MigrationUtils::getMigrationStartEntry(0); + $this->assertNotNull($result); + $dict = $result->getKeyValueDict(); + $this->assertArrayHasKey('version', $dict); + $this->assertArrayHasKey('checksum', $dict); + } + + public function testGetMigrationStartEntryGeneration1ReturnsModel(): void { + putenv('HASHTOPOLIS_DB_TYPE=mysql'); + StartupConfig::getInstance(true); + $result = MigrationUtils::getMigrationStartEntry(1); + $this->assertNotNull($result); + } + + public function testGetMigrationStartEntryUnknownGenerationReturnsNull(): void { + putenv('HASHTOPOLIS_DB_TYPE=mysql'); + StartupConfig::getInstance(true); + $result = MigrationUtils::getMigrationStartEntry(999); + $this->assertNull($result); + } +} diff --git a/ci/phpunit/inc/utils/PreprocessorUtilsTest.php b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php new file mode 100644 index 000000000..48de0df46 --- /dev/null +++ b/ci/phpunit/inc/utils/PreprocessorUtilsTest.php @@ -0,0 +1,358 @@ +preprocessor = PreprocessorUtils::addPreprocessor( + 'test_pp_' . uniqid(), + 'test_binary_' . uniqid(), + 'https://example.com/test.zip', + '--keyspace', + '--skip', + '--limit' + ); + } + + #[Override] + protected function tearDown(): void { + try { + PreprocessorUtils::delete($this->preprocessor->getId()); + } + catch (Exception) { + } + parent::tearDown(); + } + + private function createPreprocessor(string $suffix = ''): Preprocessor { + $suffix = $suffix ?: uniqid(); + $pp = PreprocessorUtils::addPreprocessor( + 'tmp_pp_' . $suffix, + 'tmp_binary_' . $suffix, + 'https://example.com/' . $suffix . '.zip', + '--ks', + '--sk', + '--lm' + ); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + return $pp; + } + + public function testAddPreprocessorCreatesWithValidData(): void { + $name = 'add_create_' . uniqid(); + $binaryName = 'add_binary_' . uniqid(); + $url = 'https://example.com/add_create.zip'; + + $pp = PreprocessorUtils::addPreprocessor($name, $binaryName, $url, '--keyspace', '--skip', '--limit'); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + + $this->assertInstanceOf(Preprocessor::class, $pp); + $this->assertSame($name, $pp->getName()); + $this->assertSame($binaryName, $pp->getBinaryName()); + $this->assertSame($url, $pp->getUrl()); + $this->assertSame('--keyspace', $pp->getKeyspaceCommand()); + $this->assertSame('--skip', $pp->getSkipCommand()); + $this->assertSame('--limit', $pp->getLimitCommand()); + $this->assertNotNull($pp->getId()); + } + + public function testAddPreprocessorConvertsEmptyCommandsToNull(): void { + $pp = PreprocessorUtils::addPreprocessor( + 'add_null_cmds_' . uniqid(), + 'binary_null', + 'https://example.com/null.zip', + '', '', '' + ); + $this->registerDatabaseObject(Factory::getPreprocessorFactory(), $pp); + + $this->assertNull($pp->getKeyspaceCommand()); + $this->assertNull($pp->getSkipCommand()); + $this->assertNull($pp->getLimitCommand()); + } + + public function testAddPreprocessorThrowsForDuplicateName(): void { + $this->expectException(HttpConflict::class); + PreprocessorUtils::addPreprocessor( + $this->preprocessor->getName(), + 'binary_dup', + 'https://example.com/dup.zip', + '', '', '' + ); + } + + public function testAddPreprocessorThrowsForEmptyName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('', 'binary', 'https://example.com/e.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForEmptyBinaryName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', '', 'https://example.com/e.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForEmptyUrl(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', 'binary', '', '', '', ''); + } + + public function testAddPreprocessorThrowsForBlacklistedBinaryName(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor('name', 'bad|binary', 'https://example.com/b.zip', '', '', ''); + } + + public function testAddPreprocessorThrowsForBlacklistedKeyspace(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace;rm', '--skip', '--limit' + ); + } + + public function testAddPreprocessorThrowsForBlacklistedSkip(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace', '--skip$test', '--limit' + ); + } + + public function testAddPreprocessorThrowsForBlacklistedLimit(): void { + $this->expectException(HttpError::class); + PreprocessorUtils::addPreprocessor( + 'name', 'binary', 'https://example.com/b.zip', + '--keyspace', '--skip', '--limit&test&' + ); + } + + public function testGetPreprocessorReturnsPreprocessor(): void { + $retrieved = PreprocessorUtils::getPreprocessor($this->preprocessor->getId()); + $this->assertInstanceOf(Preprocessor::class, $retrieved); + $this->assertSame($this->preprocessor->getId(), $retrieved->getId()); + } + + public function testGetPreprocessorThrowsForInvalidId(): void { + $this->expectException(HTException::class); + PreprocessorUtils::getPreprocessor(-1); + } + + public function testDeleteRemovesPreprocessor(): void { + $pp = $this->createPreprocessor('del_test'); + $ppId = $pp->getId(); + + PreprocessorUtils::delete($ppId); + + $this->assertNull(Factory::getPreprocessorFactory()->get($ppId)); + } + + public function testDeleteThrowsForNonExistentPreprocessor(): void { + $this->expectException(HTException::class); + PreprocessorUtils::delete(-1); + } + + public function testDeleteThrowsWhenTaskUsesPreprocessor(): void { + $pp = $this->createPreprocessor('del_task'); + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($this->createAccessGroup('del_pp'), $hashType); + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $taskWrapper = $this->createTaskWrapper($this->createAccessGroup('del_pp'), $hashlist); + $this->createDatabaseObject( + Factory::getTaskFactory(), + new Task( + null, 'task_with_pp_' . uniqid(), '--attack-mode 0', 60, 30, 0, 0, 1, 1, + '#ffffff', 0, 0, 0, 0, $crackerBinary->getId(), $crackerBinaryType->getId(), + $taskWrapper->getId(), 0, '', 0, 0, 0, $pp->getId(), '' + ) + ); + + $this->expectException(HttpError::class); + PreprocessorUtils::delete($pp->getId()); + } + + public function testEditNameUpdatesName(): void { + $newName = 'rename_pp_' . uniqid(); + PreprocessorUtils::editName($this->preprocessor->getId(), $newName); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newName, $updated->getName()); + } + + public function testEditNameThrowsForDuplicateName(): void { + $other = $this->createPreprocessor('rename_dup'); + + $this->expectException(HTException::class); + PreprocessorUtils::editName($this->preprocessor->getId(), $other->getName()); + } + + public function testEditBinaryNameUpdates(): void { + $newBinary = 'new_binary_' . uniqid(); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), $newBinary); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newBinary, $updated->getBinaryName()); + } + + public function testEditBinaryNameThrowsForEmpty(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), ''); + } + + public function testEditBinaryNameThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editBinaryName($this->preprocessor->getId(), 'bad|binary'); + } + + public function testEditKeyspaceCommandUpdates(): void { + $newCmd = '--new-keyspace'; + PreprocessorUtils::editKeyspaceCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getKeyspaceCommand()); + } + + public function testEditKeyspaceCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editKeyspaceCommand($this->preprocessor->getId(), 'keyspace;rm'); + } + + public function testEditSkipCommandUpdates(): void { + $newCmd = '--new-skip'; + PreprocessorUtils::editSkipCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getSkipCommand()); + } + + public function testEditSkipCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editSkipCommand($this->preprocessor->getId(), 'skip$test'); + } + + public function testEditLimitCommandUpdates(): void { + $newCmd = '--new-limit'; + PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), $newCmd); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newCmd, $updated->getLimitCommand()); + } + + public function testEditLimitCommandThrowsForBlacklistedChars(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editLimitCommand($this->preprocessor->getId(), 'limit&test&'); + } + + public function testEditPreprocessorUpdatesAllFields(): void { + $newName = 'full_edit_' . uniqid(); + $newBinary = 'full_bin_' . uniqid(); + $newUrl = 'https://example.com/full.zip'; + + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), $newName, $newBinary, $newUrl, + '--ks', '--sk', '--lm' + ); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertSame($newName, $updated->getName()); + $this->assertSame($newBinary, $updated->getBinaryName()); + $this->assertSame($newUrl, $updated->getUrl()); + $this->assertSame('--ks', $updated->getKeyspaceCommand()); + $this->assertSame('--sk', $updated->getSkipCommand()); + $this->assertSame('--lm', $updated->getLimitCommand()); + } + + public function testEditPreprocessorThrowsForDuplicateName(): void { + $other = $this->createPreprocessor('full_dup'); + + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), $other->getName(), + 'binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), '', 'binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyBinaryName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), '', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForEmptyUrl(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', '', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedBinaryName(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'bad|binary', 'https://example.com/f.zip', + '', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedKeyspace(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', + 'keyspace;rm', '', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedSkip(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', + '', 'skip$test', '' + ); + } + + public function testEditPreprocessorThrowsForBlacklistedLimit(): void { + $this->expectException(HTException::class); + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', + '', '', 'limit`test`' + ); + } + + public function testEditPreprocessorConvertsEmptyCommandsToNull(): void { + PreprocessorUtils::editPreprocessor( + $this->preprocessor->getId(), 'name' . uniqid(), 'binary', 'https://example.com/f.zip', + '', '', '' + ); + + $updated = Factory::getPreprocessorFactory()->get($this->preprocessor->getId()); + $this->assertNull($updated->getKeyspaceCommand()); + $this->assertNull($updated->getSkipCommand()); + $this->assertNull($updated->getLimitCommand()); + } +} diff --git a/ci/phpunit/inc/utils/TaskUtilsTest.php b/ci/phpunit/inc/utils/TaskUtilsTest.php new file mode 100644 index 000000000..92bf8012d --- /dev/null +++ b/ci/phpunit/inc/utils/TaskUtilsTest.php @@ -0,0 +1,270 @@ +createTaskHelper(); + + TaskUtils::editNotes($taskObjects["task"]->getId(), 'task note', $taskObjects["user"]); + + $taskUpdated = Factory::getTaskFactory()->get($taskObjects["task"]->getId()); + $this->assertEquals('task note', $taskUpdated->getNotes()); + } + + /** + * Test the status calculation of a task. + * + * @return void + * @throws Exception + */ + public function testGetStatus(): void { + $this->assertEquals(3, TaskUtils::getStatus([], 100, 100)); + $this->assertEquals(3, TaskUtils::getStatus([], 100, 101)); + + //TODO test status 1 (running) and 2 (idle) too + } + + /** + * Test the deletion of archived tasks. + * + * @return void + * @throws Exception + */ + /*public function testDeleteArchived(): void { + $this->task1->setIsArchived(1); + + //TODO filter for specific user too on $numberOfArchivedTasks and $numberOfArchivedTasksUpdated + $numberOfArchivedTasks = Factory::getTaskFactory()->filter(['isArchived' => true, ]); + + TaskUtil::deleteArchived($this->user1); + $numberOfArchivedTasksUpdated = Factory::getTaskFactory()->filter(['isArchived' => true, ]); + + $this->assertEquals(0, $numberOfArchivedTasksUpdated); + $this->assertNotEquals($numberOfArchivedTasks, $numberOfArchivedTasksUpdated); + }*/ + + /** + * Test changing the attack command. + * + * @return void + * @throws Exception + */ + public function testChangeAttackCmd(): void { + $taskObjects = $this->createTaskHelper(); + TaskUtils::changeAttackCmd($taskObjects["task"]->getId(), '#HL# custom attack cmd', $taskObjects["user"]); + + $taskUpdated = Factory::getTaskFactory()->get($taskObjects["task"]->getId()); + $this->assertEquals('#HL# custom attack cmd', $taskUpdated->getAttackCmd()); + } + + /** + * Test archiving a supertask. + * + * @return void + * @throws Exception + */ + /*public function testArchiveSupertask(): void { + $supertask; + $supertaskWrapper; + $user; + + TaskUtils::archiveSupertask($supertask->getId(), $user); + + //TODO filter all task wrappers with the id of the $supertaskWrapper (using taskfactory?) and check if they're archived + + $supertaskWrapperUpdated = Factory::getTaskWrapperFactory()->get($supertaskWrapper); + $this->assertEquals(1, $supertaskWrapperUpdated->getIsArchived()); + }*/ + + /** + * Test archiving a task. + * + * @return void + * @throws Exception + */ + public function testArchiveTask(): void { + $taskObjects = $this->createTaskHelper(); + TaskUtils::archiveTask($taskObjects["task"]->getId(), $taskObjects["user"]); + + $taskWrapperUpdated = TaskUtils::getTaskWrapper($taskObjects["task"]->getTaskWrapperId(), $taskObjects["user"]); + $this->assertEquals(1, $taskWrapperUpdated->getIsArchived()); + + $taskUpdated = Factory::getTaskFactory()->get($taskObjects["task"]->getId()); + $this->assertEquals(1, $taskUpdated->getIsArchived()); + } + + /** + * Test toggle of archiving a normal task and a supertask. + * + * @return void + * @throws Exception + */ + /*public function testToggleArchiveTask(): void { + $task; + $taskTaskWrapper; + $supertask; + $supertaskWrapper; + $user; + + //Archive task + TaskUtils::toggleArchiveTask($task->getId(), 1, $user); + + $taskWrapperUpdated = TaskUtils::getTaskWrapper($task->getTaskWrapperId(), $user); + $this->assertEquals(1, $taskWrapperUpdated->getIsArchived()); + + $taskUpdated = Factory::getTaskFactory()->get($task->getId()); + $this->assertEquals(1, $taskUpdated->getIsArchived()); + + + //Un-archive task again + TaskUtils::toggleArchiveTask($task->getId(), 0, $user); + + $taskWrapperUpdated = TaskUtils::getTaskWrapper($task->getTaskWrapperId(), $user); + $this->assertEquals(0, $taskWrapperUpdated->getIsArchived()); + + $taskUpdated = Factory::getTaskFactory()->get($task->getId()); + $this->assertEquals(0, $taskUpdated->getIsArchived()); + + + //Archive supertask + TaskUtils::toggleArchiveTask($supertask->getId(), 1, $user); + + //TODO filter all task wrappers with the id of the $supertaskWrapper (using taskfactory?) and check if they're archived + + $supertaskWrapperUpdated = Factory::getTaskWrapperFactory()->get($supertaskWrapper); + $this->assertEquals(1, $supertaskWrapperUpdated->getIsArchived()); + + + //Un-archive supertask again + TaskUtils::toggleArchiveTask($supertask->getId(), 0, $user); + + //TODO filter all task wrappers with the id of the $supertaskWrapper (using taskfactory?) and check if they're archived + + $supertaskWrapperUpdated = Factory::getTaskWrapperFactory()->get($supertaskWrapper); + $this->assertEquals(0, $supertaskWrapperUpdated->getIsArchived()); + }*/ + + /** + * Test renaming a running supertask. + * + * @return void + * @throws Exception + */ + /*public function testRenameSupertask(): void { + $supertask; + $supertaskWrapper; + $user; + + TaskUtils::renameSupertask($supertaskWrapper->getId(), 'custom new supertask name', $user); + + $supertaskWrapperUpdated = TaskUtils::getTaskWrapper($supertaskWrapper->getId(), $user); + $this->assertEquals('custom new supertask name', $supertaskWrapperUpdated->getTaskWrapperName()); + }*/ + + + /** + * Test getting the task of wrapper. + * + * @return void + * @throws Exception + */ + public function testGetTaskOfWrapper(): void { + $taskObjects = $this->createTaskHelper(); + $this->assertEquals($taskObjects["task"]->getId(), TaskUtils::getTaskOfWrapper($taskObjects["taskWrapper"]->getId())->getId()); + } + + /** + * Test getting tasks of wrapper. + * + * @return void + * @throws Exception + */ + /*public function testGetTasksOfWrapper(): void { + //TODO create supertask + $this->assertEquals(2, count(TaskUtils::getTasksOfWrapper($this->taskWrapper1->getId()))); + }*/ + + /** + * Test getting task wrappers for a user. + * + * @return void + * @throws Exception + */ + /*public function testGetTaskWrappersForUser(): void { + $taskObjects = $this->createTaskHelper(); + $taskObjects2 = $this->createTaskHelper(); + + $taskObjects2["taskWrapper"]->setAccessGroupId($taskObjects["accessGroup"]->getId()); + //$this->createAccessGroupUser($taskObjects2["user"], $taskObjects["accessGroup"]); + + //var_dump($taskObjects); + //var_dump($taskObjects2); + + $this->assertEquals(2, count(TaskUtils::getTaskWrappersForUser($taskObjects["user"]))); + }*/ + + + /** + * Test setting the CPU only flag for a task. + * + * @return void + * @throws Exception + */ + public function testSetCpuTask(): void { + $taskObjects = $this->createTaskHelper(); + + //Set to CPU-only + TaskUtils::setCpuTask($taskObjects["task"]->getId(), 1, $taskObjects["user"]); + $taskUpdated = Factory::getTaskFactory()->get($taskObjects["task"]->getId()); + $this->assertEquals(1, $taskUpdated->getIsCpuTask()); + + //Set to use GPU and CPU + TaskUtils::setCpuTask($taskObjects["task"]->getId(), 0, $taskObjects["user"]); + $taskUpdated = Factory::getTaskFactory()->get($taskObjects["task"]->getId()); + $this->assertEquals(0, $taskUpdated->getIsCpuTask()); + } + + public function createTaskHelper(): array { + $user = $this->createUser("phpunit"); + $accessGroup = $this->createAccessGroup("phpunit"); + $this->createAccessGroupUser($user, $accessGroup); + + $hashType = $this->createHashType(); + $hashlist = $this->createHashlist($accessGroup, $hashType); + + $taskWrapper = $this->createTaskWrapper($accessGroup, $hashlist); + + $crackerBinaryType = $this->createCrackerBinaryType(); + $crackerBinary = $this->createCrackerBinary($crackerBinaryType); + $task = $this->createTask($taskWrapper, $crackerBinary, $crackerBinaryType); + + return array("user"=> $user, "accessGroup"=>$accessGroup, "hashType"=>$hashType, "hashlist"=>$hashlist, "taskWrapper"=>$taskWrapper, "crackerBinaryType"=>$crackerBinaryType, "crackerBinary"=>$crackerBinary, "task"=>$task); + } +} diff --git a/ci/phpunit/inc/utils/UserUtilsTest.php b/ci/phpunit/inc/utils/UserUtilsTest.php new file mode 100644 index 000000000..8ee5264b3 --- /dev/null +++ b/ci/phpunit/inc/utils/UserUtilsTest.php @@ -0,0 +1,107 @@ +uniqueUsername('mail_disabled'); + + \hashtopolis_set_test_mock('Hashtopolis\\inc\\is_file', static function ($path): bool { + return false; + }); + \hashtopolis_set_test_mock('Hashtopolis\\inc\\mail', static function () use (&$mailCallCount): bool { + $mailCallCount++; + return true; + }); + + $createdUser = UserUtils::createUser($username, $username . '@example.com', $this->createRightGroup()->getId(), $this->adminUser); + $this->registerDatabaseObject(Factory::getUserFactory(), $createdUser); + + $this->assertSame($username, $createdUser->getUsername()); + $this->assertSame(0, $mailCallCount); + } + + /** + * @throws InternalError + * @throws HTException + * @throws HttpError + * @throws HttpConflict + */ + public function testCreateUserCallsSendMailWhenMailIsConfigured(): void { + $mailCalls = []; + $username = $this->uniqueUsername('mail_enabled'); + + \hashtopolis_set_test_mock('Hashtopolis\\inc\\is_file', static function ($path): bool { + return true; + }); + \hashtopolis_set_test_mock('Hashtopolis\\inc\\mail', static function ($to, $subject, $message, $additionalHeaders = null, $additionalParams = null) use (&$mailCalls): bool { + $mailCalls[] = [$to, $subject, $message, $additionalHeaders, $additionalParams]; + return true; + }); + + $createdUser = UserUtils::createUser($username, $username . '@example.com', $this->createRightGroup()->getId(), $this->adminUser); + $this->registerDatabaseObject(Factory::getUserFactory(), $createdUser); + + $this->assertCount(1, $mailCalls); + $this->assertSame($username . '@example.com', $mailCalls[0][0]); + $this->assertSame('Account at ' . APP_NAME, $mailCalls[0][1]); + } + + /** + * @throws HTException + * @throws HttpError + * @throws HttpConflict + */ + public function testCreateUserThrowsWhenConfiguredSendMailFails(): void { + $mailCallCount = 0; + $username = $this->uniqueUsername('mail_failure'); + + \hashtopolis_set_test_mock('Hashtopolis\\inc\\is_file', static function ($path): bool { + return true; + }); + \hashtopolis_set_test_mock('Hashtopolis\\inc\\mail', static function () use (&$mailCallCount): bool { + $mailCallCount++; + return false; + }); + + $this->expectException(InternalError::class); + try { + $createdUser = UserUtils::createUser($username, $username . '@example.com', $this->createRightGroup()->getId(), $this->adminUser); + $this->registerDatabaseObject(Factory::getUserFactory(), $createdUser); + } + finally { + $this->assertSame(1, $mailCallCount); + $this->registerDatabaseObject(Factory::getUserFactory(), Factory::getUserFactory()->filter([Factory::FILTER => new QueryFilter(User::USERNAME, $username, "=")], true)); + } + } + + private function uniqueUsername(string $prefix): string { + return $prefix . '_' . uniqid(); + } +} diff --git a/ci/run.php b/ci/run.php index 747d4bb12..709d37afc 100644 --- a/ci/run.php +++ b/ci/run.php @@ -1,13 +1,13 @@ query("CREATE DATABASE IF NOT EXISTS hashtopolis;"); $db->query("USE hashtopolis;"); - $db->query(file_get_contents($envPath . "src/install/hashtopolis.sql")); + $db->query(file_get_contents($envPath . "src/migrations/mysql/20251127000000_initial.sql")); } catch (PDOException $e) { fwrite(STDERR, "Failed to initialize database: " . $e->getMessage()); exit(-1); } - -$load = file_get_contents($envPath . "src/inc/load.php"); -$load = str_replace('ini_set("display_errors", "0");', 'ini_set("display_errors", "1");', $load); -file_put_contents($envPath . "src/inc/load.php", $load); diff --git a/ci/tests/AccountTest.class.php b/ci/tests/AccountTest.class.php deleted file mode 100644 index 1077982d9..000000000 --- a/ci/tests/AccountTest.class.php +++ /dev/null @@ -1,125 +0,0 @@ -getTestName() . "..."); - parent::init($version); - } - - public function run() { - HashtopolisTestFramework::log(HashtopolisTestFramework::LOG_INFO, "Running " . $this->getTestName() . "..."); - $this->testGetInformation(["userId" => 1, "rightGroupId" => 1]); - $this->testSetEmail('otheremail@example.org'); - $this->testGetInformation(["userId" => 1, "rightGroupId" => 1, 'email' => 'otheremail@example.org']); - $this->testSetEmail('invalid-email', false); - $this->testSetEmail('', false); - $this->testGetInformation(["userId" => 1, "rightGroupId" => 1, 'email' => 'otheremail@example.org']); - $this->testSetSessionLength(6000); - $this->testSetSessionLength(500000, false); - $this->testSetSessionLength(0, false); - $this->testSetSessionLength(-6000, false); - $this->testGetInformation(["userId" => 1, "rightGroupId" => 1, 'email' => 'otheremail@example.org', 'sessionLength' => 6000]); - $this->testChangePassword(HashtopolisTest::USER_PASS, 'newPassword'); - $this->testChangePassword(HashtopolisTest::USER_PASS, 'newPassword', false); - $this->testChangePassword('newPassword', 'newPassword', false); - $this->testChangePassword('newPassword', '', false); - $this->testChangePassword('newPassword', '123', false); - HashtopolisTestFramework::log(HashtopolisTestFramework::LOG_INFO, $this->getTestName() . " completed"); - } - - private function testChangePassword($old, $new, $assert = true) { - $response = HashtopolisTestFramework::doRequest([ - "section" => "account", - "request" => "changePassword", - "oldPassword" => $old, - "newPassword" => $new, - "accessKey" => "mykey" - ], HashtopolisTestFramework::REQUEST_UAPI - ); - if ($response === false) { - $this->testFailed("AccountTest:testChangePassword($old,$new,$assert)", "Empty response"); - } - else if (!$this->validState($response['response'], $assert)) { - $this->testFailed("AccountTest:testChangePassword($old,$new,$assert)", "Response does not match assert"); - } - else { - $this->testSuccess("AccountTest:testChangePassword($old,$new,$assert)"); - } - } - - private function testSetSessionLength($length, $assert = true) { - $response = HashtopolisTestFramework::doRequest([ - "section" => "account", - "request" => "setSessionLength", - "sessionLength" => $length, - "accessKey" => "mykey" - ], HashtopolisTestFramework::REQUEST_UAPI - ); - if ($response === false) { - $this->testFailed("AccountTest:testSetSessionLength($length,$assert)", "Empty response"); - } - else if (!$this->validState($response['response'], $assert)) { - $this->testFailed("AccountTest:testSetSessionLength($length,$assert)", "Response does not match assert"); - } - else { - $this->testSuccess("AccountTest:testSetSessionLength($length,$assert)"); - } - } - - private function testSetEmail($email, $assert = true) { - $response = HashtopolisTestFramework::doRequest([ - "section" => "account", - "request" => "setEmail", - "email" => $email, - "accessKey" => "mykey" - ], HashtopolisTestFramework::REQUEST_UAPI - ); - if ($response === false) { - $this->testFailed("AccountTest:testSetEmail($email,$assert)", "Empty response"); - } - else if (!$this->validState($response['response'], $assert)) { - $this->testFailed("AccountTest:testSetEmail($email,$assert)", "Response does not match assert"); - } - else { - $this->testSuccess("AccountTest:testSetEmail($email,$assert)"); - } - } - - private function testGetInformation($data, $assert = true) { - $response = HashtopolisTestFramework::doRequest([ - "section" => "account", - "request" => "getInformation", - "accessKey" => "mykey" - ], HashtopolisTestFramework::REQUEST_UAPI - ); - if ($response === false) { - $this->testFailed("AccountTest:testGetInformation([" . implode(", ", $data) . "],$assert)", "Empty response"); - } - else if (!$this->validState($response['response'], $assert)) { - $this->testFailed("AccountTest:testGetInformation([" . implode(", ", $data) . "],$assert)", "Response does not match assert"); - } - else { - if (!$assert) { - $this->testSuccess("AccountTest:testGetInformation([" . implode(", ", $data) . "],$assert)"); - return; - } - foreach ($data as $key => $val) { - if (!isset($response[$key]) || $val != $response[$key]) { - $this->testFailed("AccountTest:testGetInformation([" . implode(", ", $data) . "],$assert)", "Response OK, but wrong response"); - return; - } - } - $this->testSuccess("AccountTest:testGetInformation([" . implode(", ", $data) . "],$assert)"); - } - } - - public function getTestName() { - return "Account Test"; - } -} - -HashtopolisTestFramework::register(new AccountTest()); \ No newline at end of file diff --git a/ci/tests/AgentTest.class.php b/ci/tests/AgentTest.class.php index 62154028b..6ce10619d 100644 --- a/ci/tests/AgentTest.class.php +++ b/ci/tests/AgentTest.class.php @@ -1,5 +1,8 @@ "importCracked", "hashlistId" => 1, "separator" => ":", + "overwrite" => 0, // sending 3 founds of the hashlist "data" => "MDAyODA4MGU3ZmE4YzgxMjY4ZWYzNDBkN2Q2OTI2ODE6Zm91bmQxCjAwMmU5NWQ4MmJlMzAzOTZmY2NkMzc1ZmYyM2Y4YjRjOmZvdW5kMgowMDM0YzVlNDE4YWU0ZjJlYmE1OTBhMTY2OTZlZGJiMzpmb3VuZDM=", "accessKey" => "mykey" diff --git a/ci/tests/PretaskTest.class.php b/ci/tests/PretaskTest.class.php index f107bdaff..4fe69c402 100644 --- a/ci/tests/PretaskTest.class.php +++ b/ci/tests/PretaskTest.class.php @@ -1,5 +1,8 @@ "getChunk", "taskId" => $task1Id, "token" => $agent2["token"]]); - if ($response["response"] !== "ERROR" || $response["message"] != "Task already saturated by other agents, no other task available!") { + if ($response["response"] !== "ERROR" || $response["message"] != "You are not assigned to this task!") { $this->testFailed("MaxAgentsTest:testTaskMaxAgents()", sprintf("Expected getChunk to fail, instead got: %s", implode(", ", $response))); return; } @@ -607,4 +610,4 @@ public function getTestName() { } } -HashtopolisTestFramework::register(new MaxAgentsTest()); \ No newline at end of file +HashtopolisTestFramework::register(new MaxAgentsTest()); diff --git a/ci/tests/integration/RuleSplitTest.class.php b/ci/tests/integration/RuleSplitTest.class.php index fe5651230..68ca145ab 100644 --- a/ci/tests/integration/RuleSplitTest.class.php +++ b/ci/tests/integration/RuleSplitTest.class.php @@ -1,6 +1,8 @@ testFailed("RuleSplitTest:testRuleSplit()", sprintf("Expected benchmark to return OK.")); } else { - if (!$this->getTask('task-1 (From Rule Split)') === false) { + if ($this->getTask('task-1 (From Rule Split)')) { $this->testSuccess("RuleSplitTest:testRuleSplit()"); } else { $this->testFailed("RuleSplitTest:testRuleSplit()", sprintf("Couldn't find the created supertask")); diff --git a/composer.json b/composer.json index 2e590d5df..e830b353a 100644 --- a/composer.json +++ b/composer.json @@ -7,34 +7,37 @@ "router", "psr7" ], - "homepage": "http://github.com/hashtopolis/server", + "homepage": "https://github.com/hashtopolis/server", "license": "MIT", "authors": [ { "name": "Various Authors", "email": "noreply@example.org", - "homepage": "http://example.org" + "homepage": "https://example.org" } ], "require": { - "php": "^7.4 || ^8.0", + "php": "^8.2", + "ext-gd": "*", "ext-json": "*", + "ext-pdo": "*", + "composer/semver": "^3.4", "crell/api-problem": "^3.6", + "firebase/php-jwt": "7.0.2", + "jimtools/basic-auth": "^1.0", + "jimtools/jwt-auth": "^3.0", + "middlewares/encoder": "^2.1", "middlewares/negotiation": "^2.1", "monolog/monolog": "^2.8", "php-di/php-di": "7.0.7", "slim/psr7": "^1.5", - "slim/slim": "^4.10", - "tuupola/slim-basic-auth": "^3.3", - "tuupola/slim-jwt-auth": "^3.6", - "ext-pdo" : "*" + "slim/slim": "^4.10" }, "require-dev": { - "jangregor/phpstan-prophecy": "^1.0.0", "phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.1.0", - "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^9.5.25", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.5.7", "squizlabs/php_codesniffer": "^3.7" }, "config": { @@ -49,12 +52,12 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "Hashtopolis\\": "src/" } }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Hashtopolis\\": "ci/phpunit/" } }, "scripts": { diff --git a/composer.lock b/composer.lock index 7f158432b..58b16b6f3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,31 +4,108 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bcdc14df33ed1da39f804212f6df89e0", + "content-hash": "f79a8ed206218eeeefbc541ed3ff19a9", "packages": [ + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "crell/api-problem", - "version": "3.7.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/Crell/ApiProblem.git", - "reference": "b41d66dc1d403b2d406699e2e05bb2b48efe3b7f" + "reference": "ddd6893a0aac8ecbebd6a6741b82eff974fc3b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Crell/ApiProblem/zipball/b41d66dc1d403b2d406699e2e05bb2b48efe3b7f", - "reference": "b41d66dc1d403b2d406699e2e05bb2b48efe3b7f", + "url": "https://api.github.com/repos/Crell/ApiProblem/zipball/ddd6893a0aac8ecbebd6a6741b82eff974fc3b4b", + "reference": "ddd6893a0aac8ecbebd6a6741b82eff974fc3b4b", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^8.3" }, "require-dev": { - "nyholm/psr7": "^1.8", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "psr/http-factory": "^1.0", - "psr/http-message": "1.*" + "nyholm/psr7": "^1.8.2", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.6.31", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1" }, "suggest": { "psr/http-factory": "Common interfaces for PSR-7 HTTP message factories", @@ -67,7 +144,7 @@ ], "support": { "issues": "https://github.com/Crell/ApiProblem/issues", - "source": "https://github.com/Crell/ApiProblem/tree/3.7.0" + "source": "https://github.com/Crell/ApiProblem/tree/3.8.0" }, "funding": [ { @@ -75,7 +152,7 @@ "type": "github" } ], - "time": "2024-09-30T22:47:27+00:00" + "time": "2026-02-03T20:47:47+00:00" }, { "name": "fig/http-message-util", @@ -135,25 +212,31 @@ }, { "name": "firebase/php-jwt", - "version": "v5.5.1", + "version": "v7.0.2", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "83b609028194aa042ea33b5af2d41a7427de80e6" + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", - "reference": "83b609028194aa042ea33b5af2d41a7427de80e6", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^8.0" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" }, "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" }, "type": "library", @@ -185,10 +268,152 @@ "php" ], "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v5.5.1" + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.0.2" + }, + "time": "2025-12-16T22:17:28+00:00" + }, + { + "name": "jimtools/basic-auth", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/JimTools/basic-auth.git", + "reference": "29488cce4694773997b67b535ce9d6bf353d3acc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JimTools/basic-auth/zipball/29488cce4694773997b67b535ce9d6bf353d3acc", + "reference": "29488cce4694773997b67b535ce9d6bf353d3acc", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/http-server-middleware": "^1.0", + "tuupola/callable-handler": "^0.3.0|^0.4.0|^1.0", + "tuupola/http-factory": "^0.4.0|^1.0.2" + }, + "replace": { + "tuupola/slim-basic-auth": "*" + }, + "require-dev": { + "equip/dispatch": "^2.0", + "laminas/laminas-diactoros": "^1.3|^2.0|^3.0", + "overtrue/phplint": "^3.0|^4.0|^5.0|^6.0", + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^8.5.30|^9.0", + "rector/rector": "^0.14.5", + "symplify/easy-coding-standard": "^11.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Middleware\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "https://appelsiini.net/", + "role": "Creator" + }, + { + "name": "James Read", + "email": "james.read.18@gmail.com", + "role": "Maintainer" + } + ], + "description": "PSR-7 and PSR-15 HTTP Basic Authentication Middleware", + "homepage": "https://appelsiini.net/projects/slim-basic-auth", + "keywords": [ + "auth", + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "issues": "https://github.com/JimTools/basic-auth/issues", + "source": "https://github.com/JimTools/basic-auth/tree/v1.0.0" + }, + "time": "2026-03-31T18:51:01+00:00" + }, + { + "name": "jimtools/jwt-auth", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/JimTools/jwt-auth.git", + "reference": "9e116b1e976b91d60c701bc37ee4c7c2a9fcadb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JimTools/jwt-auth/zipball/9e116b1e976b91d60c701bc37ee4c7c2a9fcadb9", + "reference": "9e116b1e976b91d60c701bc37ee4c7c2a9fcadb9", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^7.0", + "php": "~8.2 || ~8.3 || ~8.4 || ~8.5", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-middleware": "^1.0" + }, + "replace": { + "tuupola/slim-jwt-auth": "*" + }, + "require-dev": { + "equip/dispatch": "^2.0", + "ext-openssl": "*", + "friendsofphp/php-cs-fixer": "^3.89", + "laminas/laminas-diactoros": "^3.7", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5 || ^12.4", + "rector/rector": "^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "JimTools\\JwtAuth\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Read", + "email": "james.read.18@gmail.com", + "homepage": "https://github.com/jimtools", + "role": "Developer" + } + ], + "description": "PSR-15 JWT Authentication middleware, A replacement for tuupola/slim-jwt-auth", + "homepage": "https://github.com/jimtools/jwt-auth", + "keywords": [ + "auth", + "json", + "jwt", + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "issues": "https://github.com/JimTools/jwt-auth/issues", + "source": "https://github.com/JimTools/jwt-auth/tree/3.0.1" }, - "time": "2021-11-08T20:18:51+00:00" + "funding": [ + { + "url": "https://github.com/JimTools", + "type": "github" + } + ], + "time": "2026-03-17T23:38:37+00:00" }, { "name": "laravel/serializable-closure", @@ -251,6 +476,62 @@ }, "time": "2024-11-14T18:34:49+00:00" }, + { + "name": "middlewares/encoder", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/middlewares/encoder.git", + "reference": "08097bf64bcafc997ffd52757e2c63b4d9cd4265" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/middlewares/encoder/zipball/08097bf64bcafc997ffd52757e2c63b4d9cd4265", + "reference": "08097bf64bcafc997ffd52757e2c63b4d9cd4265", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "middlewares/utils": "^2 || ^3 || ^4", + "php": "^7.2 || ^8.0", + "psr/http-server-middleware": "^1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "laminas/laminas-diactoros": "^2 || ^3", + "oscarotero/php-cs-fixer-config": "^2", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9", + "squizlabs/php_codesniffer": "^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Middlewares\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Middleware to encode the response body to gzip or deflate", + "homepage": "https://github.com/middlewares/encoder", + "keywords": [ + "compression", + "deflate", + "encoding", + "gzip", + "http", + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "issues": "https://github.com/middlewares/encoder/issues", + "source": "https://github.com/middlewares/encoder/tree/v2.2.0" + }, + "time": "2025-03-23T10:27:13+00:00" + }, { "name": "middlewares/negotiation", "version": "v2.2.0", @@ -369,16 +650,16 @@ }, { "name": "monolog/monolog", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" + "reference": "37308608e599f34a1a4845b16440047ec98a172a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", "shasum": "" }, "require": { @@ -396,7 +677,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2@dev", "guzzlehttp/guzzle": "^7.4", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "phpspec/prophecy": "^1.15", "phpstan/phpstan": "^1.10", @@ -455,7 +736,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.10.0" + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" }, "funding": [ { @@ -467,7 +748,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T12:43:37+00:00" + "time": "2026-01-01T13:05:00+00:00" }, { "name": "nikic/fast-route", @@ -1340,39 +1621,34 @@ "time": "2021-09-14T12:46:25+00:00" }, { - "name": "tuupola/slim-basic-auth", - "version": "3.4.0", + "name": "willdurand/negotiation", + "version": "3.1.0", "source": { "type": "git", - "url": "https://github.com/tuupola/slim-basic-auth.git", - "reference": "4f3061cd1632a28aa7342495011b3467fe0fe1d1" + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tuupola/slim-basic-auth/zipball/4f3061cd1632a28aa7342495011b3467fe0fe1d1", - "reference": "4f3061cd1632a28aa7342495011b3467fe0fe1d1", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "psr/http-message": "^1.0.1|^2.0", - "psr/http-server-middleware": "^1.0", - "tuupola/callable-handler": "^0.3.0|^0.4.0|^1.0", - "tuupola/http-factory": "^0.4.0|^1.0.2" + "php": ">=7.1.0" }, "require-dev": { - "equip/dispatch": "^2.0", - "laminas/laminas-diactoros": "^1.3|^2.0|^3.0", - "overtrue/phplint": "^3.0|^4.0|^5.0|^6.0", - "phpstan/phpstan": "^1.11", - "phpunit/phpunit": "^8.5.30|^9.0", - "rector/rector": "^0.14.5", - "symplify/easy-coding-standard": "^11.1" + "symfony/phpunit-bridge": "^5.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { "psr-4": { - "Tuupola\\Middleware\\": "src" + "Negotiation\\": "src/Negotiation" } }, "notification-url": "https://packagist.org/downloads/", @@ -1381,179 +1657,52 @@ ], "authors": [ { - "name": "Mika Tuupola", - "email": "tuupola@appelsiini.net", - "homepage": "https://appelsiini.net/" + "name": "William Durand", + "email": "will+git@drnd.me" } ], - "description": "PSR-7 and PSR-15 HTTP Basic Authentication Middleware", - "homepage": "https://appelsiini.net/projects/slim-basic-auth", + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", "keywords": [ - "auth", - "middleware", - "psr-15", - "psr-7" + "accept", + "content", + "format", + "header", + "negotiation" ], "support": { - "issues": "https://github.com/tuupola/slim-basic-auth/issues", - "source": "https://github.com/tuupola/slim-basic-auth/tree/3.4.0" + "issues": "https://github.com/willdurand/Negotiation/issues", + "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" }, - "time": "2024-10-01T09:13:06+00:00" - }, + "time": "2022-01-30T20:08:53+00:00" + } + ], + "packages-dev": [ { - "name": "tuupola/slim-jwt-auth", - "version": "3.8.0", + "name": "doctrine/deprecations", + "version": "1.1.6", "source": { "type": "git", - "url": "https://github.com/tuupola/slim-jwt-auth.git", - "reference": "7829d4482034e9eb5e051f3a1619db0c704ba7e7" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tuupola/slim-jwt-auth/zipball/7829d4482034e9eb5e051f3a1619db0c704ba7e7", - "reference": "7829d4482034e9eb5e051f3a1619db0c704ba7e7", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { - "firebase/php-jwt": "^3.0|^4.0|^5.0", - "php": "^7.4|^8.0", - "psr/http-message": "^1.0|^2.0", - "psr/http-server-middleware": "^1.0", - "psr/log": "^1.0|^2.0|^3.0", - "tuupola/callable-handler": "^1.0", - "tuupola/http-factory": "^1.3" - }, - "require-dev": { - "equip/dispatch": "^2.0", - "laminas/laminas-diactoros": "^2.0|^3.0", - "overtrue/phplint": "^1.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^7.0|^8.5.30|^9.0", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-3.x": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Tuupola\\Middleware\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mika Tuupola", - "email": "tuupola@appelsiini.net", - "homepage": "https://appelsiini.net/", - "role": "Developer" - } - ], - "description": "PSR-7 and PSR-15 JWT Authentication Middleware", - "homepage": "https://github.com/tuupola/slim-jwt-auth", - "keywords": [ - "auth", - "json", - "jwt", - "middleware", - "psr-15", - "psr-7" - ], - "support": { - "issues": "https://github.com/tuupola/slim-jwt-auth/issues", - "source": "https://github.com/tuupola/slim-jwt-auth/tree/3.8.0" - }, - "abandoned": "jimtools/jwt-auth", - "time": "2023-10-20T09:51:26+00:00" - }, - { - "name": "willdurand/negotiation", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/willdurand/Negotiation.git", - "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", - "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Negotiation\\": "src/Negotiation" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "William Durand", - "email": "will+git@drnd.me" - } - ], - "description": "Content Negotiation tools for PHP provided as a standalone library.", - "homepage": "http://williamdurand.fr/Negotiation/", - "keywords": [ - "accept", - "content", - "format", - "header", - "negotiation" - ], - "support": { - "issues": "https://github.com/willdurand/Negotiation/issues", - "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" - }, - "time": "2022-01-30T20:08:53+00:00" - } - ], - "packages-dev": [ - { - "name": "doctrine/deprecations", - "version": "1.1.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" + "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -1573,36 +1722,35 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -1629,7 +1777,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -1645,66 +1793,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" - }, - { - "name": "jangregor/phpstan-prophecy", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/Jan0707/phpstan-prophecy.git", - "reference": "5ee56c7db1d58f0578c82a35e3c1befe840e85a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jan0707/phpstan-prophecy/zipball/5ee56c7db1d58f0578c82a35e3c1befe840e85a9", - "reference": "5ee56c7db1d58f0578c82a35e3c1befe840e85a9", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0.0" - }, - "conflict": { - "phpspec/prophecy": "<1.7.0 || >=2.0.0", - "phpunit/phpunit": "<6.0.0 || >=12.0.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.1.1", - "ergebnis/license": "^1.0.0", - "ergebnis/php-cs-fixer-config": "~2.2.0", - "phpspec/prophecy": "^1.7.0", - "phpunit/phpunit": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "JanGregor\\Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Gregor Emge-Triebel", - "email": "jan@jangregor.me" - } - ], - "description": "Provides a phpstan/phpstan extension for phpspec/prophecy", - "support": { - "issues": "https://github.com/Jan0707/phpstan-prophecy/issues", - "source": "https://github.com/Jan0707/phpstan-prophecy/tree/1.0.2" - }, - "time": "2024-04-03T08:15:54+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "myclabs/deep-copy", @@ -1997,16 +2086,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.5", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2014,9 +2103,9 @@ "ext-filter": "*", "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -2025,7 +2114,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -2055,44 +2145,44 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2025-11-27T19:50:05+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.12.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -2113,37 +2203,37 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2025-11-21T15:09:14+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.24.0", + "version": "v1.26.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "a24f1bda2d00a03877f7f99d9e6b150baf543f6d" + "reference": "09c2e5949d676286358a62af818f8407167a9dd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a24f1bda2d00a03877f7f99d9e6b150baf543f6d", - "reference": "a24f1bda2d00a03877f7f99d9e6b150baf543f6d", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/09c2e5949d676286358a62af818f8407167a9dd6", + "reference": "09c2e5949d676286358a62af818f8407167a9dd6", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2 || ^2.0", "php": "8.2.* || 8.3.* || 8.4.* || 8.5.*", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpdocumentor/reflection-docblock": "^5.2 || ^6.0", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2.5 || ^3.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.88", + "php-cs-fixer/shim": "^3.93.1", "phpspec/phpspec": "^6.0 || ^7.0 || ^8.0", - "phpstan/phpstan": "^2.1.13", - "phpunit/phpunit": "^11.0 || ^12.0" + "phpstan/phpstan": "^2.1.13, <2.1.34 || ^2.1.39", + "phpunit/phpunit": "^11.0 || ^12.0 || ^13.0" }, "type": "library", "extra": { @@ -2184,28 +2274,28 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.24.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.26.1" }, - "time": "2025-11-21T13:10:52+00:00" + "time": "2026-04-13T14:35:16+00:00" }, { "name": "phpspec/prophecy-phpunit", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy-phpunit.git", - "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded" + "reference": "89f91b01d0640b7820e427e02a007bc6489d8a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/d3c28041d9390c9bca325a08c5b2993ac855bded", - "reference": "d3c28041d9390c9bca325a08c5b2993ac855bded", + "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/89f91b01d0640b7820e427e02a007bc6489d8a26", + "reference": "89f91b01d0640b7820e427e02a007bc6489d8a26", "shasum": "" }, "require": { "php": "^7.3 || ^8", "phpspec/prophecy": "^1.18", - "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0 || ^12.0" + "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0 || ^12.0 || ^13.0" }, "require-dev": { "phpstan/phpstan": "^1.10" @@ -2239,9 +2329,9 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy-phpunit/issues", - "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.4.0" + "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.5.0" }, - "time": "2025-05-13T13:52:32+00:00" + "time": "2026-02-09T15:40:55+00:00" }, { "name": "phpstan/extension-installer", @@ -2293,16 +2383,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -2334,21 +2424,21 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -2389,39 +2479,37 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "12.5.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "876099a072646c7745f673d7aeab5382c4439691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", - "theseer/tokenizer": "^1.2.3" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -2430,7 +2518,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -2459,40 +2547,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2026-04-15T08:23:17+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2519,36 +2619,49 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -2556,7 +2669,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2582,7 +2695,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -2590,32 +2704,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2641,7 +2755,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -2649,32 +2764,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -2700,7 +2815,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -2708,24 +2824,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.31", + "version": "12.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5" + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -2735,27 +2850,23 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", - "sebastian/global-state": "^5.0.8", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.6", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.0", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, "bin": [ "phpunit" @@ -2763,7 +2874,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -2795,56 +2906,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2025-12-06T07:45:52+00:00" + "time": "2026-05-01T04:21:04+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -2867,153 +2962,60 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "7.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -3052,7 +3054,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" }, "funding": [ { @@ -3072,33 +3075,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-04-14T08:23:15+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3121,7 +3124,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -3129,33 +3133,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3187,7 +3191,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -3195,27 +3200,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "8.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -3223,7 +3228,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -3242,7 +3247,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -3250,42 +3255,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2026-04-15T12:13:01+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3327,7 +3345,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { @@ -3347,38 +3366,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.8", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -3397,13 +3413,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { @@ -3423,33 +3440,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3472,7 +3489,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -3480,34 +3498,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3529,7 +3547,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -3537,32 +3556,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3584,7 +3603,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -3592,32 +3612,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3647,7 +3667,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -3667,86 +3688,32 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3769,37 +3736,50 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3822,7 +3802,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -3830,7 +3811,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -3911,18 +3892,70 @@ ], "time": "2025-11-04T16:30:35+00:00" }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -3935,7 +3968,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3960,7 +3993,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -3971,32 +4004,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -4018,7 +4055,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -4026,27 +4063,27 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "webmozart/assert", - "version": "1.12.1", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "shasum": "" }, "require": { "ext-ctype": "*", "ext-date": "*", "ext-filter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.2" }, "suggest": { "ext-intl": "", @@ -4056,7 +4093,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -4072,6 +4109,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -4082,9 +4123,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.1" + "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, - "time": "2025-10-29T15:56:20+00:00" + "time": "2026-04-11T10:33:05+00:00" } ], "aliases": [], @@ -4093,7 +4134,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4 || ^8.0", + "php": "^8.2", + "ext-gd": "*", "ext-json": "*", "ext-pdo": "*" }, diff --git a/doc/.gitignore b/doc/.gitignore index d3ec28174..c85ac21e1 100755 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1,4 +1,4 @@ *.aux *.log *.synctex.gz - +openapi.json diff --git a/doc/README.md b/doc/README.md index 48cbf867f..05116b17b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,38 +1,18 @@ -# Documentation - -## Hashtopolis Protocol - -The current up-to-date protocol version which Hashtopolis uses to communicate with clients is contained in the `protocol.pdf` file. -The documentation for the User API can be found in `user-api/user-api.pdf`, listing all functions which can be called. - -## Generic Crackers - -Custom crackers which should be able to get distributed with Hashtopolis need to fulfill some minimal requirements as command line options. Shown here with the help function of a generic example implementation (which is available [here](https://github.com/hashtopolis/generic-cracker)): - -``` -cracker.exe [options] action -Generic Cracker compatible with Hashtopolis - -Options: - -m, --mask+ hashcat.bin --exam -m 3200 + + hashcat (v6.2.6-850-gfafb277e0+) starting in hash-info mode + Hash Info: + Hash mode #3200 + Name................: bcrypt $2*$, Blowfish (Unix) + Category............: Operating System + Slow.Hash...........: Yes + Password.Len.Min....: 0 + Password.Len.Max....: 72 + Salt.Type...........: Embedded + Salt.Len.Min........: 0 + Salt.Len.Max........: 256 + Kernel.Type(s)......: pure + Example.Hash.Format.: plain + Example.Hash........: $2a$05$MBCzKhG1KhezLh.0LRa0Kuw12nLJtpHy6DIaU.JAnqJUDYspHC.Ou + Example.Pass........: hashcat + Benchmark.Mask......: ?b?b?b?b?b?b?b + Autodetect.Enabled..: Yes + Self.Test.Enabled...: Yes + Potfile.Enabled.....: Yes + Custom.Plugin.......: No + Plaintext.Encoding..: ASCII, HEX ++ + +You cannot create a new hashtype using an existing hashtype value. In case you want to modify an existing hashtype, you must delete it and recreate it. This is unfortunately not possible if there are existing hashlists associated to this hashtype. + +## Health Checks + +Health checks offer an excellent opportunity to ensure that the cracker binary set up is working correctly. For this purpose, a test command is executed on the agent and it is checked whether everything is working properly. The result and a possible error code are then displayed in the frontend + +A new health check can be created by clicking on **New Health Check**. All you have to do is select the binary and the test can be started. The result is displayed transparently on the overview page. + +Additional information is displayed by clicking on the ID. Here you will then find the detailed test result and the possible error code, which can be used for debugging. + + +## Log + +Important events can be viewed in the log area. For example, failed logins are documented or document uploads are tracked: + +
- * require_once 'Auth/Yubico.php';
- * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
- *
- * # Generate a new id+key from https://api.yubico.com/get-api-key/
- * $yubi = new Auth_Yubico('42', 'FOOBAR=');
- * $auth = $yubi->verify($otp);
- * if (PEAR::isError($auth)) {
- * print "Authentication failed: " . $auth->getMessage();
- * print "
Debug output from server: " . $yubi->getLastResponse();
- * } else {
- * print "
You are authenticated!";
- * }
- *
- */
-class Auth_Yubico
-{
- /**#@+
- * @access private
- */
- /**
- * Yubico client ID
- * @var string
- */
- var $_id;
- /**
- * Yubico client key
- * @var string
- */
- var $_key;
- /**
- * URL part of validation server
- * @var string
- */
- var $_url;
- /**
- * List with URL part of validation servers
- * @var array
- */
- var $_url_list;
- /**
- * index to _url_list
- * @var int
- */
- var $_url_index;
- /**
- * Last query to server
- * @var string
- */
- var $_lastquery;
- /**
- * Response from server
- * @var string
- */
- var $_response;
- /**
- * Flag whether to use https or not.
- * @var boolean
- */
- var $_https;
- /**
- * Flag whether to verify HTTPS server certificates or not.
- * @var boolean
- */
- var $_httpsverify;
- /**
- * Constructor
- *
- * Sets up the object
- * @param string $id The client identity
- * @param string $key The client MAC key (optional)
- * @param boolean $https Flag whether to use https (optional)
- * @param boolean $httpsverify Flag whether to use verify HTTPS
- * server certificates (optional,
- * default true)
- * @access public
- */
- public function __construct($id, $key = '', $https = 0, $httpsverify = 1)
- {
- $this->_id = $id;
- $this->_key = base64_decode($key);
- $this->_https = $https;
- $this->_httpsverify = $httpsverify;
- }
- /**
- * Specify to use a different URL part for verification.
- * The default is "api.yubico.com/wsapi/verify".
- *
- * @param string $url New server URL part to use
- * @access public
- */
- function setURLpart($url)
- {
- $this->_url = $url;
- }
- /**
- * Get URL part to use for validation.
- *
- * @return string Server URL part
- * @access public
- */
- function getURLpart()
- {
- if ($this->_url) {
- return $this->_url;
- } else {
- return "api.yubico.com/wsapi/verify";
- }
- }
- /**
- * Get next URL part from list to use for validation.
- *
- * @return mixed string with URL part of false if no more URLs in list
- * @access public
- */
- function getNextURLpart()
- {
- if ($this->_url_list) $url_list=$this->_url_list;
- else $url_list=array('api.yubico.com/wsapi/2.0/verify',
- 'api2.yubico.com/wsapi/2.0/verify',
- 'api3.yubico.com/wsapi/2.0/verify',
- 'api4.yubico.com/wsapi/2.0/verify',
- 'api5.yubico.com/wsapi/2.0/verify');
-
- if ($this->_url_index>=count($url_list)) return false;
- else return $url_list[$this->_url_index++];
- }
- /**
- * Resets index to URL list
- *
- * @access public
- */
- function URLreset()
- {
- $this->_url_index=0;
- }
- /**
- * Add another URLpart.
- *
- * @access public
- */
- function addURLpart($URLpart)
- {
- $this->_url_list[]=$URLpart;
- }
-
- /**
- * Return the last query sent to the server, if any.
- *
- * @return string Request to server
- * @access public
- */
- function getLastQuery()
- {
- return $this->_lastquery;
- }
- /**
- * Return the last data received from the server, if any.
- *
- * @return string Output from server
- * @access public
- */
- function getLastResponse()
- {
- return $this->_response;
- }
- /**
- * Parse input string into password, yubikey prefix,
- * ciphertext, and OTP.
- *
- * @param string Input string to parse
- * @param string Optional delimiter re-class, default is '[:]'
- * @return array Keyed array with fields
- * @access public
- */
- function parsePasswordOTP($str, $delim = '[:]')
- {
- if (!preg_match("/^((.*)" . $delim . ")?" .
- "(([cbdefghijklnrtuv]{0,16})" .
- "([cbdefghijklnrtuv]{32}))$/i",
- $str, $matches)) {
- /* Dvorak? */
- if (!preg_match("/^((.*)" . $delim . ")?" .
- "(([jxe\.uidchtnbpygk]{0,16})" .
- "([jxe\.uidchtnbpygk]{32}))$/i",
- $str, $matches)) {
- return false;
- } else {
- $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
- }
- } else {
- $ret['otp'] = $matches[3];
- }
- $ret['password'] = $matches[2];
- $ret['prefix'] = $matches[4];
- $ret['ciphertext'] = $matches[5];
- return $ret;
- }
-
- /**
- * Parse parameters from last response
- *
- * example: getParameters("timestamp", "sessioncounter", "sessionuse");
- *
- * @param array @parameters Array with strings representing
- * parameters to parse
- * @return array parameter array from last response
- * @access public
- */
- function getParameters($parameters)
- {
- if ($parameters == null) {
- $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
- }
- $param_array = array();
- foreach ($parameters as $param) {
- if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
- return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
- }
- $param_array[$param]=$out[1];
- }
- return $param_array;
- }
- /**
- * Verify Yubico OTP against multiple URLs
- * Protocol specification 2.0 is used to construct validation requests
- *
- * @param string $token Yubico OTP
- * @param int $use_timestamp 1=>send request with ×tamp=1 to
- * get timestamp and session information
- * in the response
- * @param boolean $wait_for_all If true, wait until all
- * servers responds (for debugging)
- * @param string $sl Sync level in percentage between 0
- * and 100 or "fast" or "secure".
- * @param int $timeout Max number of seconds to wait
- * for responses
- * @return mixed PEAR error on error, true otherwise
- * @access public
- */
- function verify($token, $use_timestamp=null, $wait_for_all=False,
- $sl=null, $timeout=null)
- {
- $ans = "0";
- /* Construct parameters string */
- $ret = $this->parsePasswordOTP($token);
- if (!$ret) {
- return PEAR::raiseError('Could not parse Yubikey OTP');
- }
- $params = array('id'=>$this->_id,
- 'otp'=>$ret['otp'],
- 'nonce'=>md5(uniqid(rand())));
- /* Take care of protocol version 2 parameters */
- if ($use_timestamp) $params['timestamp'] = 1;
- if ($sl) $params['sl'] = $sl;
- if ($timeout) $params['timeout'] = $timeout;
- ksort($params);
- $parameters = '';
- foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
- $parameters = ltrim($parameters, "&");
-
- /* Generate signature. */
- if($this->_key <> "") {
- $signature = base64_encode(hash_hmac('sha1', $parameters,
- $this->_key, true));
- $signature = preg_replace('/\+/', '%2B', $signature);
- $parameters .= '&h=' . $signature;
- }
- /* Generate and prepare request. */
- $this->_lastquery=null;
- $this->URLreset();
- $mh = curl_multi_init();
- $ch = array();
- while($URLpart=$this->getNextURLpart())
- {
- /* Support https. */
- if ($this->_https) {
- $query = "https://";
- } else {
- $query = "http://";
- }
- $query .= $URLpart . "?" . $parameters;
- if ($this->_lastquery) { $this->_lastquery .= " "; }
- $this->_lastquery .= $query;
-
- $handle = curl_init($query);
- curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
- curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
- if (!$this->_httpsverify) {
- curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
- curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
- }
- curl_setopt($handle, CURLOPT_FAILONERROR, true);
- /* If timeout is set, we better apply it here as well
- in case the validation server fails to follow it.
- */
- if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
- curl_multi_add_handle($mh, $handle);
-
- $ch[(int)$handle] = $handle;
- }
- /* Execute and read request. */
- $this->_response=null;
- $replay=False;
- $valid=False;
- do {
- /* Let curl do its work. */
- while (($mrc = curl_multi_exec($mh, $active))
- == CURLM_CALL_MULTI_PERFORM)
- ;
- while ($info = curl_multi_info_read($mh)) {
- if ($info['result'] == CURLE_OK) {
- /* We have a complete response from one server. */
- $str = curl_multi_getcontent($info['handle']);
- $cinfo = curl_getinfo ($info['handle']);
-
- if ($wait_for_all) { # Better debug info
- $this->_response .= 'URL=' . $cinfo['url'] ."\n"
- . $str . "\n";
- }
- if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
- $status = $out[1];
- /*
- * There are 3 cases.
- *
- * 1. OTP or Nonce values doesn't match - ignore
- * response.
- *
- * 2. We have a HMAC key. If signature is invalid -
- * ignore response. Return if status=OK or
- * status=REPLAYED_OTP.
- *
- * 3. Return if status=OK or status=REPLAYED_OTP.
- */
- //if (!preg_match("/otp=".$params['otp']."/", $str) ||
- // !preg_match("/nonce=".$params['nonce']."/", $str)) {
- /* Case 1. Ignore response. */
- //$ans="11";
- //}
- if ($this->_key <> "") {
- /* Case 2. Verify signature first */
- $rows = explode("\r\n", trim($str));
- $response=array();
- while (list($key, $val) = each($rows)) {
- /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
- $val = preg_replace('/=/', '#', $val, 1);
- $row = explode("#", $val);
- $response[$row[0]] = $row[1];
- }
-
- $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
- sort($parameters);
- $check=Null;
- foreach ($parameters as $param) {
- if (array_key_exists($param, $response)) {
- if ($check) $check = $check . '&';
- $check = $check . $param . '=' . $response[$param];
- }
- }
- $checksignature =
- base64_encode(hash_hmac('sha1', utf8_encode($check),
- $this->_key, true));
- if($response['h'] == $checksignature) {
- if ($status == 'REPLAYED_OTP') {
- if (!$wait_for_all) { $this->_response = $str; }
- $replay=True;
- }
- if ($status == 'OK') {
- if (!$wait_for_all) { $this->_response = $str; }
- $valid=True;
- }
- }
- } else {
- /* Case 3. We check the status directly */
- if ($status == 'REPLAYED_OTP') {
- if (!$wait_for_all) { $this->_response = $str; }
- $replay=True;
- }
- if ($status == 'OK') {
- if (!$wait_for_all) { $this->_response = $str; }
- $valid=True;
- }
- }
- }
- if (!$wait_for_all && ($valid || $replay))
- {
- /* We have status=OK or status=REPLAYED_OTP, return. */
- foreach ($ch as $h) {
- curl_multi_remove_handle($mh, $h);
- curl_close($h);
- }
- curl_multi_close($mh);
- if ($replay) return PEAR::raiseError('REPLAYED_OTP');
- if ($valid) return true;
- return PEAR::raiseError($status);
- }
-
- curl_multi_remove_handle($mh, $info['handle']);
- curl_close($info['handle']);
- unset ($ch[(int)$info['handle']]);
- }
- curl_multi_select($mh);
- }
- } while ($active);
- /* Typically this is only reached for wait_for_all=true or
- * when the timeout is reached and there is no
- * OK/REPLAYED_REQUEST answer (think firewall).
- */
- foreach ($ch as $h) {
- curl_multi_remove_handle ($mh, $h);
- curl_close ($h);
- }
- curl_multi_close ($mh);
-
- if ($replay) return PEAR::raiseError('REPLAYED_OTP');
- if ($valid) return true;
- return PEAR::raiseError('NO_VALID_ANSWER');
- //return PEAR::raiseError($ans);
- }
-}
-?>
\ No newline at end of file
diff --git a/src/inc/Auth_Yubico.php b/src/inc/Auth_Yubico.php
new file mode 100644
index 000000000..82533819e
--- /dev/null
+++ b/src/inc/Auth_Yubico.php
@@ -0,0 +1,444 @@
+, Olov Danielson
+ * require_once 'Auth/Yubico.php';
+ * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
+ *
+ * # Generate a new id+key from https://api.yubico.com/get-api-key/
+ * $yubi = new Auth_Yubico('42', 'FOOBAR=');
+ * $auth = $yubi->verify($otp);
+ * if (PEAR::isError($auth)) {
+ * print "Authentication failed: " . $auth->getMessage();
+ * print "
Debug output from server: " . $yubi->getLastResponse();
+ * } else {
+ * print "
You are authenticated!";
+ * }
+ *
+ */
+class Auth_Yubico
+{
+ /**#@+
+ * @access private
+ */
+ /**
+ * Yubico client ID
+ * @var string
+ */
+ var $_id;
+ /**
+ * Yubico client key
+ * @var string
+ */
+ var $_key;
+ /**
+ * URL part of validation server
+ * @var string
+ */
+ var $_url;
+ /**
+ * List with URL part of validation servers
+ * @var array
+ */
+ var $_url_list;
+ /**
+ * index to _url_list
+ * @var int
+ */
+ var $_url_index;
+ /**
+ * Last query to server
+ * @var string
+ */
+ var $_lastquery;
+ /**
+ * Response from server
+ * @var string
+ */
+ var $_response;
+ /**
+ * Flag whether to use https or not.
+ * @var boolean
+ */
+ var $_https;
+ /**
+ * Flag whether to verify HTTPS server certificates or not.
+ * @var boolean
+ */
+ var $_httpsverify;
+ /**
+ * Constructor
+ *
+ * Sets up the object
+ * @param string $id The client identity
+ * @param string $key The client MAC key (optional)
+ * @param boolean $https Flag whether to use https (optional)
+ * @param boolean $httpsverify Flag whether to use verify HTTPS
+ * server certificates (optional,
+ * default true)
+ * @access public
+ */
+ public function __construct($id, $key = '', $https = 0, $httpsverify = 1)
+ {
+ $this->_id = $id;
+ $this->_key = base64_decode($key);
+ $this->_https = $https;
+ $this->_httpsverify = $httpsverify;
+ }
+ /**
+ * Specify to use a different URL part for verification.
+ * The default is "api.yubico.com/wsapi/verify".
+ *
+ * @param string $url New server URL part to use
+ * @access public
+ */
+ function setURLpart($url)
+ {
+ $this->_url = $url;
+ }
+ /**
+ * Get URL part to use for validation.
+ *
+ * @return string Server URL part
+ * @access public
+ */
+ function getURLpart()
+ {
+ if ($this->_url) {
+ return $this->_url;
+ } else {
+ return "api.yubico.com/wsapi/verify";
+ }
+ }
+ /**
+ * Get next URL part from list to use for validation.
+ *
+ * @return mixed string with URL part of false if no more URLs in list
+ * @access public
+ */
+ function getNextURLpart()
+ {
+ if ($this->_url_list) $url_list=$this->_url_list;
+ else $url_list=array('api.yubico.com/wsapi/2.0/verify',
+ 'api2.yubico.com/wsapi/2.0/verify',
+ 'api3.yubico.com/wsapi/2.0/verify',
+ 'api4.yubico.com/wsapi/2.0/verify',
+ 'api5.yubico.com/wsapi/2.0/verify');
+
+ if ($this->_url_index>=count($url_list)) return false;
+ else return $url_list[$this->_url_index++];
+ }
+ /**
+ * Resets index to URL list
+ *
+ * @access public
+ */
+ function URLreset()
+ {
+ $this->_url_index=0;
+ }
+ /**
+ * Add another URLpart.
+ *
+ * @access public
+ */
+ function addURLpart($URLpart)
+ {
+ $this->_url_list[]=$URLpart;
+ }
+
+ /**
+ * Return the last query sent to the server, if any.
+ *
+ * @return string Request to server
+ * @access public
+ */
+ function getLastQuery()
+ {
+ return $this->_lastquery;
+ }
+ /**
+ * Return the last data received from the server, if any.
+ *
+ * @return string Output from server
+ * @access public
+ */
+ function getLastResponse()
+ {
+ return $this->_response;
+ }
+ /**
+ * Parse input string into password, yubikey prefix,
+ * ciphertext, and OTP.
+ *
+ * @param string Input string to parse
+ * @param string Optional delimiter re-class, default is '[:]'
+ * @return array Keyed array with fields
+ * @access public
+ */
+ function parsePasswordOTP($str, $delim = '[:]')
+ {
+ if (!preg_match("/^((.*)" . $delim . ")?" .
+ "(([cbdefghijklnrtuv]{0,16})" .
+ "([cbdefghijklnrtuv]{32}))$/i",
+ $str, $matches)) {
+ /* Dvorak? */
+ if (!preg_match("/^((.*)" . $delim . ")?" .
+ "(([jxe\.uidchtnbpygk]{0,16})" .
+ "([jxe\.uidchtnbpygk]{32}))$/i",
+ $str, $matches)) {
+ return false;
+ } else {
+ $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
+ }
+ } else {
+ $ret['otp'] = $matches[3];
+ }
+ $ret['password'] = $matches[2];
+ $ret['prefix'] = $matches[4];
+ $ret['ciphertext'] = $matches[5];
+ return $ret;
+ }
+
+ /**
+ * Parse parameters from last response
+ *
+ * example: getParameters("timestamp", "sessioncounter", "sessionuse");
+ *
+ * @param array @parameters Array with strings representing
+ * parameters to parse
+ * @return array parameter array from last response
+ * @access public
+ */
+ function getParameters($parameters)
+ {
+ if ($parameters == null) {
+ $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
+ }
+ $param_array = array();
+ foreach ($parameters as $param) {
+ if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
+ return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
+ }
+ $param_array[$param]=$out[1];
+ }
+ return $param_array;
+ }
+ /**
+ * Verify Yubico OTP against multiple URLs
+ * Protocol specification 2.0 is used to construct validation requests
+ *
+ * @param string $token Yubico OTP
+ * @param int $use_timestamp 1=>send request with ×tamp=1 to
+ * get timestamp and session information
+ * in the response
+ * @param boolean $wait_for_all If true, wait until all
+ * servers responds (for debugging)
+ * @param string $sl Sync level in percentage between 0
+ * and 100 or "fast" or "secure".
+ * @param int $timeout Max number of seconds to wait
+ * for responses
+ * @return mixed PEAR error on error, true otherwise
+ * @access public
+ */
+ function verify($token, $use_timestamp=null, $wait_for_all=False,
+ $sl=null, $timeout=null)
+ {
+ $ans = "0";
+ /* Construct parameters string */
+ $ret = $this->parsePasswordOTP($token);
+ if (!$ret) {
+ return PEAR::raiseError('Could not parse Yubikey OTP');
+ }
+ $params = array('id'=>$this->_id,
+ 'otp'=>$ret['otp'],
+ 'nonce'=>md5(uniqid(rand())));
+ /* Take care of protocol version 2 parameters */
+ if ($use_timestamp) $params['timestamp'] = 1;
+ if ($sl) $params['sl'] = $sl;
+ if ($timeout) $params['timeout'] = $timeout;
+ ksort($params);
+ $parameters = '';
+ foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
+ $parameters = ltrim($parameters, "&");
+
+ /* Generate signature. */
+ if($this->_key <> "") {
+ $signature = base64_encode(hash_hmac('sha1', $parameters,
+ $this->_key, true));
+ $signature = preg_replace('/\+/', '%2B', $signature);
+ $parameters .= '&h=' . $signature;
+ }
+ /* Generate and prepare request. */
+ $this->_lastquery=null;
+ $this->URLreset();
+ $mh = curl_multi_init();
+ $ch = array();
+ while($URLpart=$this->getNextURLpart())
+ {
+ /* Support https. */
+ if ($this->_https) {
+ $query = "https://";
+ } else {
+ $query = "http://";
+ }
+ $query .= $URLpart . "?" . $parameters;
+ if ($this->_lastquery) { $this->_lastquery .= " "; }
+ $this->_lastquery .= $query;
+
+ $handle = curl_init($query);
+ curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
+ curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
+ if (!$this->_httpsverify) {
+ curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
+ curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
+ }
+ curl_setopt($handle, CURLOPT_FAILONERROR, true);
+ /* If timeout is set, we better apply it here as well
+ in case the validation server fails to follow it.
+ */
+ if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
+ curl_multi_add_handle($mh, $handle);
+
+ $ch[(int)$handle] = $handle;
+ }
+ /* Execute and read request. */
+ $this->_response=null;
+ $replay=False;
+ $valid=False;
+ do {
+ /* Let curl do its work. */
+ while (($mrc = curl_multi_exec($mh, $active))
+ == CURLM_CALL_MULTI_PERFORM)
+ ;
+ while ($info = curl_multi_info_read($mh)) {
+ if ($info['result'] == CURLE_OK) {
+ /* We have a complete response from one server. */
+ $str = curl_multi_getcontent($info['handle']);
+ $cinfo = curl_getinfo ($info['handle']);
+
+ if ($wait_for_all) { # Better debug info
+ $this->_response .= 'URL=' . $cinfo['url'] ."\n"
+ . $str . "\n";
+ }
+ if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
+ $status = $out[1];
+ /*
+ * There are 3 cases.
+ *
+ * 1. OTP or Nonce values doesn't match - ignore
+ * response.
+ *
+ * 2. We have a HMAC key. If signature is invalid -
+ * ignore response. Return if status=OK or
+ * status=REPLAYED_OTP.
+ *
+ * 3. Return if status=OK or status=REPLAYED_OTP.
+ */
+ //if (!preg_match("/otp=".$params['otp']."/", $str) ||
+ // !preg_match("/nonce=".$params['nonce']."/", $str)) {
+ /* Case 1. Ignore response. */
+ //$ans="11";
+ //}
+ if ($this->_key <> "") {
+ /* Case 2. Verify signature first */
+ $rows = explode("\r\n", trim($str));
+ $response=array();
+ while (list($key, $val) = each($rows)) {
+ /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
+ $val = preg_replace('/=/', '#', $val, 1);
+ $row = explode("#", $val);
+ $response[$row[0]] = $row[1];
+ }
+
+ $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
+ sort($parameters);
+ $check=Null;
+ foreach ($parameters as $param) {
+ if (array_key_exists($param, $response)) {
+ if ($check) $check = $check . '&';
+ $check = $check . $param . '=' . $response[$param];
+ }
+ }
+ $checksignature =
+ base64_encode(hash_hmac('sha1', utf8_encode($check),
+ $this->_key, true));
+ if($response['h'] == $checksignature) {
+ if ($status == 'REPLAYED_OTP') {
+ if (!$wait_for_all) { $this->_response = $str; }
+ $replay=True;
+ }
+ if ($status == 'OK') {
+ if (!$wait_for_all) { $this->_response = $str; }
+ $valid=True;
+ }
+ }
+ } else {
+ /* Case 3. We check the status directly */
+ if ($status == 'REPLAYED_OTP') {
+ if (!$wait_for_all) { $this->_response = $str; }
+ $replay=True;
+ }
+ if ($status == 'OK') {
+ if (!$wait_for_all) { $this->_response = $str; }
+ $valid=True;
+ }
+ }
+ }
+ if (!$wait_for_all && ($valid || $replay))
+ {
+ /* We have status=OK or status=REPLAYED_OTP, return. */
+ foreach ($ch as $h) {
+ curl_multi_remove_handle($mh, $h);
+ curl_close($h);
+ }
+ curl_multi_close($mh);
+ if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+ if ($valid) return true;
+ return PEAR::raiseError($status);
+ }
+
+ curl_multi_remove_handle($mh, $info['handle']);
+ curl_close($info['handle']);
+ unset ($ch[(int)$info['handle']]);
+ }
+ curl_multi_select($mh);
+ }
+ } while ($active);
+ /* Typically this is only reached for wait_for_all=true or
+ * when the timeout is reached and there is no
+ * OK/REPLAYED_REQUEST answer (think firewall).
+ */
+ foreach ($ch as $h) {
+ curl_multi_remove_handle ($mh, $h);
+ curl_close ($h);
+ }
+ curl_multi_close ($mh);
+
+ if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+ if ($valid) return true;
+ return PEAR::raiseError('NO_VALID_ANSWER');
+ //return PEAR::raiseError($ans);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/inc/CSRF.class.php b/src/inc/CSRF.class.php
deleted file mode 100644
index b980f0c83..000000000
--- a/src/inc/CSRF.class.php
+++ /dev/null
@@ -1,41 +0,0 @@
-getPepper(3)])) {
+ // generate a secret
+ $_SESSION[StartupConfig::getInstance()->getPepper(3)] = Util::randomString(40);
+ }
+
+ // set a token
+ $key = Util::randomString(30);
+ UI::add('csrf', $key . ":" . base64_encode(hash("sha256", $key . $_SESSION[StartupConfig::getInstance()->getPepper(3)] . $key, true)));
+ }
+
+ public static function check($csrf) {
+ $csrf = explode(":", $csrf);
+ if (sizeof($csrf) != 2) {
+ UI::addMessage(UI::ERROR, "Invalid form submission!");
+ return false;
+ }
+ else if (!isset($_SESSION[StartupConfig::getInstance()->getPepper(3)])) {
+ UI::addMessage(UI::ERROR, "Invalid form submission!");
+ return false;
+ }
+
+ $key = $csrf[0];
+ $check = base64_encode(hash("sha256", $key . $_SESSION[StartupConfig::getInstance()->getPepper(3)] . $key, true));
+ if ($check == $csrf[1]) {
+ return true;
+ }
+ UI::addMessage(UI::ERROR, "Invalid form submission!");
+ return false;
+ }
+}
+
+
+
diff --git a/src/inc/DataSet.php b/src/inc/DataSet.php
new file mode 100755
index 000000000..7ab6db0ae
--- /dev/null
+++ b/src/inc/DataSet.php
@@ -0,0 +1,38 @@
+values = $arr;
+ }
+
+ public function setValues($arr) {
+ $this->values = $arr;
+ }
+
+ public function addValue($key, $val) {
+ $this->values[$key] = $val;
+ }
+
+ public function getVal($key) {
+ if (isset($this->values[$key])) {
+ return $this->values[$key];
+ }
+ return false;
+ }
+
+ public function getKeys() {
+ $keys = [];
+ foreach ($this->values as $key => $val) {
+ $keys[] = $key;
+ }
+ return $keys;
+ }
+
+ public function getAllValues() {
+ return $this->values;
+ }
+}
\ No newline at end of file
diff --git a/src/inc/Dataset.class.php b/src/inc/Dataset.class.php
deleted file mode 100755
index a788bc274..000000000
--- a/src/inc/Dataset.class.php
+++ /dev/null
@@ -1,36 +0,0 @@
-values = $arr;
- }
-
- public function setValues($arr) {
- $this->values = $arr;
- }
-
- public function addValue($key, $val) {
- $this->values[$key] = $val;
- }
-
- public function getVal($key) {
- if (isset($this->values[$key])) {
- return $this->values[$key];
- }
- return false;
- }
-
- public function getKeys() {
- $keys = [];
- foreach ($this->values as $key => $val) {
- $keys[] = $key;
- }
- return $keys;
- }
-
- public function getAllValues() {
- return $this->values;
- }
-}
\ No newline at end of file
diff --git a/src/inc/Encryption.class.php b/src/inc/Encryption.class.php
deleted file mode 100755
index ba931d7f2..000000000
--- a/src/inc/Encryption.class.php
+++ /dev/null
@@ -1,125 +0,0 @@
- 12);
- $CIPHER = password_hash($CIPHER, PASSWORD_BCRYPT, $options);
- return $CIPHER;
- }
-
- public static function passwordVerify($password, $salt, $hash) {
- global $PEPPER;
-
- $CIPHER = $PEPPER[1] . $password . $salt;
- if (!password_verify($CIPHER, $hash)) {
- return false;
- }
- return true;
- }
-
- /**
- * Get the number of cycles for a given string
- *
- * @param string $string
- * @param int $mincycles
- * @param int $maxcycles
- * @return int num cycles
- */
- private static function getCount($string, $mincycles = 3000, $maxcycles = 5000) {
- $count = 0;
- for ($x = 0; $x < strlen($string); $x++) {
- $count += $x * ord($string[$x]) * pow($x, 15);
- $count = $count % 10000;
- }
- return $count % $maxcycles + $mincycles;
- }
-
- /**
- * Generates a hash for the validation of a user email
- *
- * @param int $id userID to validate
- * @param string $username username to validate
- * @return string base64 encoded hash
- */
- public static function validationHash($id, $username) {
- global $PEPPER;
-
- $KEY = pack('H*', hash("sha256", $id));
- $cycles = Encryption::getCount($username . $PEPPER[2], 500, 1000);
- $CIPHER = $id . $username;
- $CIPHER = openssl_encrypt($CIPHER, 'blowfish', $KEY, 0, substr($PEPPER[2], 0, 8));
- for ($x = 0; $x < $cycles; $x++) {
- $KEY = pack('H*', hash("sha256", $CIPHER . $id . $PEPPER[2] . $username . $KEY));
- }
- return Util::strToHex($KEY);
- }
-}
-
-
-
diff --git a/src/inc/Encryption.php b/src/inc/Encryption.php
new file mode 100755
index 000000000..d7f40698d
--- /dev/null
+++ b/src/inc/Encryption.php
@@ -0,0 +1,122 @@
+getPepper(0) . $KEY));
+ }
+ return Util::strToHex($KEY);
+ }
+
+ /**
+ * Detect if a given passwords is complex enough to be accepted as password.
+ *
+ * @param string $string password to check
+ * @return boolean true if password is complex enough, false if not
+ */
+ public static function validPassword(string $string): bool {
+ if (strlen($string) < 8) {
+ return false;
+ }
+ $number = false;
+ $special = false;
+ $upper = false;
+ $lower = false;
+ for ($x = 0; $x < strlen($string); $x++) {
+ if (ctype_upper($string[$x])) {
+ $upper = true;
+ }
+ else if (ctype_lower($string[$x])) {
+ $lower = true;
+ }
+ else if (ctype_digit($string[$x])) {
+ $number = true;
+ }
+ else {
+ $special = true;
+ }
+ }
+ return ($number && $special && $upper && $lower);
+ }
+
+ /**
+ * Generates a password hash out of the given parameters.
+ *
+ * @param string $password plain password
+ * @param string $salt salt which belongs to the password
+ * @return string hash
+ */
+ public static function passwordHash(string $password, string $salt): string {
+ $CIPHER = StartupConfig::getInstance()->getPepper(1) . $password . $salt;
+ $options = array('cost' => 12);
+ return password_hash($CIPHER, PASSWORD_BCRYPT, $options);
+ }
+
+ /**
+ * @param string $password
+ * @param string $salt
+ * @param string $hash
+ * @return bool
+ */
+ public static function passwordVerify(string $password, string $salt, string $hash): bool {
+ $CIPHER = StartupConfig::getInstance()->getPepper(1) . $password . $salt;
+ if (!password_verify($CIPHER, $hash)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the number of cycles for a given string
+ *
+ * @param string $string
+ * @param int $minCycles
+ * @param int $maxCycles
+ * @return int num cycles
+ */
+ private static function getCount(string $string, int $minCycles = 3000, int $maxCycles = 5000): int {
+ $count = 0;
+ for ($x = 0; $x < strlen($string); $x++) {
+ $count += $x * ord($string[$x]) * bcpowmod($x, 15, 10000);
+ $count = $count % 10000;
+ }
+ return $count % $maxCycles + $minCycles;
+ }
+
+ /**
+ * Generates a hash for the validation of a user email
+ *
+ * @param int $id userID to validate
+ * @param string $username username to validate
+ * @return string hex encoded hash
+ */
+ public static function validationHash(int $id, string $username): string {
+ $KEY = pack('H*', hash("sha256", $id));
+ $cycles = Encryption::getCount($username . StartupConfig::getInstance()->getPepper(2), 500, 1000);
+ $CIPHER = $id . $username;
+ for ($x = 0; $x < $cycles; $x++) {
+ $KEY = pack('H*', hash("sha256", $CIPHER . $id . StartupConfig::getInstance()->getPepper(2) . $username . $KEY));
+ }
+ return Util::strToHex($KEY);
+ }
+}
+
+
+
diff --git a/src/inc/HTException.class.php b/src/inc/HTException.class.php
deleted file mode 100644
index 825d4cce9..000000000
--- a/src/inc/HTException.class.php
+++ /dev/null
@@ -1,5 +0,0 @@
-arr = $message;
- $this->message = implode("\n", $this->arr);
- }
-
- public function getHTMLMessage() {
- return implode("| Type | -+ | |||||||
|---|---|---|---|---|---|---|---|---|
| Operating Systems | @@ -108,7 +108,7 @@||||||||
| [[binary.getId()]] | -[[binary.getType()]] | +[[binary.getBinaryType()]] | [[binary.getOperatingSystems()]] | [[binary.getFilename()]] |
[[binary.getVersion()]] | diff --git a/src/templates/cracks.template.html b/src/templates/cracks.template.html index 889ca178c..269661fae 100644 --- a/src/templates/cracks.template.html +++ b/src/templates/cracks.template.html @@ -32,10 +32,10 @@- [[crackDetailsPrimary.getVal([[crack.getId()]]).getPlaintext()]] + [[htmlentities([[crackDetailsPrimary.getVal([[crack.getId()]]).getPlaintext()]], ENT_QUOTES, "UTF-8")]] | - [[crackDetailsPrimary.getVal([[crack.getId()]]).getHash()]] + [[htmlentities([[crackDetailsPrimary.getVal([[crack.getId()]]).getHash()]], ENT_QUOTES, "UTF-8")]] | {{IF [[accessControl.hasPermission([[$DAccessControl::VIEW_HASHLIST_ACCESS]])]]}} diff --git a/src/users.php b/src/users.php index 6fb4179b8..f31054ece 100755 --- a/src/users.php +++ b/src/users.php @@ -1,12 +1,22 @@ isLoggedin()) { header("Location: index.php?err=4" . time() . "&fw=" . urlencode($_SERVER['PHP_SELF'] . "?" . $_SERVER['QUERY_STRING'])); diff --git a/ssmtp.conf.example b/ssmtp.conf.example new file mode 100644 index 000000000..57c67666f --- /dev/null +++ b/ssmtp.conf.example @@ -0,0 +1,20 @@ +# The user that gets all the mails (UID < 1000, usually the admin) +root=username@domain.com + +# The mail server (where the mail is sent to) +mailhub=smtp.domain.com:465 + +# The address where the mail appears to come from for user authentication. +rewriteDomain=domain.com + +# Use implicit TLS (port 465). When using port 587, change UseSTARTTLS=Yes +UseTLS=Yes +UseSTARTTLS=No + +# Username/Password +AuthUser=username +AuthPass=password +AuthMethod=PLAIN + +# Email 'From header's can override the default domain? +FromLineOverride=yes |