Skip to content

Commit c71fe9c

Browse files
committed
Use CID compliant hrefs in XOP Include encoder
1 parent eaed608 commit c71fe9c

File tree

6 files changed

+101
-22
lines changed

6 files changed

+101
-22
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,9 @@ use Soap\Psr18AttachmentsMiddleware\Attachment\Attachment;
131131
// Your request can now contain Attachments directly:
132132
// These attachments will be automatically added to the AttachmentStorageInterface and a <xop:Include> element will be added to your request instead.
133133
$yourSoapPayload = (object) [
134-
'file' => Attachment::create(
135-
name: 'file',
134+
// A special cid named constructor is added to make sure your attachment Content-Id is cid spec-compliant and therefore can be used with XOP.
135+
'file' => Attachment::cid(
136+
uri: 'foo@domain.com',
136137
filename: 'your.pdf',
137138
content: FileStream::create('path/to/your.pdf', FileStream::READ_MODE)
138139
)

src/Attachment/Attachment.php

+26
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,30 @@ public static function create(
4040
$content
4141
);
4242
}
43+
44+
/**
45+
* A named constructor for creating attachments for XOP.
46+
* This makes the ID "cid"-spec compliant.
47+
*
48+
* @see https://www.ietf.org/rfc/rfc2392.txt
49+
*
50+
* @param ResourceStream<resource> $content
51+
*/
52+
public static function cid(
53+
string $uri,
54+
string $name,
55+
string $filename,
56+
ResourceStream $content,
57+
?string $mimeType = null,
58+
): self {
59+
$mimeType ??= (new ApacheMimetypeHelper)->getMimetypeFromFilename($filename) ?? 'application/octet-stream';
60+
61+
return new self(
62+
'<'.$uri.'>',
63+
$name,
64+
$filename,
65+
$mimeType,
66+
$content
67+
);
68+
}
4369
}

src/Encoding/Xop/XopIncludeEncoder.php

+40-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Soap\Psr18AttachmentsMiddleware\Storage\AttachmentStorageInterface;
1010
use VeeWee\Reflecta\Iso\Iso;
1111
use VeeWee\Xml\Writer\Writer;
12+
use function Psl\Type\non_empty_string;
1213
use function VeeWee\Xml\Writer\Builder\attribute;
1314
use function VeeWee\Xml\Writer\Builder\namespaced_element;
1415
use function VeeWee\Xml\Writer\Mapper\memory_output;
@@ -26,35 +27,67 @@ public function __construct(
2627
}
2728

2829
/**
30+
* Encodes an attachment to a <xop:Include> element based on the XOP specification:
31+
*
32+
* @see https://www.w3.org/TR/xop10/#RFC2392
33+
*
2934
* @return Iso<Attachment, non-empty-string>
3035
*/
3136
public function iso(Context $context): Iso
3237
{
38+
$cid = $this->cid();
39+
3340
return new Iso(
3441
/**
3542
* @return non-empty-string
3643
*/
37-
function (Attachment $raw): string {
38-
39-
$this->attachmentStorage->requestAttachments()->add($raw);
40-
44+
static function (Attachment $raw) use ($cid): string {
4145
/** @var non-empty-string */
4246
return Writer::inMemory()
4347
->write(namespaced_element(
4448
self::XMLNS_XOP,
4549
'xop',
4650
'Include',
47-
attribute('href', 'cid:' . $raw->id)
51+
attribute('href', $cid->to($raw))
4852
))
4953
->map(memory_output());
5054
},
5155
/**
5256
* @param non-empty-string|Element $xml
5357
*/
54-
function (Element|string $xml): Attachment {
58+
static function (Element|string $xml) use ($cid) : Attachment {
5559
$element = ($xml instanceof Element ? $xml : Element::fromString($xml))->element();
5660
$href = $element->getAttribute('href');
57-
$id = preg_replace('/^cid:(.*)/', '$1', $href);
61+
62+
return $cid->from(non_empty_string()->assert($href));
63+
}
64+
);
65+
}
66+
67+
/**
68+
* Encodes the cid href for the attachment based on the cid specification:
69+
*
70+
* @see https://www.ietf.org/rfc/rfc2392.txt
71+
*
72+
* @return Iso<Attachment, non-empty-string>
73+
*/
74+
private function cid(): Iso
75+
{
76+
/** @var Iso<Attachment, non-empty-string> */
77+
return new Iso(
78+
/**
79+
* @return non-empty-string
80+
*/
81+
function (Attachment $raw): string {
82+
$this->attachmentStorage->requestAttachments()->add($raw);
83+
84+
return 'cid:' . preg_replace('/^<(.*)>$/', '$1', $raw->id);
85+
},
86+
/**
87+
* @param non-empty-string $xml
88+
*/
89+
function (string $xml): Attachment {
90+
$id = '<' . preg_replace('/^cid:(.*)/', '$1', $xml) . '>';
5891

5992
return $this->attachmentStorage->responseAttachments()->findById($id);
6093
}

tests/Unit/Attachment/AttachmentTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,21 @@ public function it_can_create_an_attachment(): void
4242
static::assertSame('application/pdf', $attachment->mimeType);
4343
static::assertSame($stream, $attachment->content);
4444
}
45+
46+
#[Test]
47+
public function it_can_create_a_cid_compliant_attachment(): void
48+
{
49+
$attachment = Attachment::cid(
50+
'some@uri.com',
51+
'name',
52+
'filename.pdf',
53+
$stream = MemoryStream::create(),
54+
);
55+
56+
static::assertSame('<some@uri.com>', $attachment->id);
57+
static::assertSame('name', $attachment->name);
58+
static::assertSame('filename.pdf', $attachment->filename);
59+
static::assertSame('application/pdf', $attachment->mimeType);
60+
static::assertSame($stream, $attachment->content);
61+
}
4562
}

tests/Unit/Encoding/Xop/XopIncludeEncoderTest.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ public function it_can_encode_xop_include_attachment(): void
2828
$iso = $encoder->iso($this->createContext());
2929

3030
$result = $iso->to(
31-
$attachment = new Attachment('foo', 'file', 'file.pdf', 'application/pdf', MemoryStream::create())
31+
$attachment = new Attachment('<foo@x.com>', 'file', 'file.pdf', 'application/pdf', MemoryStream::create())
3232
);
3333

34-
static::assertSame('<xop:Include href="cid:foo" xmlns:xop="http://www.w3.org/2004/08/xop/include"/>', $result);
35-
static::assertSame($attachment, $storage->requestAttachments()->findById('foo'));
34+
static::assertSame('<xop:Include href="cid:foo@x.com" xmlns:xop="http://www.w3.org/2004/08/xop/include"/>', $result);
35+
static::assertSame($attachment, $storage->requestAttachments()->findById('<foo@x.com>'));
3636
}
3737

3838
#[Test]
@@ -44,9 +44,9 @@ public function it_can_decode_xop_include_attachment(): void
4444
$iso = $encoder->iso($this->createContext());
4545

4646
$storage->responseAttachments()->add(
47-
$attachment = new Attachment('foo', 'file', 'file.pdf', 'application/pdf', MemoryStream::create())
47+
$attachment = Attachment::cid('foo@x.com', 'file', 'file.pdf', MemoryStream::create())
4848
);
49-
$result = $iso->from('<xop:Include href="cid:foo" xmlns:xop="http://www.w3.org/2004/08/xop/include"/>');
49+
$result = $iso->from('<xop:Include href="cid:foo@x.com" xmlns:xop="http://www.w3.org/2004/08/xop/include"/>');
5050

5151
static::assertSame($attachment, $result);
5252
}

tests/Unit/Multipart/ResponseBuilderTest.php

+10-8
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public function it_can_parse_swa_related_multipart(): void
8181
}
8282

8383
#[Test]
84-
public function it_can_parse_mtom_related_multipart(): void
84+
public function it_can_parse_mtom_related_multipart_with_cid_compliance(): void
8585
{
8686
$attachmentStorage = self::createAttachmentsStore();
8787
$responseFactory = Psr17FactoryDiscovery::findResponseFactory();
@@ -100,14 +100,14 @@ public function it_can_parse_mtom_related_multipart(): void
100100
<SOAP-ENV:Body xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"/>
101101
</SOAP-ENV:Envelope>
102102
--{$boundary}
103-
Content-ID: attachment1
103+
Content-ID: <attachment1@domain.com>
104104
Content-Type: text/plain
105105
Content-Disposition: attachment; name="file1"; filename="attachment1.txt"
106106
Content-Transfer-Encoding: binary
107107
108108
attachment1
109109
--{$boundary}
110-
Content-ID: attachment2
110+
Content-ID: <attachment2@domain.com>
111111
Content-Type: text/plain
112112
Content-Disposition: attachment; name="file2"; filename="attachment2.txt"
113113
Content-Transfer-Encoding: binary
@@ -130,21 +130,23 @@ public function it_can_parse_mtom_related_multipart(): void
130130
EOXML,
131131
(string) $actual->getBody()
132132
);
133-
self::assertResponseAttachments($attachmentStorage);
133+
self::assertResponseAttachments($attachmentStorage, ['<attachment1@domain.com>', '<attachment2@domain.com>']);
134134
}
135135

136-
private static function assertResponseAttachments(AttachmentStorage $storage): void
137-
{
136+
private static function assertResponseAttachments(
137+
AttachmentStorage $storage,
138+
array $expectedIds = ['attachment1', 'attachment2']
139+
): void {
138140
$responseAttachments = [...$storage->responseAttachments()];
139141
static::assertCount(2, $responseAttachments);
140142

141-
static::assertSame('attachment1', $responseAttachments[0]->id);
143+
static::assertSame($expectedIds[0], $responseAttachments[0]->id);
142144
static::assertSame('file1', $responseAttachments[0]->name);
143145
static::assertSame('attachment1.txt', $responseAttachments[0]->filename);
144146
static::assertSame('text/plain', $responseAttachments[0]->mimeType);
145147
static::assertSame('attachment1', $responseAttachments[0]->content->getContents());
146148

147-
static::assertSame('attachment2', $responseAttachments[1]->id);
149+
static::assertSame($expectedIds[1], $responseAttachments[1]->id);
148150
static::assertSame('file2', $responseAttachments[1]->name);
149151
static::assertSame('attachment2.txt', $responseAttachments[1]->filename);
150152
static::assertSame('text/plain', $responseAttachments[1]->mimeType);

0 commit comments

Comments
 (0)