Skip to content

Commit ca1ad8e

Browse files
authored
Merge pull request #11 from 123inkt/get_attributes_operation
Add GetPrinterAttributesOperation Add a function to get printer attributes, Implement several parsing functions to allow correct parsing of these attributes. Adds two new protocol entities; for Collections and Resolution Refactors the Ipp response parsing to maintain readability
2 parents a3bfc67 + 8ed3036 commit ca1ad8e

File tree

11 files changed

+312
-69
lines changed

11 files changed

+312
-69
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ composer require digitalrevolution/ipp
6363
$ipp->printerAdministration()->deletePrinter($printer);
6464
```
6565

66+
### Get Printer attributes
67+
```php
68+
$printer = new IppPrinter();
69+
$printer->setHostname('my.printer');
70+
71+
$response = $ipp->getPrinterAttributes($printer);
72+
$attributes = $response->getAttributes();
73+
$printerName = $attributes["printer-name"]->getValue();
74+
```
75+
6676
### Creating a custom IPP operation
6777

6878
This project is created to be easily extensible, adding a new IPP operation is as simple as making sure it has an identifier in IppOperationEnum

src/Entity/Response/CupsIppResponse.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class CupsIppResponse implements IppResponseInterface
1414
{
1515
/**
16-
* @param IppAttribute[] $attributes
16+
* @param array<string, IppAttribute> $attributes
1717
*/
1818
public function __construct(private readonly IppStatusCodeEnum $statusCode, private readonly array $attributes)
1919
{
@@ -57,7 +57,7 @@ public function getStatusMessage(): ?string
5757
}
5858

5959
/**
60-
* @return IppAttribute[]
60+
* @inheritDoc
6161
*/
6262
public function getAttributes(): array
6363
{

src/Entity/Response/IppResponseInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace DR\Ipp\Entity\Response;
66

77
use DR\Ipp\Enum\JobStateEnum;
8+
use DR\Ipp\Protocol\IppAttribute;
89

910
interface IppResponseInterface
1011
{
@@ -13,4 +14,9 @@ public function getJobUri(): ?string;
1314
public function getJobState(): ?JobStateEnum;
1415

1516
public function getStatusMessage(): ?string;
17+
18+
/**
19+
* @return array<string, IppAttribute>
20+
*/
21+
public function getAttributes(): array;
1622
}

src/Enum/IppTypeEnum.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
enum IppTypeEnum: int
88
{
9+
case Unsupported = 0x10;
10+
case Default = 0x11;
11+
case Unknown = 0x12;
12+
case NoValue = 0x13;
13+
case NotSettable = 0x15;
14+
case DeleteAttribute = 0x16;
15+
case AdminDefine = 0x17;
916
case Int = 0x21;
1017
case Bool = 0x22;
1118
case Enum = 0x23;

src/Ipp.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use DR\Ipp\Operations\Cups\CupsCreatePrinter;
1515
use DR\Ipp\Operations\Cups\CupsDeletePrinter;
1616
use DR\Ipp\Operations\GetJobAttributesOperation;
17+
use DR\Ipp\Operations\GetPrinterAttributesOperation;
1718
use DR\Ipp\Operations\PrintOperation;
1819
use DR\Ipp\Protocol\IppResponseParser;
1920
use DR\Ipp\Service\PrinterAdminService;
@@ -31,6 +32,7 @@ class Ipp implements LoggerAwareInterface
3132

3233
private ?PrintOperation $printOperation = null;
3334
private ?GetJobAttributesOperation $getJobAttributes = null;
35+
private ?GetPrinterAttributesOperation $getPrinterAttributes = null;
3436

3537
private ?PrinterAdminService $printerAdmin = null;
3638

@@ -72,4 +74,15 @@ public function getJobAttributes(string $jobUri): IppResponseInterface
7274

7375
return $this->getJobAttributes->getJob($jobUri);
7476
}
77+
78+
/**
79+
* Requests a list of printer attributes for the specified printer
80+
* @throws ClientExceptionInterface
81+
*/
82+
public function getPrinterAttributes(IppPrinter $printer): IppResponseInterface
83+
{
84+
$this->getPrinterAttributes ??= new GetPrinterAttributesOperation($this->server, $this->httpClient);
85+
86+
return $this->getPrinterAttributes->getAttributes($printer);
87+
}
7588
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DR\Ipp\Operations;
6+
7+
use DR\Ipp\Client\IppHttpClientInterface;
8+
use DR\Ipp\Entity\IppPrinter;
9+
use DR\Ipp\Entity\IppServer;
10+
use DR\Ipp\Entity\Response\IppResponseInterface;
11+
use DR\Ipp\Enum\IppOperationEnum;
12+
use DR\Ipp\Enum\IppTypeEnum;
13+
use DR\Ipp\Protocol\IppAttribute;
14+
use DR\Ipp\Protocol\IppOperation;
15+
use Psr\Http\Client\ClientExceptionInterface;
16+
17+
/**
18+
* @internal
19+
*/
20+
class GetPrinterAttributesOperation
21+
{
22+
public function __construct(private readonly IppServer $server, private readonly IppHttpClientInterface $client)
23+
{
24+
}
25+
26+
/**
27+
* @throws ClientExceptionInterface
28+
*/
29+
public function getAttributes(IppPrinter $printer): IppResponseInterface
30+
{
31+
$printerUri = $this->server->getUri() . '/printers/' . $printer->getHostname();
32+
33+
$operation = new IppOperation(IppOperationEnum::GetPrinterAttributes);
34+
$operation->addOperationAttribute(new IppAttribute(IppTypeEnum::Charset, 'attributes-charset', 'utf-8'));
35+
$operation->addOperationAttribute(new IppAttribute(IppTypeEnum::NaturalLanguage, 'attributes-natural-language', 'en'));
36+
$operation->addOperationAttribute(new IppAttribute(IppTypeEnum::Uri, 'printer-uri', $printerUri));
37+
$operation->addOperationAttribute(new IppAttribute(IppTypeEnum::Keyword, 'requested-attributes', 'all'));
38+
39+
return $this->client->sendRequest($operation);
40+
}
41+
}

src/Protocol/IppAttribute.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ public function getValue(): mixed
2828
return $this->value;
2929
}
3030

31+
public function appendValue(mixed $additionalValue): self
32+
{
33+
if (is_array($this->value) === false) {
34+
$values = [$this->value];
35+
}
36+
$values[] = $additionalValue;
37+
38+
return new self($this->type, $this->name, $values);
39+
}
40+
3141
/**
3242
* @internal
3343
*/

src/Protocol/IppCollection.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DR\Ipp\Protocol;
6+
7+
class IppCollection
8+
{
9+
/** @var array<string, mixed> */
10+
private array $values = [];
11+
12+
public function add(string $name, mixed $value): void
13+
{
14+
$this->values[$name] = $value;
15+
}
16+
17+
/**
18+
* @return array<string, mixed>
19+
*/
20+
public function getValues(): array
21+
{
22+
return $this->values;
23+
}
24+
}

src/Protocol/IppResolution.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DR\Ipp\Protocol;
6+
7+
class IppResolution
8+
{
9+
public function __construct(private readonly int $crossFeedResolution, private readonly int $feedResolution, private readonly int $unit)
10+
{
11+
}
12+
13+
public function getCrossFeedResolution(): int
14+
{
15+
return $this->crossFeedResolution;
16+
}
17+
18+
public function getFeedResolution(): int
19+
{
20+
return $this->feedResolution;
21+
}
22+
23+
public function getUnit(): int
24+
{
25+
return $this->unit;
26+
}
27+
}

src/Protocol/IppResponseParser.php

Lines changed: 60 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,45 @@
44

55
namespace DR\Ipp\Protocol;
66

7-
use DateTime;
87
use DR\Ipp\Entity\Response\CupsIppResponse;
98
use DR\Ipp\Entity\Response\IppResponseInterface;
109
use DR\Ipp\Enum\IppOperationTagEnum;
1110
use DR\Ipp\Enum\IppStatusCodeEnum;
1211
use DR\Ipp\Enum\IppTypeEnum;
13-
use RuntimeException;
1412

1513
/**
1614
* @internal
1715
*/
1816
class IppResponseParser implements IppResponseParserInterface
1917
{
18+
private IppAttribute $lastAttribute;
19+
20+
/**
21+
* @see https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1
22+
*/
2023
public function getResponse(string $response): IppResponseInterface
2124
{
22-
[, $response] = $this->consume($response, 2, IppTypeEnum::Int); // version 0x0101
25+
$state = new IppResponseState($response);
26+
27+
$state->consume(2, IppTypeEnum::Int); // version 0x0101
2328
/** @var int $status */
24-
[$status, $response] = $this->consume($response, 2, IppTypeEnum::Int); // status 0x0502
25-
[, $response] = $this->consume($response, 4, IppTypeEnum::Int); // requestId 0x00000001
26-
[, $response] = $this->consume($response, 1, IppTypeEnum::Int); // IPPOperationTag::OPERATION_ATTRIBUTE_START
29+
$status = $state->consume(2, IppTypeEnum::Int); // status 0x0502
30+
$state->consume(4, IppTypeEnum::Int); // requestId 0x00000001
31+
$state->consume(1, IppTypeEnum::Int); // IPPOperationTag::OPERATION_ATTRIBUTE_START
2732

2833
$attributesTags = [
2934
IppOperationTagEnum::JobAttributeStart->value,
3035
IppOperationTagEnum::PrinterAttributeStart->value,
3136
IppOperationTagEnum::UnsupportedAttributes->value,
3237
];
3338
$attributes = [];
34-
while ($this->unpack('c', $response) !== IppOperationTagEnum::AttributeEnd->value) {
35-
// look for attribute tag and remove it before parsing further attributes
36-
if (in_array($this->unpack('c', $response), $attributesTags, true)) {
37-
[, $response] = $this->consume($response, 1, null);
39+
while ($state->getNextByte() !== IppOperationTagEnum::AttributeEnd->value) {
40+
// look for an attribute tag and remove it before parsing further attributes
41+
if (in_array($state->getNextByte(), $attributesTags, true)) {
42+
$state->consume(1, null);
3843
}
3944

40-
[$attribute, $response] = $this->consumeAttribute($response);
45+
$attribute = $this->getAttribute($state);
4146
$attributes[$attribute->getName()] = $attribute;
4247
}
4348
$statusCode = IppStatusCodeEnum::tryFrom($status) ?? IppStatusCodeEnum::Unknown;
@@ -46,78 +51,66 @@ public function getResponse(string $response): IppResponseInterface
4651
}
4752

4853
/**
49-
* Decodes an attribute from the response, and returns the decoded values and the rest of the response
50-
* @return array{IppAttribute, string}
54+
* @see https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1.6
5155
*/
52-
private function consumeAttribute(string $response): array
56+
private function getCollection(IppResponseState $state): IppCollection
5357
{
54-
/** @var int $type */
55-
[$type, $response] = $this->consume($response, 1, null);
56-
[$nameLength, $response] = $this->consume($response, 2, IppTypeEnum::Int);
57-
/** @var int $nameLength */
58-
[$attrName, $response] = $this->consume($response, $nameLength, IppTypeEnum::NameWithoutLang);
59-
/** @var string $attrName */
60-
[$valueLength, $response] = $this->consume($response, 2, IppTypeEnum::Int);
61-
/** @var int $valueLength */
62-
[$attrValue, $response] = $this->consume(
63-
$response,
64-
$valueLength,
65-
IppTypeEnum::tryFrom($type),
66-
);
58+
$collection = new IppCollection();
59+
60+
$state->consume(2, null); // 0x0000
61+
while ($state->getNextByte() !== IppTypeEnum::EndCollection->value) {
62+
$state->consume(3, null); // 0x4a 0x00 0x00
63+
64+
/** @var string $name */
65+
$name = $this->getAttributeValue(IppTypeEnum::MemberAttributeName, $state);
66+
/** @var int $valueType */
67+
$valueType = $state->consume(1, null);
68+
$state->consume(2, null); // 0x00 0x00
69+
70+
/** @var int $valueLength */
71+
$valueLength = $state->consume(2, IppTypeEnum::Int);
72+
$value = $state->consume($valueLength, IppTypeEnum::tryFrom($valueType));
73+
$collection->add($name, $value);
74+
}
75+
$state->consume(5, null); // 0x37 0x00 0x00 0x00 0x00
6776

68-
return [new IppAttribute(IppTypeEnum::tryFrom($type) ?? IppTypeEnum::Int, $attrName, $attrValue), $response];
77+
return $collection;
6978
}
7079

7180
/**
72-
* Decodes part of a binary string, and returns the decoded value and the rest of the binary string
73-
* @return array{int|string|DateTime, string}
81+
* Decodes an attribute from the response, and returns the decoded value(s)
7482
*/
75-
private function consume(string $response, int $length, ?IppTypeEnum $type): array
83+
private function getAttribute(IppResponseState $state): IppAttribute
7684
{
77-
switch ($type) {
78-
case IppTypeEnum::Int:
79-
case IppTypeEnum::Enum:
80-
$unpack = $length === 2 ? 'n' : 'N';
81-
break;
82-
case IppTypeEnum::DateTime:
83-
return [$this->unpackDateTime($response), substr($response, $length)];
84-
case null:
85-
$unpack = 'c' . $length;
86-
break;
87-
default:
88-
$unpack = 'a' . $length;
89-
}
90-
91-
return [$this->unpack($unpack, $response), substr($response, $length)];
92-
}
85+
/** @var int $type */
86+
$type = $state->consume(1, null);
87+
$attrType = IppTypeEnum::tryFrom($type);
9388

94-
private function unpackDateTime(string $response): DateTime
95-
{
96-
// Datetime in rfc2579 format: https://datatracker.ietf.org/doc/html/rfc2579
97-
/** @var array{year: int, month: int, day: int, hour: int, min: int, sec: int, int, tz: string, tzhour:int, tzmin: int}|false $dateTime */
98-
$dateTime = @unpack('nyear/cmonth/cday/chour/cmin/csec/c/atz/ctzhour/ctzmin', $response);
99-
if ($dateTime === false) {
100-
throw new RuntimeException('Failed to unpack IPP datetime');
89+
/** @var int $nameLength */
90+
$nameLength = $state->consume(2, IppTypeEnum::Int);
91+
// Additional value https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1.5
92+
if ($nameLength === 0x0000) {
93+
return $this->lastAttribute->appendValue($this->getAttributeValue($attrType, $state));
10194
}
102-
$date = $dateTime['year'] . '-' . $dateTime['month'] . '-' . $dateTime['day'];
103-
$time = $dateTime['hour'] . ':' . sprintf('%02d', $dateTime['min']) . ':' . sprintf('%02d', $dateTime['sec']);
104-
$timeZone = $dateTime['tz'] . sprintf('%02d', $dateTime['tzhour']) . sprintf('%02d', $dateTime['tzmin']);
10595

106-
$converted = DateTime::createFromFormat('Y-n-j G:i:sO', $date . ' ' . $time . $timeZone);
107-
if ($converted === false) {
108-
throw new RuntimeException('Invalid DateTime in IPP attribute');
109-
}
96+
/** @var string $attrName */
97+
$attrName = $state->consume($nameLength, IppTypeEnum::NameWithoutLang);
98+
$attrValue = $this->getAttributeValue($attrType, $state);
99+
100+
$this->lastAttribute = new IppAttribute($attrType ?? IppTypeEnum::Int, $attrName, $attrValue);
110101

111-
return $converted;
102+
return $this->lastAttribute;
112103
}
113104

114-
private function unpack(string $unpack, string $string): string|int
105+
private function getAttributeValue(?IppTypeEnum $type, IppResponseState $state): mixed
115106
{
116-
$data = @unpack($unpack, $string);
117-
if ($data === false || isset($data[1]) === false || (is_string($data[1]) === false && is_int($data[1]) === false)) {
118-
throw new RuntimeException();
107+
if ($type === IppTypeEnum::Collection) {
108+
return $this->getCollection($state);
119109
}
120110

121-
return $data[1];
111+
/** @var int $valueLength */
112+
$valueLength = $state->consume(2, IppTypeEnum::Int);
113+
114+
return $state->consume($valueLength, $type);
122115
}
123116
}

0 commit comments

Comments
 (0)