[Bps-public-commit] rt-extension-rest2 branch, master, updated. 1.07-34-g6b9d3a8

? sunnavy sunnavy at bestpractical.com
Fri May 1 13:20:27 EDT 2020


The branch, master has been updated
       via  6b9d3a89e96d6a98750d3775cf1a84f727793853 (commit)
       via  cb41cb698e9dfc505840d7732aa786cdbc754873 (commit)
       via  c00d83344fe6d15ac29e0d5af048e8194be037f0 (commit)
       via  aff286f50f2d20a8815eeac3988c9ed598a1fea5 (commit)
       via  b39d88e316f3f0a313a39f5f63b1d2a599d76dbd (commit)
      from  77e6be3eca589a1d7bcef2adec78854b7a56eae7 (commit)

Summary of changes:
 README                                     | 156 +++++++++++++++++
 lib/RT/Extension/REST2.pm                  | 171 +++++++++++++++++++
 lib/RT/Extension/REST2/Resource/Message.pm |  63 ++++++-
 xt/attachments.t                           | 259 +++++++++++++++++++++++++++++
 xt/data/image.png                          | Bin 0 -> 3929 bytes
 5 files changed, 645 insertions(+), 4 deletions(-)
 create mode 100644 xt/attachments.t
 create mode 100644 xt/data/image.png

- Log -----------------------------------------------------------------
commit b39d88e316f3f0a313a39f5f63b1d2a599d76dbd
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

diff --git a/README b/README
index 801bbe0..df683e1 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 Attachments 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 Attachments
+        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 Attachements property in the JSON object, 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>attachments</em></p>',
+            ContentType => 'text/html',
+            Subject => 'Attachments in JSON Array',
+            Attachments => [
+                {
+                    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 8a61c1f..998bdf5 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -224,6 +224,148 @@ 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<Attachments> 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<Attachments> 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 C<Attachements> property in the JSON object, 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>attachments</em></p>',
+        ContentType => 'text/html',
+        Subject => 'Attachments in JSON Array',
+        Attachments => [
+            {
+                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 720791c..a50c5c8 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 update_custom_fields );
@@ -49,7 +50,20 @@ sub from_json {
     my $self = shift;
     my $body = JSON::decode_json( $self->request->content );
 
-    if (!$body->{ContentType}) {
+    if ($body->{Attachments}) {
+        foreach my $attachment (@{$body->{Attachments}}) {
+            foreach my $field ('FileName', 'FileType', 'FileContent') {
+                return error_as_json(
+                    $self->response,
+                    \400, "$field is a required field for each attachment in Attachments")
+                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");
@@ -65,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{Attachments}}) {
+        $MIME->attach(
+            Type => $attachment->{FileType},
+            Filename => $attachment->{FileName},
+            Data => MIME::Base64::decode_base64($attachment->{FileContent}),
+        );
+    }
+
     my ( $Trans, $msg, $TransObj );
     if ($self->type eq 'correspond') {
         ( $Trans, $msg, $TransObj ) = $self->record->Correspond(

commit aff286f50f2d20a8815eeac3988c9ed598a1fea5
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

diff --git a/xt/attachments.t b/xt/attachments.t
new file mode 100644
index 0000000..966be4d
--- /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 through JSON Base64
+{
+    my $payload = {
+        Content     => 'Have you seen this <b>image</b>',
+        ContentType => 'text/html',
+        Subject     => 'HTML comment with PNG image and text file',
+        Attachments => [
+            {
+                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' : 'base64';
+    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',
+        Attachments => [
+            {
+                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' : 'base64';
+    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 c00d83344fe6d15ac29e0d5af048e8194be037f0
Author: gibus <gibus at easter-eggs.com>
Date:   Sat Oct 20 11:57:52 2018 +0200

    Allow attachments with multipart/form-data

diff --git a/README b/README
index df683e1..9b49e36 100644
--- a/README
+++ b/README
@@ -320,6 +320,35 @@ USAGE
         my $res = $ua->request($req);
         print Dumper($json->decode($res->content)) . "\n";
 
+    Encoding the content of attachments file in 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 POSTing, instead of a JSON
+    request, a 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 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
+    JSON, which, as previously, is a JSON object with Subject, Content,
+    ContentType, TimeTaken properties. Files can then be attached by
+    specifying a field named Attachments for each of them, with the content
+    of the file as value and the appropriate MIME type.
+
+    The curl invocation is quite straightforward:
+
+        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 'Attachments=@/tmp/my_image.png;type=image/png'
+             -F 'Attachments=@/tmp/.bashrc;type=text/plain'
+             -H 'Authorization: token XX_TOKEN_XX'
+                'XX_TICKET_URL_XX'/comment
+
    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 998bdf5..e91273b 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -366,6 +366,35 @@ comment:
     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<Attachments> for each of them, with the content of the file as value and
+the appropriate MIME type.
+
+The curl invocation is quite straightforward:
+
+    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 'Attachments=@/tmp/my_image.png;type=image/png'
+         -F 'Attachments=@/tmp/.bashrc;type=text/plain'
+         -H 'Authorization: token XX_TOKEN_XX'
+            'XX_TICKET_URL_XX'/comment
+
 =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 a50c5c8..9b05b00 100644
--- a/lib/RT/Extension/REST2/Resource/Message.pm
+++ b/lib/RT/Extension/REST2/Resource/Message.pm
@@ -44,11 +44,43 @@ 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('Attachments');
+    foreach my $attachment (@attachments) {
+        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->{Attachments}},
+                {
+                    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->{Attachments}) {
         foreach my $attachment (@{$body->{Attachments}}) {

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

    Add test for attachments with multipart/form-data

diff --git a/xt/attachments.t b/xt/attachments.t
index 966be4d..99e9b51 100644
--- a/xt/attachments.t
+++ b/xt/attachments.t
@@ -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),
+            'Attachments' => [$image_path, $image_name, 'Content-Type' => 'image/png'],
+            'Attachments' => [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' : 'base64';
+    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),
+            'Attachments' => [$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' : 'base64';
+    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;

commit 6b9d3a89e96d6a98750d3775cf1a84f727793853
Merge: 77e6be3 cb41cb6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat May 2 01:13:41 2020 +0800

    Merge branch 'upload-attachments'


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


More information about the Bps-public-commit mailing list