From 8d3f1c0b1c4b1f478f909b58f3919108839aa532 Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 5 Nov 2025 17:07:02 +0100 Subject: [PATCH 1/6] Implement remaining minimal requirements for supporting ipp 2.0 spec BC break: removed PrinterStateReasonEnum as it is a csv of client defined reasons instead of a single known enum --- README.md | 6 + composer.json | 10 +- phpcs.baseline.xml | 6 + src/Client/CupsIppHttpClient.php | 5 +- src/Entity/IppJob.php | 92 +- src/Entity/IppPrinter.php | 228 ++++- src/Enum/AuthenticationSupportEnum.php | 17 + src/Enum/PrintQualityEnum.php | 12 + src/Enum/PrinterStateReasonEnum.php | 833 ------------------- src/Enum/SecuritySupportEnum.php | 14 + src/Factory/IppJobFactory.php | 13 + src/Factory/IppPrinterFactory.php | 72 +- src/Ipp.php | 10 + src/Operations/PrintOperation.php | 4 +- src/Protocol/Response/IppResponseState.php | 6 +- tests/Unit/Factory/IppPrinterFactoryTest.php | 5 +- 16 files changed, 469 insertions(+), 864 deletions(-) create mode 100644 phpcs.baseline.xml create mode 100644 src/Enum/AuthenticationSupportEnum.php create mode 100644 src/Enum/PrintQualityEnum.php delete mode 100644 src/Enum/PrinterStateReasonEnum.php create mode 100644 src/Enum/SecuritySupportEnum.php diff --git a/README.md b/README.md index ac42d2d..7ea5f56 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ composer require digitalrevolution/ipp $ipp->print($printer, $ippFile); ``` +### Print job validation +If you want to test if a print operation should work, without actually printing anything, you can use the validatePrintJob operation instead. +```php + $ipp->validatePrintJob($printer, $ippFile); +``` + ### Fetch job attributes ```php diff --git a/composer.json b/composer.json index ad3b172..31bb970 100644 --- a/composer.json +++ b/composer.json @@ -16,23 +16,23 @@ }, "require": { "php": "^8.3", + "digitalrevolution/utils": "^1.0", "nyholm/psr7": "^1.8", "psr/http-client": "^1.0", - "psr/log": "^3.0", - "digitalrevolution/utils": "^1.0" + "psr/log": "^3.0" }, "require-dev": { "digitalrevolution/accessorpair-constraint": "^v2.4.1", + "digitalrevolution/php-codesniffer-baseline": "^1.3", "digitalrevolution/phpunit-file-coverage-inspection": "^3.0", - "digitalrevolution/php-codesniffer-baseline": "^1.1", "phpmd/phpmd": "^2.12", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^2.0.0", "phpstan/phpstan-phpunit": "^2.0.0", "phpunit/phpunit": "^12.0", "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^4.0", - "slevomat/coding-standard": "^8.16" + "slevomat/coding-standard": "^8.16", + "squizlabs/php_codesniffer": "^4.0" }, "scripts": { "baseline": [ diff --git a/phpcs.baseline.xml b/phpcs.baseline.xml new file mode 100644 index 0000000..f18701d --- /dev/null +++ b/phpcs.baseline.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Client/CupsIppHttpClient.php b/src/Client/CupsIppHttpClient.php index 50186a0..599b7d8 100644 --- a/src/Client/CupsIppHttpClient.php +++ b/src/Client/CupsIppHttpClient.php @@ -14,8 +14,7 @@ class CupsIppHttpClient implements IppHttpClientInterface { - private const int HTTP_STATUS_UNAUTHORIZED = 401; - private const int HTTP_STATUS_SERVER_ERROR = 500; + private const int HTTP_STATUS_BAD_REQUEST = 400; public function __construct(private readonly IppServer $server, private readonly ClientInterface $client) { @@ -42,7 +41,7 @@ public function sendRequest(IppOperation $operation): ResponseInterface $response = $this->client->sendRequest($request); // a server error won't be an ipp response we can parse. - if ($response->getStatusCode() === self::HTTP_STATUS_UNAUTHORIZED || $response->getStatusCode() >= self::HTTP_STATUS_SERVER_ERROR) { + if ($response->getStatusCode() >= self::HTTP_STATUS_BAD_REQUEST) { throw new IppRequestException($request, $response); } diff --git a/src/Entity/IppJob.php b/src/Entity/IppJob.php index 07c71fd..83b490c 100644 --- a/src/Entity/IppJob.php +++ b/src/Entity/IppJob.php @@ -12,15 +12,21 @@ class IppJob { private int $id; private string $uri; + private ?string $name = null; private JobStateEnum $jobState; private JobStateReasonEnum $jobStateReason; - private ?string $userName; - private ?int $fileSize; - private ?int $numberOfDocuments; - private ?int $copies; - private ?DateTimeInterface $creationDate; - private ?DateTimeInterface $completionDate; - private ?string $documentFormat; + private ?string $userName = null; + private ?int $fileSize = null; + private ?int $numberOfDocuments = null; + private ?int $copies = null; + private ?DateTimeInterface $creationDate = null; + private ?DateTimeInterface $processingDate = null; + private ?DateTimeInterface $completionDate = null; + private ?string $documentFormat = null; + private ?string $documentName = null; + private ?string $printerUri = null; + private ?DateTimeInterface $printerUpSince = null; + private ?int $priority = null; public function getId(): int { @@ -46,6 +52,18 @@ public function setUri(string $uri): self return $this; } + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + public function getJobStateReason(): JobStateReasonEnum { return $this->jobStateReason; @@ -130,6 +148,18 @@ public function setCreationDate(?DateTimeInterface $creationDate): self return $this; } + public function getProcessingDate(): ?DateTimeInterface + { + return $this->processingDate; + } + + public function setProcessingDate(?DateTimeInterface $processingDate): self + { + $this->processingDate = $processingDate; + + return $this; + } + public function getCompletionDate(): ?DateTimeInterface { return $this->completionDate; @@ -153,4 +183,52 @@ public function setDocumentFormat(?string $documentFormat): self return $this; } + + public function getDocumentName(): ?string + { + return $this->documentName; + } + + public function setDocumentName(?string $documentName): self + { + $this->documentName = $documentName; + + return $this; + } + + public function getPrinterUpSince(): ?DateTimeInterface + { + return $this->printerUpSince; + } + + public function setPrinterUpSince(?DateTimeInterface $printerUpTime): self + { + $this->printerUpSince = $printerUpTime; + + return $this; + } + + public function getPrinterUri(): ?string + { + return $this->printerUri; + } + + public function setPrinterUri(?string $setPrinterUri): self + { + $this->printerUri = $setPrinterUri; + + return $this; + } + + public function getPriority(): ?int + { + return $this->priority; + } + + public function setPriority(?int $priority): self + { + $this->priority = $priority; + + return $this; + } } diff --git a/src/Entity/IppPrinter.php b/src/Entity/IppPrinter.php index 9f6da52..1a5efc4 100644 --- a/src/Entity/IppPrinter.php +++ b/src/Entity/IppPrinter.php @@ -4,19 +4,41 @@ namespace DR\Ipp\Entity; +use DateTimeInterface; +use DR\Ipp\Enum\AuthenticationSupportEnum; use DR\Ipp\Enum\PrinterStateEnum; -use DR\Ipp\Enum\PrinterStateReasonEnum; +use DR\Ipp\Enum\PrintQualityEnum; +use DR\Ipp\Enum\SecuritySupportEnum; class IppPrinter { private string $hostname; private string $deviceUri; private string $location; + private ?string $printerUri; private ?string $trayName = null; private ?string $ppdName = null; private ?PrinterStateEnum $printerState = null; - private ?PrinterStateReasonEnum $printerStateReason = null; + private ?string $printerStateReason = null; private ?string $printerType = null; + /** @var AuthenticationSupportEnum[] */ + private array $uriAuthSupported = []; + /** @var SecuritySupportEnum[] */ + private array $uriSecuritySupported = []; + private ?string $defaultDocumentFormat = null; + /** @var string[] */ + private array $documentFormatsSupported = []; + private ?bool $acceptingJobs = null; + private ?int $queuedJobsCount = null; + private ?DateTimeInterface $upSince = null; + private ?int $fileSizeLimit = null; + private ?int $pageLimit = null; + private ?string $defaultMedia = null; + /** @var string[] */ + private array $supportedMedia = []; + private ?PrintQualityEnum $defaultQuality = null; + /** @var PrintQualityEnum[] */ + private array $printQualitiesSupported = []; public function getHostname(): string { @@ -54,6 +76,18 @@ public function setLocation(string $location): self return $this; } + public function getPrinterUri(): ?string + { + return $this->printerUri; + } + + public function setPrinterUri(?string $printerUri): self + { + $this->printerUri = $printerUri; + + return $this; + } + public function getTrayName(): ?string { return $this->trayName; @@ -90,12 +124,12 @@ public function setPrinterState(?PrinterStateEnum $printerState): self return $this; } - public function getPrinterStateReason(): ?PrinterStateReasonEnum + public function getPrinterStateReason(): ?string { return $this->printerStateReason; } - public function setPrinterStateReason(?PrinterStateReasonEnum $printerStateReason): self + public function setPrinterStateReason(?string $printerStateReason): self { $this->printerStateReason = $printerStateReason; @@ -113,4 +147,190 @@ public function setPrinterType(?string $printerType): self return $this; } + + public function getUpSince(): ?DateTimeInterface + { + return $this->upSince; + } + + public function setUpSince(?DateTimeInterface $upSince): self + { + $this->upSince = $upSince; + + return $this; + } + + public function getQueuedJobsCount(): ?int + { + return $this->queuedJobsCount; + } + + public function setQueuedJobsCount(?int $queuedJobsCount): self + { + $this->queuedJobsCount = $queuedJobsCount; + + return $this; + } + + public function getAcceptingJobs(): ?bool + { + return $this->acceptingJobs; + } + + public function setAcceptingJobs(?bool $acceptingJobs): self + { + $this->acceptingJobs = $acceptingJobs; + + return $this; + } + + /** + * @return string[] + */ + public function getDocumentFormatsSupported(): array + { + return $this->documentFormatsSupported; + } + + /** + * @param string[] $documentFormatsSupported + */ + public function setDocumentFormatsSupported(array $documentFormatsSupported): self + { + $this->documentFormatsSupported = $documentFormatsSupported; + + return $this; + } + + public function getDefaultDocumentFormat(): ?string + { + return $this->defaultDocumentFormat; + } + + public function setDefaultDocumentFormat(?string $defaultDocumentFormat): self + { + $this->defaultDocumentFormat = $defaultDocumentFormat; + + return $this; + } + + /** + * @return AuthenticationSupportEnum[]|null + */ + public function getUriAuthSupported(): ?array + { + return $this->uriAuthSupported; + } + + /** + * @param AuthenticationSupportEnum[] $uriAuthSupported + */ + public function setUriAuthSupported(array $uriAuthSupported): self + { + $this->uriAuthSupported = $uriAuthSupported; + + return $this; + } + + /** + * @return SecuritySupportEnum[] + */ + public function getUriSecuritySupported(): array + { + return $this->uriSecuritySupported; + } + + /** + * @param SecuritySupportEnum[] $uriSecuritySupported + */ + public function setUriSecuritySupported(array $uriSecuritySupported): self + { + $this->uriSecuritySupported = $uriSecuritySupported; + + return $this; + } + + public function getFileSizeLimit(): ?int + { + return $this->fileSizeLimit; + } + + public function setFileSizeLimit(?int $fileSizeLimit): self + { + $this->fileSizeLimit = $fileSizeLimit; + + return $this; + } + + public function getPageLimit(): ?int + { + return $this->pageLimit; + } + + public function setPageLimit(?int $pageLimit): self + { + $this->pageLimit = $pageLimit; + + return $this; + } + + public function getDefaultMedia(): ?string + { + return $this->defaultMedia; + } + + public function setDefaultMedia(?string $defaultMedia): self + { + $this->defaultMedia = $defaultMedia; + + return $this; + } + + /** + * @return string[] + */ + public function getSupportedMedia(): array + { + return $this->supportedMedia; + } + + /** + * @param string[] $supportedMedia + */ + public function setSupportedMedia(array $supportedMedia): self + { + $this->supportedMedia = $supportedMedia; + + return $this; + } + + public function getDefaultQuality(): ?PrintQualityEnum + { + return $this->defaultQuality; + } + + public function setDefaultQuality(?PrintQualityEnum $defaultQuality): self + { + $this->defaultQuality = $defaultQuality; + + return $this; + } + + /** + * @return PrintQualityEnum[] + */ + public function getPrintQualitiesSupported(): array + { + return $this->printQualitiesSupported; + } + + /** + * @param PrintQualityEnum[] $printQualitiesSupported + */ + public function setPrintQualitiesSupported(array $printQualitiesSupported): self + { + $this->printQualitiesSupported = $printQualitiesSupported; + + return $this; + } } diff --git a/src/Enum/AuthenticationSupportEnum.php b/src/Enum/AuthenticationSupportEnum.php new file mode 100644 index 0000000..6a7bea7 --- /dev/null +++ b/src/Enum/AuthenticationSupportEnum.php @@ -0,0 +1,17 @@ +getAttribute($attributes, 'job-uri')?->getValue(); + $jobName = $this->getAttribute($attributes, 'job-name')?->getValue(); $jobState = $this->getAttribute($attributes, 'job-state')?->getValue(); $stateReason = $this->getAttribute($attributes, 'job-state-reasons')?->getValue(); $nrOfDocuments = $this->getAttribute($attributes, 'number-of-documents')?->getValue(); @@ -31,12 +33,18 @@ public function create(array $attributes): ?IppJob $fileSize = $this->getAttribute($attributes, 'job-k-octets')?->getValue(); $copies = $this->getAttribute($attributes, 'copies')?->getValue(); $creationDate = $this->getAttribute($attributes, 'date-time-at-creation')?->getValue(); + $processingDate = $this->getAttribute($attributes, 'date-time-at-processing')?->getValue(); $completionDate = $this->getAttribute($attributes, 'date-time-at-completed')?->getValue(); $documentFormat = $this->getAttribute($attributes, 'document-format')?->getValue(); + $documentName = $this->getAttribute($attributes, 'document-name')?->getValue(); + $printerUri = $this->getAttribute($attributes, 'job-printer-uri')?->getValue(); + $printerUptime = $this->getAttribute($attributes, 'job-printer-up-time')?->getValue(); + $priority = $this->getAttribute($attributes, 'job-priority')?->getValue(); $job = new IppJob(); $job->setId(Assert::integer($jobId)); $job->setUri(Assert::string($jobUri)); + $job->setName($jobName === null ? null : Assert::string($jobName)); $job->setJobState(JobStateEnum::from(Assert::integer($jobState))); $job->setJobStateReason(JobStateReasonEnum::tryFrom(Assert::string($stateReason)) ?? JobStateReasonEnum::Unknown); $job->setNumberOfDocuments($nrOfDocuments === null ? null : Assert::integer($nrOfDocuments)); @@ -44,8 +52,13 @@ public function create(array $attributes): ?IppJob $job->setFileSize($fileSize === null ? null : Assert::integer($fileSize)); $job->setCopies($copies === null ? null : Assert::integer($copies)); $job->setCreationDate($creationDate instanceof DateTimeInterface ? $creationDate : null); + $job->setProcessingDate($processingDate instanceof DateTimeInterface ? $processingDate : null); $job->setCompletionDate($completionDate instanceof DateTimeInterface ? $completionDate : null); $job->setDocumentFormat($documentFormat === null ? null : Assert::string($documentFormat)); + $job->setDocumentName($documentName === null ? null : Assert::string($documentName)); + $job->setPrinterUri($printerUri === null ? null : Assert::string($printerUri)); + $job->setPrinterUpSince($printerUptime === null ? null : new DateTimeImmutable('@' . Assert::nonNegativeInt($printerUptime))); + $job->setPriority($priority === null ? null : Assert::integer($priority)); return $job; } diff --git a/src/Factory/IppPrinterFactory.php b/src/Factory/IppPrinterFactory.php index 03e063e..5cbdac2 100644 --- a/src/Factory/IppPrinterFactory.php +++ b/src/Factory/IppPrinterFactory.php @@ -4,9 +4,13 @@ namespace DR\Ipp\Factory; +use BackedEnum; +use DateTimeImmutable; use DR\Ipp\Entity\IppPrinter; +use DR\Ipp\Enum\AuthenticationSupportEnum; use DR\Ipp\Enum\PrinterStateEnum; -use DR\Ipp\Enum\PrinterStateReasonEnum; +use DR\Ipp\Enum\PrintQualityEnum; +use DR\Ipp\Enum\SecuritySupportEnum; use DR\Ipp\Protocol\IppAttribute; use DR\Utils\Assert; @@ -22,15 +26,46 @@ public function create(array $attributes): ?IppPrinter return null; } + $stateReason = Assert::string($this->getAttribute($attributes, 'printer-state-reasons')?->getValue()); + $printerType = $this->getAttribute($attributes, 'printer-make-and-model')?->getValue(); + $queuedJobsCount = $this->getAttribute($attributes, 'queued-job-count')?->getValue(); + $uriAuthSupported = $this->getEnumValues($attributes, 'uri-authentication-supported', AuthenticationSupportEnum::class); + $uriSecuritySupported = $this->getEnumValues($attributes, 'uri-security-supported', SecuritySupportEnum::class); + $documentFormatDefault = $this->getAttribute($attributes, 'document-format-default')?->getValue(); + /** @phpstan-var string[]|null $documentFormatsSupported */ + $documentFormatsSupported = $this->getAttribute($attributes, 'document-format-supported')?->getValue(); + $acceptingJobs = $this->getAttribute($attributes, 'printer-is-accepting-jobs')?->getValue(); + $printerUri = $this->getAttribute($attributes, 'printer-uri-supported')?->getValue(); + $upTime = $this->getAttribute($attributes, 'printer-up-time')?->getValue(); + $fileSizeLimit = $this->getAttribute($attributes, 'job-k-limit')?->getValue(); + $pageLimit = $this->getAttribute($attributes, 'job-page-limit')?->getValue(); + /** @phpstan-var string[]|null $mediaSupported */ + $mediaSupported = $this->getAttribute($attributes, 'media-supported')?->getValue(); + $defaultMedia = $this->getAttribute($attributes, 'media-default')?->getValue(); + $defaultPrintQuality = $this->getAttribute($attributes, 'print-quality-default')?->getValue(); + $printQualitiesSupported = $this->getEnumValues($attributes, 'print-quality-supported', PrintQualityEnum::class); + $printer = new IppPrinter(); $printer->setHostname(Assert::string($name->getValue())); $printer->setLocation(Assert::string($this->getAttribute($attributes, 'printer-location')?->getValue())); $printer->setDeviceUri(Assert::string($this->getAttribute($attributes, 'device-uri')?->getValue())); $printer->setPrinterState(PrinterStateEnum::from(Assert::integer($this->getAttribute($attributes, 'printer-state')?->getValue()))); - $stateReason = PrinterStateReasonEnum::tryFrom(Assert::string($this->getAttribute($attributes, 'printer-state-reasons')?->getValue())); - $printer->setPrinterStateReason($stateReason ?? PrinterStateReasonEnum::Unknown); - $printerType = $this->getAttribute($attributes, 'printer-make-and-model')?->getValue(); + $printer->setPrinterStateReason($stateReason); $printer->setPrinterType($printerType === null ? null : Assert::string($printerType)); + $printer->setUriAuthSupported($uriAuthSupported); + $printer->setUriSecuritySupported($uriSecuritySupported); + $printer->setDefaultDocumentFormat($documentFormatDefault === null ? null : Assert::string($documentFormatDefault)); + $printer->setDocumentFormatsSupported($documentFormatsSupported === null ? [] : Assert::isArray($documentFormatsSupported)); + $printer->setAcceptingJobs($acceptingJobs === null ? null : Assert::boolean($acceptingJobs)); + $printer->setQueuedJobsCount($queuedJobsCount === null ? null : Assert::integer($queuedJobsCount)); + $printer->setPrinterUri($printerUri === null ? null : Assert::string($printerUri)); + $printer->setUpSince($upTime === null ? null : new DateTimeImmutable('@' . Assert::integer($upTime))); + $printer->setFileSizeLimit($fileSizeLimit > 0 ? Assert::integer($fileSizeLimit) : null); + $printer->setPageLimit($pageLimit > 0 ? Assert::integer($pageLimit) : null); + $printer->setSupportedMedia($mediaSupported === null ? [] : Assert::isArray($mediaSupported)); + $printer->setDefaultMedia($defaultMedia === null ? null : Assert::string($defaultMedia)); + $printer->setDefaultQuality($defaultPrintQuality === null ? null : PrintQualityEnum::tryFrom(Assert::integer($defaultPrintQuality))); + $printer->setPrintQualitiesSupported($printQualitiesSupported); return $printer; } @@ -42,4 +77,33 @@ private function getAttribute(array $attributes, string $name): ?IppAttribute { return $attributes[$name] ?? null; } + + /** + * @param IppAttribute[] $attributes + * @template T of BackedEnum + * @param class-string $enumClass + * + * @return T[] + */ + private function getEnumValues(array $attributes, string $name, string $enumClass): array + { + /** @phpstan-var int|string|null $value */ + $value = $this->getAttribute($attributes, $name)?->getValue(); + if ($value === null) { + return []; + } + + if (is_array($value) === false) { + return $enumClass::tryFrom($value) === null ? [] : [$enumClass::from($value)]; + } + + $result = []; + foreach ($value as $item) { + if ($enumClass::tryFrom($item) !== null) { + $result[] = $enumClass::from($item); + } + } + + return $result; + } } diff --git a/src/Ipp.php b/src/Ipp.php index 104b212..0105dff 100644 --- a/src/Ipp.php +++ b/src/Ipp.php @@ -115,4 +115,14 @@ public function cancelJob(IppJob $job): IppResponseInterface return $this->cancelJob->cancel($job); } + + /** + * @throws ClientExceptionInterface + */ + public function validatePrintJob(IppPrinter $printer, IppPrintFile $file): IppResponseInterface + { + $this->printOperation ??= $this->printOperationFactory->create($this->server, $this->httpClient, $this->parserFactory, $this->logger); + + return $this->printOperation->print($printer, $file, true); + } } diff --git a/src/Operations/PrintOperation.php b/src/Operations/PrintOperation.php index c152935..186890d 100644 --- a/src/Operations/PrintOperation.php +++ b/src/Operations/PrintOperation.php @@ -37,11 +37,11 @@ public function __construct( /** * @throws ClientExceptionInterface */ - public function print(IppPrinter $printer, IppPrintFile $file): IppResponseInterface + public function print(IppPrinter $printer, IppPrintFile $file, bool $dryRun = false): IppResponseInterface { $printerUri = $this->server->getUri() . '/printers/' . $printer->getHostname(); - $operation = new IppOperation(IppOperationEnum::PrintJob); + $operation = new IppOperation($dryRun ? IppOperationEnum::ValidateJob : IppOperationEnum::PrintJob); $operation->addOperationAttribute(new IppAttribute(IppTypeEnum::Charset, 'attributes-charset', 'utf-8')); $operation->addOperationAttribute(new IppAttribute(IppTypeEnum::NaturalLanguage, 'attributes-natural-language', 'en')); $operation->addOperationAttribute(new IppAttribute(IppTypeEnum::Uri, 'printer-uri', $printerUri)); diff --git a/src/Protocol/Response/IppResponseState.php b/src/Protocol/Response/IppResponseState.php index a014a3e..523cfab 100644 --- a/src/Protocol/Response/IppResponseState.php +++ b/src/Protocol/Response/IppResponseState.php @@ -4,7 +4,7 @@ namespace DR\Ipp\Protocol\Response; -use DateTime; +use DateTimeImmutable; use DR\Ipp\Entity\IppResolution; use DR\Ipp\Enum\IppTypeEnum; use DR\Utils\Assert; @@ -63,7 +63,7 @@ private function consumeBytes(string $unpack, int $byteCount): int|string|array return str_contains($unpack, '/') ? $this->unpackArray($unpack, $byteCount) : $this->unpackSingleValue($unpack, $byteCount); } - private function consumeDateTime(int $length): DateTime + private function consumeDateTime(int $length): DateTimeImmutable { // Datetime in rfc2579 format: https://datatracker.ietf.org/doc/html/rfc2579 /** @var array{year: int, month: int, day: int, hour: int, min: int, sec: int, int, tz: string, tzhour:int, tzmin: int} $dateTime */ @@ -73,7 +73,7 @@ private function consumeDateTime(int $length): DateTime $time = $dateTime['hour'] . ':' . sprintf('%02d', $dateTime['min']) . ':' . sprintf('%02d', $dateTime['sec']); $timeZone = $dateTime['tz'] . sprintf('%02d', $dateTime['tzhour']) . sprintf('%02d', $dateTime['tzmin']); - $converted = DateTime::createFromFormat('Y-n-j G:i:sO', $date . ' ' . $time . $timeZone); + $converted = DateTimeImmutable::createFromFormat('Y-n-j G:i:sO', $date . ' ' . $time . $timeZone); if ($converted === false) { throw new RuntimeException('Invalid DateTime in IPP attribute'); } diff --git a/tests/Unit/Factory/IppPrinterFactoryTest.php b/tests/Unit/Factory/IppPrinterFactoryTest.php index a134821..f54d30c 100644 --- a/tests/Unit/Factory/IppPrinterFactoryTest.php +++ b/tests/Unit/Factory/IppPrinterFactoryTest.php @@ -6,7 +6,6 @@ use DR\Ipp\Enum\IppTypeEnum; use DR\Ipp\Enum\PrinterStateEnum; -use DR\Ipp\Enum\PrinterStateReasonEnum; use DR\Ipp\Factory\IppPrinterFactory; use DR\Ipp\Protocol\IppAttribute; use PHPUnit\Framework\Attributes\CoversClass; @@ -28,7 +27,7 @@ public function testCreate(): void $attributes['printer-location'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-location', 'test'); $attributes['device-uri'] = new IppAttribute(IppTypeEnum::Keyword, 'device-uri', 'foo'); $attributes['printer-state'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', PrinterStateEnum::Idle->value); - $attributes['printer-state-reasons'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', PrinterStateReasonEnum::Paused->value); + $attributes['printer-state-reasons'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', 'paused'); $attributes['printer-make-and-model'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-make-and-model', 'baz'); $factory = new IppPrinterFactory(); @@ -38,7 +37,7 @@ public function testCreate(): void static::assertSame('test', $printer->getLocation()); static::assertSame('foo', $printer->getDeviceUri()); static::assertSame(PrinterStateEnum::Idle, $printer->getPrinterState()); - static::assertSame(PrinterStateReasonEnum::Paused, $printer->getPrinterStateReason()); + static::assertSame('paused', $printer->getPrinterStateReason()); static::assertSame('baz', $printer->getPrinterType()); } } From c9a1d460c62cad6da8c1e99dd90a495ccfc025be Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 5 Nov 2025 17:43:55 +0100 Subject: [PATCH 2/6] Implement remaining minimal requirements for supporting ipp 2.0 spec --- phpmd.baseline.xml | 7 +++++++ tests/Unit/Client/CupsIppHttpClientTest.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 phpmd.baseline.xml diff --git a/phpmd.baseline.xml b/phpmd.baseline.xml new file mode 100644 index 0000000..8ca7aec --- /dev/null +++ b/phpmd.baseline.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Unit/Client/CupsIppHttpClientTest.php b/tests/Unit/Client/CupsIppHttpClientTest.php index 8d3ec10..a501edf 100644 --- a/tests/Unit/Client/CupsIppHttpClientTest.php +++ b/tests/Unit/Client/CupsIppHttpClientTest.php @@ -105,7 +105,7 @@ public function testException(): void $server->expects($this->once())->method('getUri')->willReturn('https://cups'); $response = $this->createMock(ResponseInterface::class); - $response->expects($this->exactly(4))->method('getStatusCode')->willReturn(500); + $response->expects($this->exactly(3))->method('getStatusCode')->willReturn(500); $response->expects($this->once())->method('getReasonPhrase')->willReturn('Internal server error'); $response->expects($this->once())->method('getBody')->willReturn($this->createMock(StreamInterface::class)); From 12d44b301850d57bfde1aca0a5dcd4aacb2034b8 Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 5 Nov 2025 17:59:18 +0100 Subject: [PATCH 3/6] Implement remaining minimal requirements for supporting ipp 2.0 spec --- tests/Unit/Factory/IppPrinterFactoryTest.php | 31 +++++++++++++++----- tests/Unit/IppTest.php | 12 ++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/tests/Unit/Factory/IppPrinterFactoryTest.php b/tests/Unit/Factory/IppPrinterFactoryTest.php index f54d30c..a9fbc2b 100644 --- a/tests/Unit/Factory/IppPrinterFactoryTest.php +++ b/tests/Unit/Factory/IppPrinterFactoryTest.php @@ -4,8 +4,11 @@ namespace DR\Ipp\Tests\Unit\Factory; +use DateTimeImmutable; +use DR\Ipp\Enum\AuthenticationSupportEnum; use DR\Ipp\Enum\IppTypeEnum; use DR\Ipp\Enum\PrinterStateEnum; +use DR\Ipp\Enum\PrintQualityEnum; use DR\Ipp\Factory\IppPrinterFactory; use DR\Ipp\Protocol\IppAttribute; use PHPUnit\Framework\Attributes\CoversClass; @@ -22,13 +25,24 @@ public function testCreateNull(): void public function testCreate(): void { - $attributes = []; - $attributes['printer-name'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-name', 'unit'); - $attributes['printer-location'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-location', 'test'); - $attributes['device-uri'] = new IppAttribute(IppTypeEnum::Keyword, 'device-uri', 'foo'); - $attributes['printer-state'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', PrinterStateEnum::Idle->value); - $attributes['printer-state-reasons'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', 'paused'); - $attributes['printer-make-and-model'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-make-and-model', 'baz'); + $attributes = []; + $attributes['printer-name'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-name', 'unit'); + $attributes['printer-location'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-location', 'test'); + $attributes['device-uri'] = new IppAttribute(IppTypeEnum::Keyword, 'device-uri', 'foo'); + $attributes['printer-state'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', PrinterStateEnum::Idle->value); + $attributes['printer-state-reasons'] = new IppAttribute(IppTypeEnum::Int, 'printer-state', 'paused'); + $attributes['printer-make-and-model'] = new IppAttribute(IppTypeEnum::Keyword, 'printer-make-and-model', 'baz'); + $attributes['printer-up-time'] = new IppAttribute(IppTypeEnum::Int, 'printer-up-time', 0); + $attributes['uri-authentication-supported'] = new IppAttribute( + IppTypeEnum::Keyword, + 'uri-authentication-supported', + AuthenticationSupportEnum::None->value, + ); + $attributes['print-quality-supported'] = new IppAttribute( + IppTypeEnum::Keyword, + 'print-quality-supported', + [PrintQualityEnum::Normal->value, PrintQualityEnum::High->value], + ); $factory = new IppPrinterFactory(); $printer = $factory->create($attributes); @@ -39,5 +53,8 @@ public function testCreate(): void static::assertSame(PrinterStateEnum::Idle, $printer->getPrinterState()); static::assertSame('paused', $printer->getPrinterStateReason()); static::assertSame('baz', $printer->getPrinterType()); + static::assertSame([AuthenticationSupportEnum::None], $printer->getUriAuthSupported()); + static::assertSame([PrintQualityEnum::Normal, PrintQualityEnum::High], $printer->getPrintQualitiesSupported()); + static::assertSame((new DateTimeImmutable('@0'))->getTimeStamp(), $printer->getUpSince()->getTimeStamp()); } } diff --git a/tests/Unit/IppTest.php b/tests/Unit/IppTest.php index a25c000..9cfe3fd 100644 --- a/tests/Unit/IppTest.php +++ b/tests/Unit/IppTest.php @@ -113,6 +113,18 @@ public function testCancelJob(): void $this->ipp->cancelJob($job); } + public function testValidatePrintJob(): void + { + $printer = $this->createMock(IppPrinter::class); + $file = $this->createMock(IppPrintFile::class); + + $mock = $this->createMock(PrintOperation::class); + $this->setPrivateProperty($this->ipp, 'printOperation', $mock); + + $mock->expects($this->once())->method('print')->with($printer, $file, true); + $this->ipp->validatePrintJob($printer, $file); + } + /** * @param class-string|object $obj * From de53ffdc60060ac6f3fe4bc4a9927ea293e4d5b0 Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 5 Nov 2025 17:59:37 +0100 Subject: [PATCH 4/6] Implement remaining minimal requirements for supporting ipp 2.0 spec --- tests/Unit/Factory/IppPrinterFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Factory/IppPrinterFactoryTest.php b/tests/Unit/Factory/IppPrinterFactoryTest.php index a9fbc2b..655ba4f 100644 --- a/tests/Unit/Factory/IppPrinterFactoryTest.php +++ b/tests/Unit/Factory/IppPrinterFactoryTest.php @@ -55,6 +55,6 @@ public function testCreate(): void static::assertSame('baz', $printer->getPrinterType()); static::assertSame([AuthenticationSupportEnum::None], $printer->getUriAuthSupported()); static::assertSame([PrintQualityEnum::Normal, PrintQualityEnum::High], $printer->getPrintQualitiesSupported()); - static::assertSame((new DateTimeImmutable('@0'))->getTimeStamp(), $printer->getUpSince()->getTimeStamp()); + static::assertSame((new DateTimeImmutable('@0'))->getTimeStamp(), $printer->getUpSince()?->getTimeStamp()); } } From 6915a8a7162abe9aececf3114cdfcc41c3a3eade Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 19 Nov 2025 10:16:23 +0100 Subject: [PATCH 5/6] Implement remaining minimal requirements for supporting ipp 2.0 spec --- src/Factory/IppJobFactory.php | 2 +- src/Factory/IppPrinterFactory.php | 4 ++-- tests/Unit/Factory/IppJobFactoryTest.php | 4 ++-- tests/Unit/Protocol/Response/IppResponseStateTest.php | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Factory/IppJobFactory.php b/src/Factory/IppJobFactory.php index c6410b2..02e3b97 100644 --- a/src/Factory/IppJobFactory.php +++ b/src/Factory/IppJobFactory.php @@ -57,7 +57,7 @@ public function create(array $attributes): ?IppJob $job->setDocumentFormat($documentFormat === null ? null : Assert::string($documentFormat)); $job->setDocumentName($documentName === null ? null : Assert::string($documentName)); $job->setPrinterUri($printerUri === null ? null : Assert::string($printerUri)); - $job->setPrinterUpSince($printerUptime === null ? null : new DateTimeImmutable('@' . Assert::nonNegativeInt($printerUptime))); + $job->setPrinterUpSince($printerUptime === null ? null : (new DateTimeImmutable())->setTimeStamp(Assert::nonNegativeInt($printerUptime))); $job->setPriority($priority === null ? null : Assert::integer($priority)); return $job; diff --git a/src/Factory/IppPrinterFactory.php b/src/Factory/IppPrinterFactory.php index 5cbdac2..d86be9c 100644 --- a/src/Factory/IppPrinterFactory.php +++ b/src/Factory/IppPrinterFactory.php @@ -59,12 +59,12 @@ public function create(array $attributes): ?IppPrinter $printer->setAcceptingJobs($acceptingJobs === null ? null : Assert::boolean($acceptingJobs)); $printer->setQueuedJobsCount($queuedJobsCount === null ? null : Assert::integer($queuedJobsCount)); $printer->setPrinterUri($printerUri === null ? null : Assert::string($printerUri)); - $printer->setUpSince($upTime === null ? null : new DateTimeImmutable('@' . Assert::integer($upTime))); + $printer->setUpSince($upTime === null ? null : (new DateTimeImmutable())->setTimeStamp(Assert::integer($upTime))); $printer->setFileSizeLimit($fileSizeLimit > 0 ? Assert::integer($fileSizeLimit) : null); $printer->setPageLimit($pageLimit > 0 ? Assert::integer($pageLimit) : null); $printer->setSupportedMedia($mediaSupported === null ? [] : Assert::isArray($mediaSupported)); $printer->setDefaultMedia($defaultMedia === null ? null : Assert::string($defaultMedia)); - $printer->setDefaultQuality($defaultPrintQuality === null ? null : PrintQualityEnum::tryFrom(Assert::integer($defaultPrintQuality))); + $printer->setDefaultQuality($defaultPrintQuality === null ? null : PrintQualityEnum::tryFrom(Assert::nonNegativeInt($defaultPrintQuality))); $printer->setPrintQualitiesSupported($printQualitiesSupported); return $printer; diff --git a/tests/Unit/Factory/IppJobFactoryTest.php b/tests/Unit/Factory/IppJobFactoryTest.php index 91f9070..210571f 100644 --- a/tests/Unit/Factory/IppJobFactoryTest.php +++ b/tests/Unit/Factory/IppJobFactoryTest.php @@ -4,7 +4,7 @@ namespace DR\Ipp\Tests\Unit\Factory; -use DateTime; +use DateTimeImmutable; use DR\Ipp\Enum\IppTypeEnum; use DR\Ipp\Enum\JobStateEnum; use DR\Ipp\Enum\JobStateReasonEnum; @@ -41,7 +41,7 @@ public function testCreateMinimal(): void public function testCreate(): void { - $date = new DateTime(); + $date = new DateTimeImmutable(); $attributes = []; $attributes['job-id'] = new IppAttribute(IppTypeEnum::Int, 'job-id', 1); diff --git a/tests/Unit/Protocol/Response/IppResponseStateTest.php b/tests/Unit/Protocol/Response/IppResponseStateTest.php index 745fad8..430328b 100644 --- a/tests/Unit/Protocol/Response/IppResponseStateTest.php +++ b/tests/Unit/Protocol/Response/IppResponseStateTest.php @@ -4,7 +4,7 @@ namespace DR\Ipp\Tests\Unit\Protocol\Response; -use DateTime; +use DateTimeImmutable; use DR\Ipp\Entity\IppResolution; use DR\Ipp\Enum\IppTypeEnum; use DR\Ipp\Protocol\Response\IppResponseState; @@ -50,7 +50,7 @@ public function testConsumeBool(): void public function testConsumeDate(): void { - $expected = new DateTime('1990-02-03T14:01:02.000000+0100'); + $expected = new DateTimeImmutable('1990-02-03T14:01:02.000000+0100'); $state = new IppResponseState(pack('nc6ac2', 1990, 2, 3, 14, 01, 02, 0, '+', 1, 0)); static::assertEquals($expected, $state->consume(11, IppTypeEnum::DateTime)); From b614bfb0574af363dee7a2a695ef5ffcfdde80b1 Mon Sep 17 00:00:00 2001 From: Ruben Kluft Date: Wed, 19 Nov 2025 10:17:31 +0100 Subject: [PATCH 6/6] Implement remaining minimal requirements for supporting ipp 2.0 spec --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ea5f56..4462263 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ composer require digitalrevolution/ipp ``` ### Print job validation -If you want to test if a print operation should work, without actually printing anything, you can use the validatePrintJob operation instead. +To test a print operation without actually printing anything, you can use the `validatePrintJob` operation instead. ```php $ipp->validatePrintJob($printer, $ippFile); ```