[Bps-public-commit] rt-extension-rest2 branch, rest2_api_attachment_handling, created. 1.07-5-g606e525

Aaron Trevena ast at bestpractical.com
Mon Apr 6 08:31:38 EDT 2020


The branch, rest2_api_attachment_handling has been created
        at  606e5256bcd32f098109b396b088a3d36082b520 (commit)

- Log -----------------------------------------------------------------
commit 551f3c870d3763945fcfa57492116572728a89e0
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Oct 18 13:56:49 2018 +0200

    Allow attachments as JSON Array with Base64 encoded content
    
    Add documentation for attachments as JSON Array with Base64 encoded content
    
    Based on public github PR #15

diff --git a/README b/README
index 40138e3..0624b11 100644
--- a/README
+++ b/README
@@ -193,6 +193,133 @@ USAGE
     You may of course choose to ignore the ETag header and not provide
     If-Match in your requests; RT doesn't require its use.
 
+   Replying/Commenting Tickets
+    You can reply to or comment a ticket by POSTing to _url from the
+    correspond or comment hyperlinks that were returned when fetching the
+    ticket.
+
+        curl -X POST
+             -H "Content-Type: application/json"
+             -d '{
+                  "Subject"    : "response",
+                  "Content"    : "What is your <em>issue</em>?",
+                  "ContentType": "text/html",
+                  "TimeTaken"  : "1"
+                }'
+             -H 'Authorization: token XX_TOKEN_XX'
+                'XX_TICKET_URL_XX'/correspond
+
+    Replying or commenting a ticket is quite similar to a ticket creation:
+    you send a POST request, with data encoded in JSON. The difference lies
+    in the properties of the JSON data object you can pass:
+
+    Subject
+        The subject of your response/comment, optional
+
+    Content
+        The content of your response/comment, mandatory unless there is a
+        non empty AttachmentsContent property to add at least one attachment
+        to the ticket (see "Add Attachments" section below).
+
+    ContentType
+        The MIME content type of your response/comment, typically text/plain
+        or /text/html, mandatory unless there is a non empty
+        AttachmentsContent property to add at least one attachment to the
+        ticket (see "Add Attachments" section below).
+
+    TimeTaken
+        The time, in minutes, you've taken to work on your response/comment,
+        optional.
+
+   Add Attachments
+    You can attach any binary or text file to your response or comment by
+    specifying in the JSON object sent a AttachementsContents property,
+    which should be a JSON array where each item represents a file you want
+    to attach. Each item is a JSON object with the following properties:
+
+    FileName
+        The name of the file to attach to your response/comment, mandatory.
+
+    FileType
+        The MIME type of the file to attach to your response/comment,
+        mandatory.
+
+    FileContent
+        The content, *encoded in MIME Base64* of the file to attach to your
+        response/comment, mandatory.
+
+    The reason why you should encode the content of any file to MIME Base64
+    is that a JSON string value should be a sequence of zero or more Unicode
+    characters. MIME Base64 is a binary-to-text encoding scheme widely used
+    (for eg. by web browser) to send binary data when text data is required.
+    Most popular language have MIME Base64 libraries that you can use to
+    encode the content of your attached files (see MIME::Base64 for Perl).
+    Note that even text files should be MIME Base64 encoded to be passed in
+    the FileContent property.
+
+    Here's a Perl example to send an image and a plain text file attached to
+    a comment:
+
+        #!/usr/bin/perl
+        use strict;
+        use warnings;
+
+        use LWP::UserAgent;
+        use JSON;
+        use MIME::Base64;
+        use Data::Dumper;
+
+        my $url = 'http://rt.local/REST/2.0/ticket/1/comment';
+
+        my $img_path = '/tmp/my_image.png';
+        my $img_content;
+        open my $img_fh, '<', $img_path or die "Cannot read $img_path: $!\n";
+        {
+            local $/;
+            $img_content = <$img_fh>;
+        }
+        close $img_fh;
+        $img_content = MIME::Base64::encode_base64($img_content);
+
+        my $txt_path = '~/.bashrc';
+        my $txt_content;
+        open my $txt_fh, '<', glob($txt_path) or die "Cannot read $txt_path: $!\n";
+        {
+            local $/;
+            $txt_content = <$txt_fh>;
+        }
+        close $txt_fh;
+        $txt_content = MIME::Base64::encode_base64($txt_content);
+
+        my $json = JSON->new->utf8;
+        my $payload = {
+            Content => '<p>I want <b>two</b> <em>attachment</em></p>',
+            ContentType => 'text/html',
+            Subject => 'Attachments in JSON Array',
+            AttachmentsContents => [
+                {
+                    FileName => 'my_image.png',
+                    FileType => 'image/png',
+                    FileContent => $img_content,
+                },
+                {
+                    FileName => '.bashrc',
+                    FileType => 'text/plain',
+                    FileContent => $txt_content,
+                },
+            ],
+        };
+
+        my $req = HTTP::Request->new(POST => $url);
+        $req->header('Authorization' => 'token 6-66-66666666666666666666666666666666');
+        $req->header('Content-Type'  => 'application/json' );
+        $req->header('Accept'        => 'application/json' );
+        $req->content($json->encode($payload));
+
+        my $ua = LWP::UserAgent->new;
+        my $res = $ua->request($req);
+        print Dumper($json->decode($res->content)) . "\n";
+
    Summary
     RT's REST2 API provides the tools you need to build robust and dynamic
     integrations. Tools like ETag/If-Match allow you to avoid conflicts such
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index c4b61c6..27083bc 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -224,6 +224,127 @@ adding time worked can be automatically be recalculated).
 You may of course choose to ignore the C<ETag> header and not provide
 C<If-Match> in your requests; RT doesn't require its use.
 
+=head3 Replying/Commenting Tickets
+
+You can reply to or comment a ticket by C<POST>ing to C<_url> from the C<correspond> or C<comment> hyperlinks that were returned when fetching the ticket.
+
+    curl -X POST
+         -H "Content-Type: application/json"
+         -d '{
+              "Subject"    : "response",
+              "Content"    : "What is your <em>issue</em>?",
+              "ContentType": "text/html",
+              "TimeTaken"  : "1"
+            }'
+         -H 'Authorization: token XX_TOKEN_XX'
+            'XX_TICKET_URL_XX'/correspond
+
+Replying or commenting a ticket is quite similar to a ticket creation: you send a C<POST> request, with data encoded in C<JSON>. The difference lies in the properties of the JSON data object you can pass:
+
+=over 4
+
+=item C<Subject>
+
+The subject of your response/comment, optional
+
+=item C<Content>
+
+The content of your response/comment, mandatory unless there is a non empty C<AttachmentsContent> property to add at least one attachment to the ticket (see L<Add Attachments> section below).
+
+=item C<ContentType>
+
+The MIME content type of your response/comment, typically C<text/plain> or C</text/html>, mandatory unless there is a non empty C<AttachmentsContent> property to add at least one attachment to the ticket (see L<Add Attachments> section below).
+
+=item C<TimeTaken>
+
+The time, in minutes, you've taken to work on your response/comment, optional.
+
+=back
+
+=head3 Add Attachments
+
+You can attach any binary or text file to your response or comment by specifying in the JSON object sent a C<AttachementsContents> property, which should be a JSON array where each item represents a file you want to attach. Each item is a JSON object with the following properties:
+
+=over 4
+
+=item C<FileName>
+
+The name of the file to attach to your response/comment, mandatory.
+
+=item C<FileType>
+
+The MIME type of the file to attach to your response/comment, mandatory.
+
+=item C<FileContent>
+
+The content, I<encoded in C<MIME Base64>> of the file to attach to your response/comment, mandatory.
+
+=back
+
+The reason why you should encode the content of any file to C<MIME Base64> is that a JSON string value should be a sequence of zero or more Unicode characters. C<MIME Base64> is a binary-to-text encoding scheme widely used (for eg. by web browser) to send binary data when text data is required. Most popular language have C<MIME Base64> libraries that you can use to encode the content of your attached files (see L<MIME::Base64> for C<Perl>). Note that even text files should be C<MIME Base64> encoded to be passed in the C<FileContent> property.
+
+Here's a Perl example to send an image and a plain text file attached to a comment:
+
+    #!/usr/bin/perl
+    use strict;
+    use warnings;
+
+    use LWP::UserAgent;
+    use JSON;
+    use MIME::Base64;
+    use Data::Dumper;
+
+    my $url = 'http://rt.local/REST/2.0/ticket/1/comment';
+
+    my $img_path = '/tmp/my_image.png';
+    my $img_content;
+    open my $img_fh, '<', $img_path or die "Cannot read $img_path: $!\n";
+    {
+        local $/;
+        $img_content = <$img_fh>;
+    }
+    close $img_fh;
+    $img_content = MIME::Base64::encode_base64($img_content);
+
+    my $txt_path = '~/.bashrc';
+    my $txt_content;
+    open my $txt_fh, '<', glob($txt_path) or die "Cannot read $txt_path: $!\n";
+    {
+        local $/;
+        $txt_content = <$txt_fh>;
+    }
+    close $txt_fh;
+    $txt_content = MIME::Base64::encode_base64($txt_content);
+
+    my $json = JSON->new->utf8;
+    my $payload = {
+        Content => '<p>I want <b>two</b> <em>attachment</em></p>',
+        ContentType => 'text/html',
+        Subject => 'Attachments in JSON Array',
+        AttachmentsContents => [
+            {
+                FileName => 'my_image.png',
+                FileType => 'image/png',
+                FileContent => $img_content,
+            },
+            {
+                FileName => '.bashrc',
+                FileType => 'text/plain',
+                FileContent => $txt_content,
+            },
+        ],
+    };
+
+    my $req = HTTP::Request->new(POST => $url);
+    $req->header('Authorization' => 'token 6-66-66666666666666666666666666666666');
+    $req->header('Content-Type'  => 'application/json' );
+    $req->header('Accept'        => 'application/json' );
+    $req->content($json->encode($payload));
+
+    my $ua = LWP::UserAgent->new;
+    my $res = $ua->request($req);
+    print Dumper($json->decode($res->content)) . "\n";
+
 =head3 Summary
 
 RT's REST2 API provides the tools you need to build robust and dynamic
diff --git a/lib/RT/Extension/REST2/Resource/Message.pm b/lib/RT/Extension/REST2/Resource/Message.pm
index 57c8ef1..bf4775e 100644
--- a/lib/RT/Extension/REST2/Resource/Message.pm
+++ b/lib/RT/Extension/REST2/Resource/Message.pm
@@ -4,6 +4,7 @@ use warnings;
 
 use Moose;
 use namespace::autoclean;
+use MIME::Base64;
 
 extends 'RT::Extension::REST2::Resource';
 use RT::Extension::REST2::Util qw( error_as_json );
@@ -49,12 +50,26 @@ sub from_json {
     my $self = shift;
     my $body = JSON::decode_json( $self->request->content );
 
-    if (!$body->{ContentType}) {
+    if ($body->{AttachmentsContents}) {
+        foreach my $attachment (@{$body->{AttachmentsContents}}) {
+            foreach my $field ('FileName', 'FileType', 'FileContent') {
+                return error_as_json(
+                    $self->response,
+                    \400, "$field is a required field for each attachment in AttachmentsContents")
+                unless $attachment->{$field};
+            }
+        }
+
+        $body->{NoContent} = 1 unless $body->{Content};
+    }
+
+    if (!$body->{NoContent} && !$body->{ContentType}) {
         return error_as_json(
             $self->response,
             \400, "ContentType is a required field for application/json");
     }
 
+
     $self->add_message(%$body);
 }
 
@@ -64,11 +79,20 @@ sub add_message {
 
     my $MIME = HTML::Mason::Commands::MakeMIMEEntity(
         Interface => 'REST',
-        Body      => $args{Content}     || $self->request->content,
+        $args{NoContent} ? () : (Body => $args{Content} || $self->request->content),
         Type      => $args{ContentType} || $self->request->content_type,
         Subject   => $args{Subject},
     );
 
+    # Process attachments
+    foreach my $attachment (@{$args{AttachmentsContents}}) {
+        $MIME->attach(
+            Type => $attachment->{FileType},
+            Filename => $attachment->{FileName},
+            Data => MIME::Base64::decode_base64($attachment->{FileContent}),
+        );
+    }
+
     my ( $Trans, $msg, $TransObj ) ;
 
     if ($self->type eq 'correspond') {

commit d119220129ab8d7ed858673493cb4b48c9bfccb7
Author: gibus <gibus at easter-eggs.com>
Date:   Thu Oct 18 13:57:39 2018 +0200

    Add test for attachments as JSON Array with Base64 encoded content
    
    Based on public github PR #15

diff --git a/MANIFEST b/MANIFEST
index 7ef88eb..30eb11b 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -68,8 +68,10 @@ README
 TODO
 xt/asset-customfields.t
 xt/assets.t
+xt/attachments.t
 xt/catalogs.t
 xt/conflict.t
+xt/data/image.png
 xt/group-members.t
 xt/not_found.t
 xt/organization.t
diff --git a/xt/attachments.t b/xt/attachments.t
new file mode 100644
index 0000000..68a8e0d
--- /dev/null
+++ b/xt/attachments.t
@@ -0,0 +1,150 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use RT::Extension::REST2::Test tests => undef;
+use Test::Deep;
+use MIME::Base64;
+
+my $mech = RT::Extension::REST2::Test->mech;
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+$user->PrincipalObj->GrantRight(Right => 'CreateTicket');
+$user->PrincipalObj->GrantRight(Right => 'ReplyToTicket');
+$user->PrincipalObj->GrantRight(Right => 'CommentOnTicket');
+$user->PrincipalObj->GrantRight(Right => 'ShowTicket');
+$user->PrincipalObj->GrantRight(Right => 'ShowTicketComments');
+
+my $ticket = RT::Ticket->new($user);
+$ticket->Create(Queue => 'General', Subject => 'hello world');
+my $ticket_id = $ticket->id;
+
+my $image_name = 'image.png';
+my $image_path = RT::Test::get_relocatable_file($image_name, 'data');
+my $image_content;
+open my $fh, '<', $image_path or die "Cannot read $image_path: $!\n";
+{
+    local $/;
+    $image_content = <$fh>;
+}
+close $fh;
+
+$image_content = MIME::Base64::encode_base64($image_content);
+
+# Comment ticket with image and text attachments
+{
+    my $payload = {
+        Content     => 'Have you seen this <b>image</b>',
+        ContentType => 'text/html',
+        Subject     => 'HTML comment with PNG image and text file',
+        AttachmentsContents => [
+            {
+                FileName => $image_name,
+                FileType => 'image/png',
+                FileContent => $image_content,
+            },
+            {
+                FileName => 'password',
+                FileType => 'text/plain',
+                FileContent => MIME::Base64::encode_base64('Hey this is secret!'),
+            },
+        ],
+    };
+    my $res = $mech->post_json("$rest_base_path/ticket/$ticket_id/comment",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    cmp_deeply($mech->json_response, [re(qr/Comments added|Message recorded/)]);
+
+    my $transaction_id = $ticket->Transactions->Last->id;
+    my $attachments = $ticket->Attachments->ItemsArrayRef;
+
+    # 3 attachments + 1 wrapper
+    is(scalar(@$attachments), 4);
+
+    # 1st attachment is wrapper
+    is($attachments->[0]->TransactionId, $transaction_id);
+    is($attachments->[0]->Parent, 0);
+    is($attachments->[0]->Subject, 'HTML comment with PNG image and text file');
+    ok(!$attachments->[0]->Filename);
+    is($attachments->[0]->ContentType, 'multipart/mixed');
+
+    # 2nd attachment is comment's content
+    is($attachments->[1]->Parent, $attachments->[0]->id);
+    is($attachments->[1]->TransactionId, $transaction_id);
+    is($attachments->[1]->ContentType, 'text/html');
+    is($attachments->[1]->ContentEncoding, 'none');
+    is($attachments->[1]->Content, 'Have you seen this <b>image</b>');
+    ok(!$attachments->[1]->Subject);
+
+    # 3rd attachment is image
+    my $expected_encoding = ($RT::Handle->BinarySafeBLOBs) ? 'none' : 'base54';
+    is($attachments->[2]->Parent, $attachments->[0]->id);
+    is($attachments->[2]->TransactionId, $transaction_id);
+    is($attachments->[2]->ContentType, 'image/png');
+    is($attachments->[2]->ContentEncoding, $expected_encoding);
+    is($attachments->[2]->Filename, $image_name);
+    ok(!$attachments->[2]->Subject);
+
+    # 4th attachment is text file
+    is($attachments->[3]->Parent, $attachments->[0]->id);
+    is($attachments->[3]->TransactionId, $transaction_id);
+    is($attachments->[3]->ContentType, 'text/plain');
+    is($attachments->[3]->ContentEncoding, 'none');
+    is($attachments->[3]->Filename, 'password');
+    is($attachments->[3]->Content, 'Hey this is secret!');
+    ok(!$attachments->[3]->Subject);
+}
+
+#Comment ticket with image attachment and no content
+{
+    my $payload = {
+        Subject     => 'No content, just an image',
+        AttachmentsContents => [
+            {
+                FileName => $image_name,
+                FileType => 'image/png',
+                FileContent => $image_content,
+            },
+        ],
+    };
+    my $res = $mech->post_json("$rest_base_path/ticket/$ticket_id/comment",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    cmp_deeply($mech->json_response, [re(qr/Comments added|Message recorded/)]);
+
+    my $transaction_id = $ticket->Transactions->Last->id;
+    my @attachments = grep { $_->TransactionId == $transaction_id } @{$ticket->Attachments->ItemsArrayRef};
+
+    # 2 attachments + 1 wrapper
+    is(scalar(@attachments), 3);
+
+    # 1st attachment is wrapper
+    is($attachments[0]->Parent, 0);
+    is($attachments[0]->Subject, 'No content, just an image');
+    ok(!$attachments[0]->Filename);
+    is($attachments[0]->ContentType, 'multipart/mixed');
+
+    # 2nd attachment is empty comment's content
+    is($attachments[1]->Parent, $attachments[0]->id);
+    is($attachments[1]->TransactionId, $transaction_id);
+    is($attachments[1]->ContentType, 'application/octet-stream');
+    ok(!$attachments[1]->ContentEncoding);
+    ok(!$attachments[1]->Content);
+    ok(!$attachments[1]->Subject);
+
+    # 3rd attachment is image
+    my $expected_encoding = ($RT::Handle->BinarySafeBLOBs) ? 'none' : 'base54';
+    is($attachments[2]->Parent, $attachments[0]->id);
+    is($attachments[2]->TransactionId, $transaction_id);
+    is($attachments[2]->ContentType, 'image/png');
+    is($attachments[2]->ContentEncoding, $expected_encoding);
+    is($attachments[2]->Filename, $image_name);
+    ok(!$attachments[2]->Subject);
+}
+
+done_testing;
diff --git a/xt/data/image.png b/xt/data/image.png
new file mode 100644
index 0000000..8a87374
Binary files /dev/null and b/xt/data/image.png differ

commit 377be7d57052db5021c4d216a47b5441f04592e7
Author: gibus <gibus at easter-eggs.com>
Date:   Sat Oct 20 11:57:52 2018 +0200

    Allow attachments with multipart/form-data
    
    Add documentation for attachments with multipart/form-data
    
    Based on public github PR #16

diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 27083bc..9eb1c94 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -226,7 +226,9 @@ C<If-Match> in your requests; RT doesn't require its use.
 
 =head3 Replying/Commenting Tickets
 
-You can reply to or comment a ticket by C<POST>ing to C<_url> from the C<correspond> or C<comment> hyperlinks that were returned when fetching the ticket.
+You can reply to or comment a ticket by C<POST>ing to C<_url> from the
+C<correspond> or C<comment> hyperlinks that were returned when fetching the
+ticket.
 
     curl -X POST
          -H "Content-Type: application/json"
@@ -239,7 +241,9 @@ You can reply to or comment a ticket by C<POST>ing to C<_url> from the C<corresp
          -H 'Authorization: token XX_TOKEN_XX'
             'XX_TICKET_URL_XX'/correspond
 
-Replying or commenting a ticket is quite similar to a ticket creation: you send a C<POST> request, with data encoded in C<JSON>. The difference lies in the properties of the JSON data object you can pass:
+Replying or commenting a ticket is quite similar to a ticket creation: you send
+a C<POST> request, with data encoded in C<JSON>. The difference lies in the
+properties of the JSON data object you can pass:
 
 =over 4
 
@@ -249,11 +253,16 @@ The subject of your response/comment, optional
 
 =item C<Content>
 
-The content of your response/comment, mandatory unless there is a non empty C<AttachmentsContent> property to add at least one attachment to the ticket (see L<Add Attachments> section below).
+The content of your response/comment, mandatory unless there is a non empty
+C<AttachmentsContent> property to add at least one attachment to the ticket (see
+L<Add Attachments> section below).
 
 =item C<ContentType>
 
-The MIME content type of your response/comment, typically C<text/plain> or C</text/html>, mandatory unless there is a non empty C<AttachmentsContent> property to add at least one attachment to the ticket (see L<Add Attachments> section below).
+The MIME content type of your response/comment, typically C<text/plain> or
+C</text/html>, mandatory unless there is a non empty C<AttachmentsContent>
+property to add at least one attachment to the ticket (see L<Add Attachments>
+section below).
 
 =item C<TimeTaken>
 
@@ -263,7 +272,10 @@ The time, in minutes, you've taken to work on your response/comment, optional.
 
 =head3 Add Attachments
 
-You can attach any binary or text file to your response or comment by specifying in the JSON object sent a C<AttachementsContents> property, which should be a JSON array where each item represents a file you want to attach. Each item is a JSON object with the following properties:
+You can attach any binary or text file to your response or comment by specifying
+in the JSON object sent a C<AttachementsContents> property, which should be a
+JSON array where each item represents a file you want to attach. Each item is
+a JSON object with the following properties:
 
 =over 4
 
@@ -277,13 +289,21 @@ The MIME type of the file to attach to your response/comment, mandatory.
 
 =item C<FileContent>
 
-The content, I<encoded in C<MIME Base64>> of the file to attach to your response/comment, mandatory.
+The content, I<encoded in C<MIME Base64>> of the file to attach to your
+response/comment, mandatory.
 
 =back
 
-The reason why you should encode the content of any file to C<MIME Base64> is that a JSON string value should be a sequence of zero or more Unicode characters. C<MIME Base64> is a binary-to-text encoding scheme widely used (for eg. by web browser) to send binary data when text data is required. Most popular language have C<MIME Base64> libraries that you can use to encode the content of your attached files (see L<MIME::Base64> for C<Perl>). Note that even text files should be C<MIME Base64> encoded to be passed in the C<FileContent> property.
+The reason why you should encode the content of any file to C<MIME Base64> is
+that a JSON string value should be a sequence of zero or more Unicode
+characters. C<MIME Base64> is a binary-to-text encoding scheme widely used (for
+eg. by web browser) to send binary data when text data is required. Most popular
+language have C<MIME Base64> libraries that you can use to encode the content of
+your attached files (see L<MIME::Base64> for C<Perl>). Note that even text files
+should be C<MIME Base64> encoded to be passed in the C<FileContent> property.
 
-Here's a Perl example to send an image and a plain text file attached to a comment:
+Here's a Perl example to send an image and a plain text file attached to a
+comment:
 
     #!/usr/bin/perl
     use strict;
@@ -318,7 +338,7 @@ Here's a Perl example to send an image and a plain text file attached to a comme
 
     my $json = JSON->new->utf8;
     my $payload = {
-        Content => '<p>I want <b>two</b> <em>attachment</em></p>',
+        Content => '<p>I want <b>two</b> <em>attachments</em></p>',
         ContentType => 'text/html',
         Subject => 'Attachments in JSON Array',
         AttachmentsContents => [
@@ -345,6 +365,37 @@ Here's a Perl example to send an image and a plain text file attached to a comme
     my $res = $ua->request($req);
     print Dumper($json->decode($res->content)) . "\n";
 
+Encoding the content of attachments file in C<MIME Base64> has the drawback
+of adding some processing overhead and to increase the sent data size by around
+33%. RT's REST2 API provides another way to attach any binary or text file to your
+response or comment by C<POST>ing, instead of a JSON request, a
+C<multipart/form-data> request. This kind of request is similar to what the browser
+sends when you add attachments in RT's reply or comment form. As its name suggests,
+a C<multipart/form-data> request message contains a series of parts, each representing
+a form field. To reply to or comment a ticket, the request has to include a field
+named C<Json>, which, as previously, is a JSON object with C<Subject>, C<Content>,
+C<ContentType>, C<TimeTaken> properties. Files can then be attached by specifying
+a field named C<Attachment> for each of them, with the content of the file as value
+and the appropriate MIME type.
+
+The curl invocation is quite straith forward:
+
+    curl -X POST
+         -H "Content-Type: multipart/form-data"
+         -F 'Json={
+                    "Subject"    : "Attachments in multipart/form-data",
+                    "Content"    : "<p>I want <b>two</b> <em>attachments</em></p>",
+                    "ContentType": "text/html",
+                    "TimeTaken"  : "1"
+                  };type=application/json'
+         -F 'Attachment_1=@/tmp/my_image.png;type=image/png'
+         -F 'Attachment_2=@~/.bashrc;type=text/plain'
+         -H 'Authorization: token XX_TOKEN_XX'
+            'XX_TICKET_URL_XX'/comment
+
+As a sidenote, fields for attached files can also be named C<attachment_1>,
+C<attachment_2>, etc. since such names were used in RT's REST 1.0 API.
+
 =head3 Summary
 
 RT's REST2 API provides the tools you need to build robust and dynamic
@@ -439,7 +490,8 @@ Below are some examples using the endpoints above.
 
     # Create an Asset
     curl -X POST -H "Content-Type: application/json" -u 'root:password'
-        -d '{"Name" : "Asset From Rest", "Catalog" : "General assets", "Content" : "Some content"}'
+        -d '{"Name" : "Asset From Rest",
+             "Catalog" : "General assets", "Content" : "Some content"}'
         'https://myrt.com/REST/2.0/asset'
 
     # Search Assets
diff --git a/lib/RT/Extension/REST2/Resource/Message.pm b/lib/RT/Extension/REST2/Resource/Message.pm
index bf4775e..d47deda 100644
--- a/lib/RT/Extension/REST2/Resource/Message.pm
+++ b/lib/RT/Extension/REST2/Resource/Message.pm
@@ -44,11 +44,51 @@ sub allowed_methods           { ['POST'] }
 sub charsets_provided         { [ 'utf-8' ] }
 sub default_charset           { 'utf-8' }
 sub content_types_provided    { [ { 'application/json' => sub {} } ] }
-sub content_types_accepted    { [ { 'text/plain' => 'add_message' }, { 'text/html' => 'add_message' }, { 'application/json' => 'from_json' } ] }
+sub content_types_accepted    { [ { 'text/plain' => 'add_message' }, { 'text/html' => 'add_message' }, { 'application/json' => 'from_json' }, { 'multipart/form-data' => 'from_multipart' } ] }
+
+sub from_multipart {
+    my $self = shift;
+    my $json_str = $self->request->parameters->{Json};
+    return error_as_json(
+        $self->response,
+        \400, "Json is a required field for multipart/form-data")
+            unless $json_str;
+
+    my $json = JSON::decode_json($json_str);
+
+    my @attachments = $self->request->upload;
+    if (@attachments && $attachments[0] =~ /^Attachment[_\d]*$/i) {
+        $json->{AttachmentsContents} = ()
+            unless $json->{AttachmentsContents};
+
+        foreach my $attach_field (sort @attachments) {
+            next unless $attach_field =~ /^Attachment[_\d]*$/i;
+
+            my $attachment = $self->request->upload($attach_field);
+            open my $filehandle, '<', $attachment->tempname;
+            if (defined $filehandle && length $filehandle) {
+                my ( @content, $buffer );
+                while ( my $bytesread = read( $filehandle, $buffer, 72*57 ) ) {
+                    push @content, MIME::Base64::encode_base64($buffer);
+                }
+                close $filehandle;
+
+                push @{$json->{AttachmentsContents}},
+                    {
+                        FileName    => $attachment->filename,
+                        FileType    => $attachment->headers->{'content-type'},
+                        FileContent => join("\n", @content),
+                    };
+            }
+        }
+    }
+
+    return $self->from_json($json);
+}
 
 sub from_json {
     my $self = shift;
-    my $body = JSON::decode_json( $self->request->content );
+    my $body = shift || JSON::decode_json( $self->request->content );
 
     if ($body->{AttachmentsContents}) {
         foreach my $attachment (@{$body->{AttachmentsContents}}) {

commit 606e5256bcd32f098109b396b088a3d36082b520
Author: gibus <gibus at easter-eggs.com>
Date:   Sat Oct 20 13:57:18 2018 +0200

    Add test for attachments with multipart/form-data
    
    Based on public github PR #16

diff --git a/xt/attachments.t b/xt/attachments.t
index 68a8e0d..29cb037 100644
--- a/xt/attachments.t
+++ b/xt/attachments.t
@@ -32,7 +32,7 @@ close $fh;
 
 $image_content = MIME::Base64::encode_base64($image_content);
 
-# Comment ticket with image and text attachments
+# Comment ticket with image and text attachments through JSON Base64
 {
     my $payload = {
         Content     => 'Have you seen this <b>image</b>',
@@ -98,7 +98,7 @@ $image_content = MIME::Base64::encode_base64($image_content);
     ok(!$attachments->[3]->Subject);
 }
 
-#Comment ticket with image attachment and no content
+# Comment ticket with image attachment and no content through JSON Base64
 {
     my $payload = {
         Subject     => 'No content, just an image',
@@ -147,4 +147,113 @@ $image_content = MIME::Base64::encode_base64($image_content);
     ok(!$attachments[2]->Subject);
 }
 
+my $json = JSON->new->utf8;
+
+# Comment ticket with image and text attachments through multipart/form-data
+{
+    my $payload = {
+        Content     => 'Have you seen this <b>image</b>',
+        ContentType => 'text/html',
+        Subject     => 'HTML comment with PNG image and text file',
+    };
+
+    $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
+    my $res = $mech->post("$rest_base_path/ticket/$ticket_id/comment",
+        'Authorization' => $auth,
+        'Content_Type'  => 'form-data',
+        'Content'       => [
+            'Json'       => $json->encode($payload),
+            'Attachment_1' => [$image_path, $image_name, 'Content-Type' => 'image/png'],
+            'Attachment_2' => [undef, 'password', 'Content-Type' => 'text/plain', Content => 'Hey this is secret!']]);
+
+    is($res->code, 201);
+    cmp_deeply($mech->json_response, [re(qr/Comments added|Message recorded/)]);
+
+    my $transaction_id = $ticket->Transactions->Last->id;
+    my @attachments = grep { $_->TransactionId == $transaction_id } @{$ticket->Attachments->ItemsArrayRef};
+
+    # 3 attachments + 1 wrapper
+    is(scalar(@attachments), 4);
+
+    # 1st attachment is wrapper
+    is($attachments[0]->TransactionId, $transaction_id);
+    is($attachments[0]->Parent, 0);
+    is($attachments[0]->Subject, 'HTML comment with PNG image and text file');
+    ok(!$attachments[0]->Filename);
+    is($attachments[0]->ContentType, 'multipart/mixed');
+
+    # 2nd attachment is comment's content
+    is($attachments[1]->Parent, $attachments[0]->id);
+    is($attachments[1]->TransactionId, $transaction_id);
+    is($attachments[1]->ContentType, 'text/html');
+    is($attachments[1]->ContentEncoding, 'none');
+    is($attachments[1]->Content, 'Have you seen this <b>image</b>');
+    ok(!$attachments[1]->Subject);
+
+    # 3rd attachment is image
+    my $expected_encoding = ($RT::Handle->BinarySafeBLOBs) ? 'none' : 'base54';
+    is($attachments[2]->Parent, $attachments[0]->id);
+    is($attachments[2]->TransactionId, $transaction_id);
+    is($attachments[2]->ContentType, 'image/png');
+    is($attachments[2]->ContentEncoding, $expected_encoding);
+    is($attachments[2]->Filename, $image_name);
+    ok(!$attachments[2]->Subject);
+
+    # 4th attachment is text file
+    is($attachments[3]->Parent, $attachments[0]->id);
+    is($attachments[3]->TransactionId, $transaction_id);
+    is($attachments[3]->ContentType, 'text/plain');
+    is($attachments[3]->ContentEncoding, 'none');
+    is($attachments[3]->Filename, 'password');
+    is($attachments[3]->Content, 'Hey this is secret!');
+    ok(!$attachments[3]->Subject);
+}
+
+# Comment ticket with image attachment and no content through multipart/form-data
+{
+    my $payload = {
+        Subject     => 'No content, just an image',
+    };
+
+    $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
+    my $res = $mech->post("$rest_base_path/ticket/$ticket_id/comment",
+        'Authorization' => $auth,
+        'Content_Type'  => 'form-data',
+        'Content'       => [
+            'Json'       => $json->encode($payload),
+            'Attachment1' => [$image_path, $image_name, 'Content-Type' => 'image/png']]);
+
+    is($res->code, 201);
+    cmp_deeply($mech->json_response, [re(qr/Comments added|Message recorded/)]);
+
+    my $transaction_id = $ticket->Transactions->Last->id;
+    my @attachments = grep { $_->TransactionId == $transaction_id } @{$ticket->Attachments->ItemsArrayRef};
+
+    # 2 attachments + 1 wrapper
+    is(scalar(@attachments), 3);
+
+    # 1st attachment is wrapper
+    is($attachments[0]->Parent, 0);
+    is($attachments[0]->Subject, 'No content, just an image');
+    ok(!$attachments[0]->Filename);
+    is($attachments[0]->ContentType, 'multipart/mixed');
+
+    # 2nd attachment is empty comment's content
+    is($attachments[1]->Parent, $attachments[0]->id);
+    is($attachments[1]->TransactionId, $transaction_id);
+    is($attachments[1]->ContentType, 'application/octet-stream');
+    ok(!$attachments[1]->ContentEncoding);
+    ok(!$attachments[1]->Content);
+    ok(!$attachments[1]->Subject);
+
+    # 3rd attachment is image
+    my $expected_encoding = ($RT::Handle->BinarySafeBLOBs) ? 'none' : 'base54';
+    is($attachments[2]->Parent, $attachments[0]->id);
+    is($attachments[2]->TransactionId, $transaction_id);
+    is($attachments[2]->ContentType, 'image/png');
+    is($attachments[2]->ContentEncoding, $expected_encoding);
+    is($attachments[2]->Filename, $image_name);
+    ok(!$attachments[2]->Subject);
+}
+
 done_testing;

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list