diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c41ce38 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Julian Finkler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9dd6a3 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Native Cron + +This package helps you to manage and modify your native *nix crontabs. + +## Features + +- View / add / edit / delete system cron jobs +- View / add / edit / delete drop in cron jobs +- View / add / edit / delete user cron jobs +- Fully tested (except the adapter for native file system functions) +- Flexible / you can easily replace any important part of the package + +## Installation +```bash +composer require mintware-de/native-cron +``` + +## Usage +```php +readUserCrontab('max'); + +// Display the current content of the crontab +echo $crontab->build(); + +$crontab + // Add a new cronjob + ->add(new CronJobLine('* * * * * echo "Hello World" >> /tmp/mylog')) + // Remove all comments and blank lines + ->removeWhere(fn ($line) => $line instanceof CommentLine || $line instanceof BlankLine); + +// Display the new content of the crontab +echo $crontab->build(); + +// Write the crontab for the user max +// Keep in mind that reading or writing crontab files may require higher user privileges. +$manager->writeUserCrontab($crontab, 'max'); +``` + +## Compatibility matrix + +| Feature | Linux | macOS | Win | +|-------------------|:-----:|:-----:|-----| +| System Cron jobs | Yes | Yes | No | +| User Cron jobs | Yes | Yes | No | +| Drop-In Cron jobs | Yes | No | No | + + +## Supported platforms +At the moment are Debian based distros and macOS supported. +If you need to add support for a different platform, take a look at the [CrontabFileLocatorInterface](./src/Filesystem/CrontabFileLocatorInterface.php) and implement it for your platform. + diff --git a/src/Crontab.php b/src/Content/Crontab.php similarity index 91% rename from src/Crontab.php rename to src/Content/Crontab.php index b8b7e84..c8be696 100644 --- a/src/Crontab.php +++ b/src/Content/Crontab.php @@ -2,13 +2,7 @@ declare(strict_types=1); -namespace MintwareDe\NativeCron; - -use MintwareDe\NativeCron\Content\BlankLine; -use MintwareDe\NativeCron\Content\CommentLine; -use MintwareDe\NativeCron\Content\CronJobLine; -use MintwareDe\NativeCron\Content\CrontabLineInterface; -use MintwareDe\NativeCron\Content\EnvironmentSetting; +namespace MintwareDe\NativeCron\Content; class Crontab { diff --git a/src/CrontabManager.php b/src/CrontabManager.php new file mode 100644 index 0000000..490afea --- /dev/null +++ b/src/CrontabManager.php @@ -0,0 +1,114 @@ +fileLocator->locateSystemCrontab(); + + return $this->readCrontabInternal($crontabFile, true); + } + + /** + * Reads the crontab file for a user. + * + * @param string $username The username of the user + * + * @return Crontab + */ + public function readUserCrontab(string $username): Crontab + { + $crontabFile = $this->fileLocator->locateUserCrontab($username); + + return $this->readCrontabInternal($crontabFile, false); + } + + /** + * Reads a drop-in crontab file. + * + * @param string $name The name of the drop-in + * + * @return Crontab + */ + public function readDropInCrontab(string $name): Crontab + { + $crontabFile = $this->fileLocator->locateDropInCrontab($name); + + return $this->readCrontabInternal($crontabFile, true); + } + + private function readCrontabInternal(string $crontabFile, bool $isSystemCrontab): Crontab + { + $crontabContent = $this->fileHandler->read($crontabFile); + + $crontab = new Crontab($isSystemCrontab); + if ($crontabContent !== null) { + $crontab->parse($crontabContent); + } + + return $crontab; + } + + public function writeSystemCrontab(Crontab $crontab): void + { + $crontabFile = $this->fileLocator->locateSystemCrontab(); + + $this->writeCrontabFileInternal($crontabFile, $crontab, 'root', true); + } + + public function writeDropInCrontab(Crontab $crontab, string $name): void + { + $crontabFile = $this->fileLocator->locateDropInCrontab($name); + + $this->writeCrontabFileInternal($crontabFile, $crontab, 'root', true); + } + + public function writeUserCrontab(Crontab $crontab, string $username): void + { + $crontabFile = $this->fileLocator->locateUserCrontab($username); + + $this->writeCrontabFileInternal($crontabFile, $crontab, $username, false); + } + + private function writeCrontabFileInternal( + string $crontabFile, + Crontab $crontab, + string $owner, + bool $isSystemFile + ): void { + $isSystemCrontab = $crontab->isSystemCrontab(); + if ($isSystemCrontab != $isSystemFile) { + if ($isSystemCrontab) { + throw new \RuntimeException('The crontab is a system crontab.'); + } else { + throw new \RuntimeException('The crontab is not a system crontab.'); + } + } + + $this->fileHandler->createFile($crontabFile); + $this->fileHandler->setPermissions($crontabFile, 0600); + $this->fileHandler->setOwner($crontabFile, $owner); + + $crontabContent = $crontab->build(); + $this->fileHandler->write($crontabFile, $crontabContent); + } +} diff --git a/src/Filesystem/DarwinCrontabFileLocator.php b/src/Filesystem/DarwinCrontabFileLocator.php index 77ed5d6..69f1702 100644 --- a/src/Filesystem/DarwinCrontabFileLocator.php +++ b/src/Filesystem/DarwinCrontabFileLocator.php @@ -6,7 +6,6 @@ class DarwinCrontabFileLocator implements CrontabFileLocatorInterface { - public function locateDropInCrontab(string $name): string { throw new \RuntimeException('This platform does not support drop-in cron tabs'); diff --git a/src/Filesystem/FileHandler.php b/src/Filesystem/FileHandler.php new file mode 100644 index 0000000..45ed561 --- /dev/null +++ b/src/Filesystem/FileHandler.php @@ -0,0 +1,58 @@ +mockFileLocator = self::createMock(CrontabFileLocatorInterface::class); + $this->mockFileHandler = self::createMock(FileHandlerInterface::class); + $this->manager = new CrontabManager($this->mockFileLocator, $this->mockFileHandler); + } + + public function testReadNonExistentSystemCrontab(): void + { + $this->setupReadSystemCrontab(null); + + $crontab = $this->manager->readSystemCrontab(); + $this->verifyReadEmptyCrontab($crontab, true); + } + + public function testReadSystemCrontab(): void + { + $this->setupReadSystemCrontab("#test system crontab\n\n17 * * * * root my-command\n"); + + $crontab = $this->manager->readSystemCrontab(); + $this->verifyCrontab($crontab, true); + } + + public function testReadNonExistentDropInCrontab(): void + { + $this->setupReadDropInCrontab(null); + + $crontab = $this->manager->readDropInCrontab('app'); + $this->verifyReadEmptyCrontab($crontab, true); + } + + public function testReadDropInCrontab(): void + { + $this->setupReadDropInCrontab("#test drop-in crontab\n\n17 * * * * root my-command\n"); + + $crontab = $this->manager->readDropInCrontab('app'); + $this->verifyCrontab($crontab, true); + } + + public function testReadNonExistentUserCrontab(): void + { + $this->setupReadUserCrontab('admin', null); + $crontab = $this->manager->readUserCrontab('admin'); + $this->verifyReadEmptyCrontab($crontab, false); + } + + public function testReadUserCrontab(): void + { + $this->setupReadUserCrontab('admin', "#test system crontab\n\n17 * * * * my-command\n"); + $crontab = $this->manager->readUserCrontab('admin'); + $this->verifyCrontab($crontab, false); + } + + public function testWriteSystemCrontabFails(): void + { + $mockCrontab = self::createMock(Crontab::class); + + $mockCrontab->expects(self::once()) + ->method('isSystemCrontab') + ->willReturn(false); + + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('The crontab is not a system crontab.'); + + $this->manager->writeSystemCrontab($mockCrontab); + } + + public function testWriteSystemCrontabPass(): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateSystemCrontab') + ->willReturn('crontab-file'); + + $mockCrontab = self::createMock(Crontab::class); + + $this->setupWriteCrontab($mockCrontab, true, 'root'); + + $this->manager->writeSystemCrontab($mockCrontab); + } + + public function testWriteDropInCrontabFails(): void + { + $mockCrontab = self::createMock(Crontab::class); + + $mockCrontab->expects(self::once()) + ->method('isSystemCrontab') + ->willReturn(false); + + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('The crontab is not a system crontab.'); + + $this->manager->writeDropInCrontab($mockCrontab, 'app'); + } + + public function testWriteDropInCrontabPass(): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateDropInCrontab') + ->with('app') + ->willReturn('crontab-file'); + + $mockCrontab = self::createMock(Crontab::class); + + $this->setupWriteCrontab($mockCrontab, true, 'root'); + + $this->manager->writeDropInCrontab($mockCrontab, 'app'); + } + + public function testWriteUserCrontabFails(): void + { + $mockCrontab = self::createMock(Crontab::class); + + $mockCrontab->expects(self::once()) + ->method('isSystemCrontab') + ->willReturn(true); + + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('The crontab is a system crontab.'); + + $this->manager->writeUserCrontab($mockCrontab, 'app'); + } + + public function testWriteUserCrontabPass(): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateUserCrontab') + ->with('foo') + ->willReturn('crontab-file'); + + $mockCrontab = self::createMock(Crontab::class); + + $this->setupWriteCrontab($mockCrontab, false, 'foo'); + + $this->manager->writeUserCrontab($mockCrontab, 'foo'); + } + + private function verifyReadEmptyCrontab(Crontab $crontab, bool $isSystemCrontab): void + { + self::assertInstanceOf(Crontab::class, $crontab); + self::assertEquals($isSystemCrontab, $crontab->isSystemCrontab()); + self::assertCount(0, $crontab->getLines()); + } + + private function verifyCrontab(Crontab $crontab, bool $isSystemCrontab): void + { + self::assertInstanceOf(Crontab::class, $crontab); + self::assertEquals($isSystemCrontab, $crontab->isSystemCrontab()); + self::assertCount(4, $crontab->getLines()); + + self::assertInstanceOf(CommentLine::class, $crontab->getLines()[0]); + self::assertInstanceOf(BlankLine::class, $crontab->getLines()[1]); + self::assertInstanceOf(CronJobLine::class, $crontab->getLines()[2]); + self::assertInstanceOf(BlankLine::class, $crontab->getLines()[3]); + } + + private function setupReadSystemCrontab(?string $content): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateSystemCrontab') + ->willReturn('crontab-file'); + + $this->mockFileHandler + ->expects(self::once()) + ->method('read') + ->with('crontab-file') + ->willReturn($content); + } + + private function setupReadDropInCrontab(?string $content): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateDropInCrontab') + ->willReturn('crontab-file'); + + $this->mockFileHandler + ->expects(self::once()) + ->method('read') + ->with('crontab-file') + ->willReturn($content); + } + + private function setupReadUserCrontab(string $username, ?string $content): void + { + $this->mockFileLocator + ->expects(self::once()) + ->method('locateUserCrontab') + ->with($username) + ->willReturn('crontab-file'); + + $this->mockFileHandler + ->expects(self::once()) + ->method('read') + ->with('crontab-file') + ->willReturn($content); + } + + private function setupWriteCrontab(Crontab&MockObject $mockCrontab, bool $isSystemCrontab, string $owner): void + { + $mockCrontab->expects(self::once()) + ->method('isSystemCrontab') + ->willReturn($isSystemCrontab); + + $mockCrontab->expects(self::once()) + ->method('build') + ->willReturn('crontab-content'); + + $this->mockFileHandler + ->expects(self::once()) + ->method('createFile') + ->with('crontab-file'); + + $this->mockFileHandler + ->expects(self::once()) + ->method('setPermissions') + ->with('crontab-file', 0600); + + $this->mockFileHandler + ->expects(self::once()) + ->method('setOwner') + ->with('crontab-file', $owner); + + $this->mockFileHandler + ->expects(self::once()) + ->method('write') + ->with('crontab-file', 'crontab-content'); + } +} diff --git a/tests/FileSystem/DarwinCrontabFileLocatorTest.php b/tests/FileSystem/DarwinCrontabFileLocatorTest.php index 1904bc6..f8ca8e3 100644 --- a/tests/FileSystem/DarwinCrontabFileLocatorTest.php +++ b/tests/FileSystem/DarwinCrontabFileLocatorTest.php @@ -10,7 +10,6 @@ class DarwinCrontabFileLocatorTest extends TestCase { - private DarwinCrontabFileLocator $locator; protected function setUp(): void