File Coverage

blib/lib/WebService/Megaplan.pm
Criterion Covered Total %
statement 33 104 31.7
branch 0 50 0.0
condition 0 10 0.0
subroutine 11 16 68.7
pod 4 4 100.0
total 48 184 26.0


line stmt bran cond sub pod time code
1             package WebService::Megaplan;
2              
3 4     4   140180 use 5.006;
  4         16  
  4         199  
4 4     4   25 use strict;
  4         16  
  4         180  
5 4     4   21 use warnings FATAL => 'all';
  4         12  
  4         208  
6              
7 4     4   22 use base qw(Class::Accessor);
  4         9  
  4         4787  
8             __PACKAGE__->mk_accessors(qw(login password hostname port use_ssl secret_key access_id http));
9              
10 4     4   9513 use Digest::MD5 qw(md5_hex);
  4         9  
  4         297  
11 4     4   3731 use Digest::HMAC_SHA1 qw(hmac_sha1_hex);
  4         41092  
  4         233  
12 4     4   4653 use JSON qw(from_json);
  4         79642  
  4         29  
13 4     4   6875 use HTTP::Tiny ();
  4         272983  
  4         142  
14 4     4   4064 use MIME::Base64 qw(encode_base64);
  4         3195  
  4         295  
15 4     4   69 use POSIX ();
  4         7  
  4         101  
16              
17             use constant {
18 4         4956 AUTHORIZE_URL => '/BumsCommonApiV01/User/authorize.api',
19 4     4   22 };
  4         6  
20              
21             =head1 NAME
22              
23             WebService::Megaplan - The API for Megaplan.ru service (Web-based business automatization service)
24              
25             =head1 VERSION
26              
27             Version 0.03
28              
29             =cut
30              
31             our $VERSION = '0.03';
32              
33              
34             =head1 SYNOPSIS
35              
36             Module allows to call Megaplan API using Perl
37              
38             See API details on http://wiki.megaplan.ru/API (Russian only)
39              
40             Currently implemented only low-level API where you have to provide URI of API calls.
41              
42             use WebService::Megaplan;
43              
44             my $api = WebService::Megaplan->new(
45             login => 'robot_user',
46             password => 'xxxxxx',
47             hostname => 'mycompany.megaplan.ru',
48             use_ssl => 1,
49             );
50             my $employee_id = $api->authorize();
51              
52             # get list of tasks
53             my $data = $api->get_data('/BumsTaskApiV01/Task/list.api', { OnlyActual => 'true' });
54             my $task_list = $data->{data}->{tasks};
55              
56             # create new task
57             my $task_reply = $api->post_data('/BumsTaskApiV01/Task/create.api', {
58             'Model[Name]' => 'Test title',
59             'Model[SuperTask]' => 'p1000001',
60             'Model[Statement]' => 'Task long description',
61             'Model[Responsible]' => $employee_id, # number like 1000020
62             });
63             printf "Created task #%d\n", $task_reply->{data}->{task}->{Id};
64              
65             =head1 METHODS
66              
67             =head2 new(%opt)
68              
69             Create new API object, providing a hash of options:
70              
71             =over 2
72              
73             =item login -- login
74              
75             =item password -- password
76              
77             =item hostname -- hostname of installed Megaplan, usually something like 'somename.megaplan.ru'
78              
79             =item port -- port to use to connect Megaplan, not required if default (80 http, 443 https)
80              
81             =item use_ssl -- 0 or 1, using SSL is recommended
82              
83             =back
84              
85             =cut
86              
87             sub new {
88 0     0 1   my($class, %opts) = @_;
89              
90 0 0         die "No login specified" if(! $opts{login});
91 0 0         die "No password specified" if(! $opts{password});
92 0 0         die "No hostname specified" if(! $opts{hostname});
93              
94 0   0       $opts{use_ssl} ||= 0;
95              
96 0           my $http = HTTP::Tiny->new();
97 0           $opts{http} = $http;
98              
99 0           return bless \%opts, $class;
100             }
101              
102             =head2 authorize
103              
104             Authenticate itself on Megaplan server and obtain AccessId and SecretKey values.
105              
106             Returns true value on success (ID of logged in Employee). This method have to be called before any other API calls.
107              
108             =cut
109              
110             sub authorize {
111 0     0 1   my $self = shift;
112              
113 0           my $params = $self->http->www_form_urlencode({
114             Login => $self->login,
115             Password => md5_hex($self->password),
116             });
117 0 0         my $url = ($self->use_ssl ? 'https' : 'http')
    0          
118             . '://'
119             . $self->hostname
120             . ($self->port ? ':' . $self->port : '')
121             . AUTHORIZE_URL
122             . '?'
123             . $params;
124             #printf STDERR "GET %s\n", $url;
125              
126 0           my $response = $self->http->get($url);
127 0 0         die 'No response from server' if(! $response);
128 0 0         if(! $response->{success}) {
129 0           die sprintf('Login failed: %03d %s', $response->{status}, $response->{reason});
130             }
131              
132 0           my $data = from_json($response->{content});
133              
134 0 0         if($data->{status}->{code} ne 'ok') {
135 0           die sprintf('Login failed: %s', $data->{status}->{message});
136             }
137              
138 0           my $secret = $data->{data}->{SecretKey};
139 0           my $access_id = $data->{data}->{AccessId};
140              
141 0           $self->secret_key($secret);
142 0           $self->access_id($access_id);
143              
144             # also there are 'UserId' value
145 0           return $data->{data}->{EmployeeId};
146             }
147              
148             =head2 get_data(uri_path, params)
149              
150             Low-level method to perform GET query to corresponding API method
151              
152             =over 2
153              
154             =item uri_path -- URI, for example '/BumsTaskApiV01/Task/list.api'
155              
156             =item params -- hash-reference of API call arguments (optional)
157              
158             =back
159              
160             Returns perl data, converted from resulted JSON. died in case of errors.
161              
162             =cut
163              
164             sub get_data {
165 0     0 1   my ($self, $uri_path, $params) = @_;
166              
167 0   0       $params ||= {};
168              
169 0 0         $self->authorize() if(! $self->secret_key);
170 0 0         die "No secret key, failed login?" if(! $self->secret_key);
171              
172 0           my ($signature, $date) = $self->_make_signature(
173             method => 'GET',
174             content => '',
175             uri_path => $uri_path,
176             query_params => $params,
177             );
178              
179 0           my $query_string = $self->http->www_form_urlencode($params);
180 0 0         my $url = ($self->use_ssl ? 'https' : 'http')
    0          
181             . '://'
182             . $self->hostname
183             . ($self->port ? ':' . $self->port : '')
184             . $uri_path;
185 0 0         if($query_string) {
186 0           $url .= '?' . $query_string;
187             }
188              
189             #printf STDERR "GET %s\n", $url;
190              
191 0           my $response = $self->http->get($url, {
192             headers => {
193             Date => $date,
194             'X-Sdf-Date' => $date,
195             Accept => 'application/json',
196             'X-Authorization' => join(':', $self->access_id, $signature),
197             },
198             });
199              
200 0 0         die 'No response from server' if(! $response);
201 0 0         if(! $response->{success}) {
202 0           die sprintf('GET failed: %03d %s', $response->{status}, $response->{reason});
203             }
204              
205 0           my $data = from_json($response->{content});
206              
207 0 0         if($data->{status}->{code} ne 'ok') {
208 0           die sprintf('GET failed: %s', $data->{status}->{message});
209             }
210              
211 0           return $data;
212             }
213              
214             =head2 post_data(uri_path, params)
215              
216             Low-level method to perform POST request to API - to create new objects or update existing ones
217              
218             =over 2
219              
220             =item uri_path -- URI, for example '/BumsCommonApiV01/Comment/create.api'
221              
222             =item params -- hash-reference of API call arguments
223              
224             =back
225              
226             Returns perl data, converted from resulted JSON. died in case of errors.
227              
228             =cut
229              
230             sub post_data {
231 0     0 1   my ($self, $uri_path, $params) = @_;
232              
233             # it's unlikely that $params is empty
234              
235 0 0         $self->authorize() if(! $self->secret_key);
236 0 0         die "No secret key, failed login?" if(! $self->secret_key);
237              
238 0           my $content = $self->http->www_form_urlencode($params);
239              
240 0           my ($signature, $date) = $self->_make_signature(
241             method => 'POST',
242             content => $content,
243             uri_path => $uri_path
244             );
245 0 0         my $url = ($self->use_ssl ? 'https' : 'http')
    0          
246             . '://'
247             . $self->hostname
248             . ($self->port ? ':' . $self->port : '')
249             . $uri_path;
250              
251 0           my $response = $self->http->post_form($url, $params, {
252             headers => {
253             Date => $date,
254             'X-Sdf-Date' => $date,
255             Accept => 'application/json',
256             'X-Authorization' => join(':', $self->access_id, $signature),
257             'Content-MD5' => md5_hex($content),
258             },
259             });
260              
261 0 0         die 'No response from server' if(! $response);
262 0 0         if(! $response->{success}) {
263 0           die sprintf('POST failed: %03d %s', $response->{status}, $response->{reason});
264             }
265              
266 0           my $data = from_json($response->{content});
267              
268 0 0         if($data->{status}->{code} ne 'ok') {
269 0           die sprintf('POST failed: %s', $data->{status}->{message});
270             }
271              
272 0           return $data;
273             }
274              
275             #-------------- private
276             sub _make_signature {
277 0     0     my ($self, %opts) = @_;
278              
279             # method, content_md5, content_type, date, url
280 0           my @fields = ($opts{method});
281 0 0         if($opts{content}) {
282 0           push @fields,
283             md5_hex($opts{content}),
284             'application/x-www-form-urlencoded';
285             }
286             else {
287 0           push @fields, '', '';
288             }
289              
290 0           my $old_locale = POSIX::setlocale(&POSIX::LC_TIME, 'C');
291 0           my $date = POSIX::strftime('%a, %d %b %Y %H:%M:%S %z', localtime);
292 0           push @fields, $date;
293 0           POSIX::setlocale(&POSIX::LC_TIME, $old_locale);
294              
295             # I think that port should not be included here, but never tested
296 0           my $url = $self->hostname . $opts{uri_path};
297 0 0 0       if( ($opts{method} eq 'GET') && $opts{query_params} && scalar(keys %{ $opts{query_params} }) > 0) {
  0   0        
298 0           my $query_string = $self->http->www_form_urlencode($opts{query_params});
299 0           $url .= '?' . $query_string;
300             }
301 0           push @fields, $url;
302              
303             #printf STDERR "Signature for:\n%s\n", join("\n", @fields);
304              
305 0           my $signature = encode_base64( hmac_sha1_hex(join("\n", @fields), $self->secret_key), '');
306              
307 0           return ($signature, $date);
308             }
309              
310              
311             =head1 AUTHOR
312              
313             Sergey Leschenko, C<< >>
314              
315             =head1 BUGS
316              
317             Please report any bugs or feature requests to C, or through
318             the web interface at L. I will be notified, and then you'll
319             automatically be notified of progress on your bug as I make changes.
320              
321              
322              
323              
324             =head1 SUPPORT
325              
326             You can find documentation for this module with the perldoc command.
327              
328             perldoc WebService::Megaplan
329              
330              
331             You can also look for information at:
332              
333             =over 4
334              
335             =item * Megaplan API (Russian only)
336              
337             L
338              
339             =item * RT: CPAN's request tracker (report bugs here)
340              
341             L
342              
343             =item * AnnoCPAN: Annotated CPAN documentation
344              
345             L
346              
347             =item * CPAN Ratings
348              
349             L
350              
351             =item * Search CPAN
352              
353             L
354              
355             =back
356              
357              
358             =head1 ACKNOWLEDGEMENTS
359              
360              
361             =head1 LICENSE AND COPYRIGHT
362              
363             Copyright 2013 Sergey Leschenko.
364              
365             This program is free software; you can redistribute it and/or modify it
366             under the terms of the the Artistic License (2.0). You may obtain a
367             copy of the full license at:
368              
369             L
370              
371             Any use, modification, and distribution of the Standard or Modified
372             Versions is governed by this Artistic License. By using, modifying or
373             distributing the Package, you accept this license. Do not use, modify,
374             or distribute the Package, if you do not accept this license.
375              
376             If your Modified Version has been derived from a Modified Version made
377             by someone other than you, you are nevertheless required to ensure that
378             your Modified Version complies with the requirements of this license.
379              
380             This license does not grant you the right to use any trademark, service
381             mark, tradename, or logo of the Copyright Holder.
382              
383             This license includes the non-exclusive, worldwide, free-of-charge
384             patent license to make, have made, use, offer to sell, sell, import and
385             otherwise transfer the Package with respect to any patent claims
386             licensable by the Copyright Holder that are necessarily infringed by the
387             Package. If you institute patent litigation (including a cross-claim or
388             counterclaim) against any party alleging that the Package constitutes
389             direct or contributory patent infringement, then this Artistic License
390             to you shall terminate on the date that such litigation is filed.
391              
392             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
393             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
394             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
395             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
396             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
397             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
398             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
399             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
400              
401              
402             =cut
403              
404             1; # End of WebService::Megaplan