File Coverage

blib/lib/Business/cXML.pm
Criterion Covered Total %
statement 159 166 95.7
branch 48 56 85.7
condition 10 10 100.0
subroutine 24 24 100.0
pod 9 9 100.0
total 250 265 94.3


line stmt bran cond sub pod time code
1             =encoding utf-8
2              
3             =head1 NAME
4              
5             Business::cXML - Perl implementation of cXML messaging
6              
7             =head1 SYNOPSIS
8              
9             B<Respond to an incoming request:>
10              
11             use Business::cXML;
12              
13             $cxml = new Business::cXML (
14             local => 'https://example.com/our/cxml',
15             handlers => {
16             PunchOutSetup => {
17             __handler => sub { ... },
18             operationAllowed => 'create',
19             },
20             },
21             );
22             # Calls $res->reply_to($req), so to/from/sender are probably OK.
23             $output_xml_string = $cxml->process($input_xml_string);
24             # Send output to requestor...
25              
26             B<Send a request to a server:>
27              
28             use Business::cXML;
29              
30             $cxml = new Business::cXML (
31             remote => 'https://example.com/rest/cxml',
32             secret => 'somesecrettoken',
33             );
34             $req = $cxml->new_request('PunchOutSetup');
35             $req->from(id => '123456', domain => 'DUNS'); # Also sets Sender by default
36             $req->to(id => '654321', domain => 'NetworkId');
37             # Populate request, see Business::cXML::Transmission documentation...
38             $res = $cxml->send($req);
39             # Do something with $res, the Business::cXML::Transmission response received...
40              
41             B<Create a one-way message:>
42              
43             use Business::cXML;
44              
45             $cxml = new Business::cXML;
46             $msg = $cxml->new_message('PunchOutOrder');
47             $msg->from(id => '123456', domain => 'DUNS'); # Also sets Sender by default
48             $msg->to(id => '654321', domain => 'NetworkId');
49             # Populate message, see Business::cXML::Transmission documentation...
50             print $cxml->stringify($msg, url => '...'); # Transmission in cXML-Base64 in an HTML FORM
51              
52             =head1 DESCRIPTION
53              
54             Dispatch incoming HTTP/HTTPS requests and respond to them. Send outgoing
55             requests and parse responses. Prepare and parse one-way messages.
56              
57             As a convention, cXML refers to overall messages as "transmissions" to
58             distinguish from C<Message> type payloads. This library includes native Perl
59             modules for the following transmission types:
60              
61             =over
62              
63             =item * L<Business::cXML::Request::PunchOutSetup> / L<Business::cXML::Response::PunchOutSetup>
64              
65             =item * L<Business::cXML::Message::PunchOutOrder>
66              
67             =item * Planned: Request::Order / Response::Order
68              
69             =back
70              
71             Specifically B<NOT> implemented are:
72              
73             =over
74              
75             =item * Attachments
76              
77             =item * Cryptographic signatures
78              
79             =item * Requesting the remote side's capabilities profile and restricting ourselves to it
80              
81             =back
82              
83             =head2 Motivation & Future Development
84              
85             While the above may implement a relatively small portion of the whole cXML
86             specification, which is designed to describe every business-to-business
87             transaction imaginable world-wide, it does fully satisfy our need (see
88             L</ACKNOWLEDGEMENTS>) to act as a "punch-out" supplier to our
89             Ariba/PeopleSoft/SAP/etc corporate clients.
90              
91             The design is completely modular (see L<Business::cXML::Object>) and uses
92             L<XML::LibXML> under the hood, to help simplify future efforts to cover more
93             of the standard.
94              
95             =head1 METHODS
96              
97             =over
98              
99             =cut
100              
101 7     7   957629 use 5.014;
  7         70  
102 7     7   33 use strict;
  7         15  
  7         213  
103              
104             use base 'Exporter';
105 7     7   38  
  7         11  
  7         607  
106             use Scalar::Util 'blessed';
107 7     7   37 use Business::cXML::Transmission;
  7         9  
  7         339  
108 7     7   2423  
  7         20  
  7         439  
109             BEGIN {
110             our $VERSION = 'v0.6.8';
111 7     7   89 our $CXML_VERSION = '1.2.036';
112 7         13 our $USERAGENT = "Business::cXML.pm $VERSION";
113 7         300 }
114              
115             our @EXPORT = qw(CXML_LOG_NOTHING CXML_LOG_ERR CXML_LOG_ERROR CXML_LOG_WARN CXML_LOG_WARNING CXML_LOG_INFO CXML_LOG_DEBUG, CXML_LOG_TRACE);
116              
117             use constant {
118             CXML_LOG_NOTHING => 0,
119 7         9509 CXML_LOG_ERR => 1,
120             CXML_LOG_ERROR => 1,
121             CXML_LOG_WARN => 2,
122             CXML_LOG_WARNING => 2,
123             CXML_LOG_INFO => 3,
124             CXML_LOG_DEBUG => 4,
125             CXML_LOG_TRACE => 5,
126             };
127 7     7   43  
  7         12  
128             =item C<B<new>( I<%options> )>
129              
130             Returns a fresh cXML handler.
131              
132             Options useful to send requests and receive responses:
133              
134             =over
135              
136             =item C<B<remote>>
137              
138             HTTP/HTTPS URL where to POST requests
139              
140             =back
141              
142             Options useful to process requests and emit responses:
143              
144             =over
145              
146             =item C<B<local>>
147              
148             HTTP/HTTPS URL to publish where clients can reach this handler
149              
150             =item C<B<secret>>
151              
152             Secret keyword expected by remote server
153              
154             =item C<B<sender_callback>>
155              
156             Subroutine, passed to L</sender_callback()>.
157              
158             =item B<log_level>
159              
160             One of: C<CXML_LOG_NOTHING> (default), C<CXML_LOG_ERR>, C<CXML_LOG_WARN>,
161             C<CXML_LOG_INFO>, C<CXML_LOG_DEBUG>, C<CXML_LOG_TRACE>. Alternates
162             C<CXML_LOG_ERROR> and C<CXML_LOG_WARNING> are also available.
163              
164             =item B<log_callback>
165              
166             Subroutine, passed to L</log_callback()>.
167              
168             =item B<handlers>
169              
170             Hash of handlers, dereferenced and passed to L</on()>.
171              
172             =back
173              
174             =cut
175              
176             my ($class, %options) = @_;
177              
178 14     14 1 35135 my $self = {
179             local => ($options{local} || ''),
180             remote => ($options{remote} || undef),
181             secret => ($options{secret} || undef),
182             sender_callback => undef,
183             log_level => ($options{log_level} || CXML_LOG_NOTHING),
184             log_callback => ($options{log_callback} || \&_log_default),
185             routes => {
186 14   100     245 Profile => {
      100        
      100        
      100        
      100        
187             __handler => \&_handle_profile,
188             },
189             },
190             };
191             bless $self, $class;
192             $self->sender_callback($options{sender_callback}) if exists $options{sender_callback};
193 14         31 $self->on(%{ $options{handlers} }) if exists $options{handlers};
194 14 100       43 return $self;
195 14 100       44 }
  4         19  
196 14         83  
197             my ($self, $req, $res) = @_;
198              
199             $res->status(200);
200 2     2   7  
201             my $data = $res->xml_payload;
202 2         6  
203             $data->attr(effectiveDate => $res->timestamp);
204 2         12 # UNIMPLEMENTED: lastRefresh?
205              
206 2         8 # UNIMPLEMENTED: service-level (outside Transaction blocks) options: service, attachments, changes, requestNames
207             # Possibly also: Locale (found in an example, but not in any documentation)
208             # There was no documentation about these in the cXML 1.2.036 PDF manual nor in the DTD comments.
209             foreach my $route (keys (%{ $self->{routes} })) {
210             my $tx = $data->add('Transaction', undef, requestName => ($route . 'Request'));
211             $tx->add(URL => $self->{local});
212 2         55 foreach my $opt (grep { $_ ne '__handler' } keys (%{ $self->{routes}{$route} })) {
  2         12  
213 3         71 $tx->add('Option', $self->{routes}{$route}{$opt}, name => $opt);
214 3         181 };
215 3         109 };
  4         15  
  3         30  
216 1         4 }
217              
218              
219             =item C<B<sender_callback>( I<$sub> )>
220              
221             By default, a request's From/Sender credentials are only used to guess
222             response credentials. If you specify a callback here, it will be invoked
223             immediately after XML parsing, before passing to transaction handlers, giving
224             you an opportunity to authenticate the caller.
225              
226             Your subroutine will be passed 3 arguments:
227              
228             =over
229              
230             =item 1. The current L<Business::cXML> object
231              
232             =item 2. The Sender L<Business::cXML::Credential> object
233              
234             =item 3. The From L<Business::cXML::Credential> object
235              
236             =back
237              
238             If you return a false value, the request will be deemed unauthorized and no
239             handler will be invoked.
240              
241             If you return anything else, it will be stored and available in the request
242             sender's C<_note> property. (See L<Business::cXML::Credential> for details.)
243              
244             Note that cXML From/Sender headers contain information about an entire company
245             you're doing business with. The identity of the specific person triggering
246             the request, if applicable, will be somewhere in contact or extrinsic data in
247             the payload itself.
248              
249             =cut
250              
251             my ($self, $callback) = @_;
252             $self->{sender_callback} = $callback if ref($callback) eq 'CODE';
253             }
254              
255 3     3 1 46125 =item C<B<log_callback>( I<$sub> )>
256 3 100       16  
257             By default, basic details about log-worthy events are dumped to C<STDERR>
258             (filtered according to the current log level). By specifying your own
259             handler, you can do anything else you'd like when informative events occur.
260              
261             Your subroutine will be passed 5 arguments:
262              
263             =over
264              
265             =item 1. The current L<Business::cXML> object
266              
267             =item 2. The level
268              
269             =over
270              
271             =item CXML_LOG_ERR = 1 = fatal error (on our side)
272              
273             =item CXML_LOG_WARN = 2 = warning (errors on the other end, network issues, etc.)
274              
275             =item CXML_LOG_INFO = 3 = normal operations like receiving or sending transmissions
276              
277             =item CXML_LOG_DEBUG = 4 = additional debugging information about processing requests
278              
279             =item CXML_LOG_TRACE = 5 = full trace logging in some areas
280              
281             =back
282              
283             =item 3. A possible long-form message describing the event
284              
285             =item 4. A possible cXML transmission string (untouched if input)
286              
287             =item 5. A possible L<Business::cXML::Transmission> object
288              
289             =back
290              
291             Successful parsing of a new transmission triggers a level 3 log, whereas
292             failure is a level 2. Failure to produce valid XML from our internal data
293             (which should never occur) is a level 1.
294              
295             NOTE: Logging is limited to this module. Thus, be sure to use L<process()>,
296             L<send()> and L<stringify()> to trap interesting events in the handling of
297             C<Request>, C<Response> and C<Message> transmissions.
298              
299             =cut
300              
301             my ($cxml, $level, $desc, $xml, $tx) = @_;
302             return unless $level <= $cxml->{log_level};
303             $level = ('error', 'warning', 'info', 'debug', 'trace')[$level-1];
304             # use Data::Dumper;
305             # print STDERR "cXML[$level]: ", $desc, " -- ", $xml, " -- ", Dumper($cxml), "\n";
306 61     61   177 $xml =~ s/>\s+</> </g;
307 61 100       200 print STDERR "cXML[$level]: ", $desc, " -- ", $xml, "\n";
308 7         20 }
309              
310             my ($self, $callback) = @_;
311 7         92 $self->{log_callback} = $callback;
312 7         697 }
313              
314              
315             =item C<B<on>( I<%handlers> )>
316 1     1 1 3176  
317 1         3 Each key in I<C<%handlers>> is the bare name of a supported transaction type.
318             For example, if you want to support C<PunchOutSetupRequest>, the key is
319             C<PunchOutSetup>. Each key should point to a hashref specifying any options
320 10     10   379 to declare in our C<Profile>.
  10         45  
  10         28  
321 7     7   13  
  7         26  
  7         43  
322 26     26   43 Special key C<__handler> is mandatory and should point to a sub which will
  26         86  
  26         51  
323 14     14   22 be called if its type of request is received, valid and optionally
  14         39  
  14         24  
324 4     4   10 authenticated. Your handler will be passed 3 arguments:
  4         21  
  4         7  
325              
326             =over
327              
328             =item 1. The current L<Business::cXML> object
329              
330             =item 2. The L<Business::cXML::Transmission> request
331              
332             =item 3. A ready-to-fill L<Business::cXML::Transmission> response
333              
334             =back
335              
336             Be sure to change the response's status, which is a 500 error by default.
337              
338             The response's to/from/sender is initialized in reciprocity to the request's,
339             so your handler might not need to change much in those.
340              
341             Keys represent the case-sensitive name of the cXML request without the
342             redundant suffix. For example, a C<PunchOutSetupRequest> is our type
343             C<PunchOutSetup>. Possible keys include: C<Order>, C<PunchOutSetup>,
344             C<StatusUpdate>, C<GetPending>, C<Confirmation>, C<ShipNotice>,
345             C<ProviderSetup>, C<PaymentRemittance>.
346              
347             Pings and C<Profile> requests are built-in, although you can override the
348             handling of the latter with your own handler if you'd like.
349              
350             =cut
351              
352             my ($self, %routes) = @_;
353              
354             foreach (keys %routes) {
355             $self->{routes}{$_} = $routes{$_};
356             };
357             }
358              
359             =item C<B<process>( [I<$input>] )>
360              
361             Decodes the I<C<$input>> XML string which is expected to be a complete cXML
362             transmission. May invoke one of your handlers declared with L<on()> if
363             appropriate.
364 9     9 1 12064  
365             Returns a string containing a valid cXML document at all times. This document
366 9         30 includes any relevant status information determined during processing.
367 9         30  
368             Note that an omitted or empty I<C<$input>> is actually valid and results in a
369             "pong" response.
370              
371             =cut
372              
373             my ($self, $input) = @_;
374             my $err;
375             my $str;
376             my $res = new Business::cXML::Transmission;
377             $res->is_response(1);
378              
379             unless ($input) {
380             $self->_notice("process(10) ping-pong");
381             $res->status(200, "Pong!");
382             ($err, $str) = $res->toString;
383             $self->_error("process(12) $err", $str) if $err;
384             $self->_trace("process(15) ping-pong returning", $str);
385             return $str;
386 17     17 1 62154 };
387 17         48  
388             my $req = new Business::cXML::Transmission $input;
389 17         114  
390 17         72 unless (defined blessed($req)) {
391             # We have an error status code
392 17 100       64 my $desc = "XML validation failure:\n" . $req->[1];
393 1         4 $self->_warning("process(21) $desc", $input);
394 1         4 $res->status($req->[0], $desc);
395 1         3 ($err, $str) = $res->toString;
396 1 50       82 $self->_error("process(22) $err", $str) if $err;
397 1         4 $self->_trace("process(25) error returning", $str);
398 1         10 return $str;
399             };
400              
401 16         51 $res->status(500, "Handler did not set a response status.");
402             $res->reply_to($req);
403 16 100       666 $res->payload; # Trigger creation of payload now that it has a class/type.
404              
405 1         5 if (defined $self->{sender_callback}) {
406 1         118 $self->_trace("process(30) sender_callback...");
407 1         8 my $note;
408 1         6 eval {
409 1 50       68 $note = $self->{sender_callback}->($self, $req->sender, $req->from);
410 1         5 };
411 1         15 if ($@) {
412             $self->_error("process(31) sender_callback crashed!!!", $@);
413             $res->status(500, "Sender validation encountered an irrecoverable error.");
414 15         61 ($err, $str) = $res->toString;
415 15         72 $self->_error("process(32) $err", $str) if $err;
416 15         73 return $str;
417             };
418 15 100       67 if ($note) {
419 2         8 $self->_debug("process(33) sender_callback successful");
420 2         3 $req->sender->_note($note);
421 2         4 } else {
422 2         7 $self->_warning("process(34) sender validation failed", $input, $req);
423             $res->status(401, "Invalid sender.");
424 2 50       14 ($err, $str) = $res->toString;
425 0         0 $self->_error("process(35) $err", $str) if $err;
426 0         0 return $str;
427 0         0 };
428 0 0       0 };
429 0         0  
430             unless (exists $self->{routes}{$req->type}) {
431 2 100       5 my $desc = "Type '" . $req->type . "' is not implemented.";
432 1         4 $self->_warning("process(41) $desc", $input, $req);
433 1         4 $res->status(450, $desc);
434             ($err, $str) = $res->toString;
435 1         5 $self->_error("process(45) $err", $str) if $err;
436 1         4 return $str;
437 1         4 };
438 1 50       63 $self->_notice("process() received request", $input, $req);
439 1         6 eval {
440             $self->{routes}{$req->type}{__handler}->($self, $req, $res);
441             };
442             if ($@) {
443 14 100       49 $self->_error("process(50) handler for type '" . $req->type . "' crashed!!!", $@);
444 1         6 $res->status(500, "Processing for type '" . $req->type . "' encountered an irrecoverable error.");
445 1         6 };
446 1         3  
447 1         5 ($err, $str) = $res->toString;
448 1 50       84 $self->_error("process(51): $err", $str, $res) if $err;
449 1         31  
450             if ($res->status >= 500) {
451 13         59 $self->_error("process(55) responding with 5xx", $str, $res);
452 13         21 } elsif ($res->status >= 400) {
453 13         55 $self->_warning("process(54) responding with 4xx", $str, $res);
454             } else {
455 13 50       26004 $self->_notice("process(52) responding with 2xx", $str, $res);
456 0         0 };
457 0         0  
458             $self->_debug("process(60) returning", $str);
459             return $str;
460 13         62 }
461 13 100       991  
462             =item C<B<new_request>( [I<$type>] )>
463 13 100       49  
    100          
464 7         27 Returns a fresh L<Business::cXML::Transmission> ready to be used as a request.
465             Optional I<C<$type>> is a convenience shortcut to
466 1         5 L<Business::cXML::Transmission::type()|cXML::Transmission/type>. The
467             request's sender secret will be pre-filled.
468 5         19  
469             =cut
470              
471 13         54 my ($self, $type) = @_;
472 13         183 my $req = new Business::cXML::Transmission;
473             $req->is_request(1);
474             $req->type($type) if defined $type;
475             $req->sender->secret($self->{secret}) if defined $self->{secret};
476             return $req;
477             }
478              
479             =item C<B<send>( I<$request> )>
480              
481             Freeze I<C<$request>>, a L<Business::cXML::Transmission>, and attempt sending
482             it to the configured remote server. Returns the received response
483             L<Business::cXML::Transmission> on success, C<undef> on failure. Note that as
484             per L<Business::cXML::Transmission::new()|Business::cXML::Transmission/new>,
485 2     2 1 50289 it is also possible that an error arrayref be returned instead of a
486 2         14 transmission if parsing failed.
487 2         18  
488 2 100       13 In case of failure, you may want to wait a certain amount of time and try
489 2 100       26 again. To give you more options to that effect, I<C<$request>> can be either
490 2         37 a L<Business::cXML::Transmission> or a string.
491              
492             =cut
493              
494             use LWP::UserAgent;
495             my ($self, $req) = @_;
496             my $err;
497             my $obj;
498              
499             if (ref($req)) {
500             $obj = $req;
501             ($err, $req) = $req->freeze();
502             return $self->_error("send(11): $err", $req, $obj) if defined $err;
503             };
504             $self->_notice("send() making HTTP request", $req, $obj);
505              
506             my $ua = new LWP::UserAgent;
507             $ua->agent($Business::cXML::USERAGENT);
508 7     7   3118 $ua->timeout(30);
  7         185248  
  7         2217  
509             my $res = $ua->post(
510 6     6 1 128125 $self->{remote},
511 6         11 'Content-Type' => 'text/xml; charset="UTF-8"',
512             'Content' => $req,
513             );
514 6 100       18 if ($res->is_success) {
515 5         10 $res = $res->decoded_content;
516 5         31 my $msg = new Business::cXML::Transmission $res;
517 5 100       20 unless (defined blessed($msg)) {
518             # We have an error status code
519 5         17 return $self->_warning('send(21) ' . ($msg->[0] == 406 ? 'response XML validation' : 'response cXML traversal') . ' failure', $res);
520             };
521 5         31 $self->_notice("send() received HTTP response", $res, $msg);
522 5         48 return $msg;
523 5         69 } else {
524             return $self->_warning("send(22) had network failure", $req, $obj);
525             };
526 5         46 }
527              
528             =item C<B<new_message>( [I<$type>] )>
529 5 100       39  
530 4         23 Returns a fresh L<Business::cXML::Transmission> ready to be used as a
531 4         45 stand-alone message. Optional I<C<$type>> is passed on to
532 4 100       1620 L<Business::cXML::Transmission::type()|Business::cXML::Transmission/type>.
533              
534 2 100       19 =cut
535              
536 2         7 my ($self, $type) = @_;
537 2         334 my $msg = new Business::cXML::Transmission;
538             $msg->is_message(1);
539 1         10 $msg->type($type) if defined $type;
540             return $msg;
541             }
542              
543             =item C<B<stringify>( I<$message>, I<%args> )>
544              
545             Convenience wrapper around
546             L<Business::cXML::Transmission::toForm()|Business::cXML::Transmission/toForm>
547             which allows you to trap logging events.
548              
549             =cut
550              
551             my ($self, $msg, %args) = @_;
552 3     3 1 126900 my ($err, $str) = $msg->toForm(%args);
553 3         19 $self->_error("stringify(): $err", $str, $msg) if defined $err;
554 3         16 return $str;
555 3 100       16 }
556 3         12  
557             =back
558              
559             =head1 VERSION
560              
561             0.6.8 based on cXML DTD 1.2.036
562              
563             =head1 AUTHOR
564              
565             Stéphane Lavergne L<https://github.com/vphantom>
566              
567             =head1 ACKNOWLEDGEMENTS
568 6     6 1 153414  
569 6         24 Graph X Design Inc. L<https://www.gxd.ca/> sponsored this project.
570 6 100       23  
571 6         114 =head1 COPYRIGHT & LICENSE
572              
573             Copyright (c) 2017-2018 Stéphane Lavergne L<https://github.com/vphantom>
574              
575             Permission is hereby granted, free of charge, to any person obtaining a copy
576             of this software and associated documentation files (the "Software"), to deal
577             in the Software without restriction, including without limitation the rights
578             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
579             copies of the Software, and to permit persons to whom the Software is
580             furnished to do so, subject to the following conditions:
581              
582             The above copyright notice and this permission notice shall be included in all
583             copies or substantial portions of the Software.
584              
585             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
586             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
587             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
588             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
589             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
590             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
591             SOFTWARE.
592             =cut
593              
594             1;