File Coverage

blib/lib/Net/SharePoint/Basic.pm
Criterion Covered Total %
statement 179 329 54.4
branch 52 114 45.6
condition 37 121 30.5
subroutine 31 43 72.0
pod 25 25 100.0
total 324 632 51.2


line stmt bran cond sub pod time code
1             package Net::SharePoint::Basic;
2              
3             # Copyright 2018 VMware, Inc.
4             # SPDX-License-Identifier: Artistic-1.0-Perl
5              
6 19     19   1334792 use 5.10.1;
  19         233  
7 19     19   125 use strict;
  19         46  
  19         585  
8 19     19   111 use warnings FATAL => 'all';
  19         40  
  19         764  
9 19     19   11357 use utf8;
  19         280  
  19         96  
10 19     19   8726 use experimental qw(smartmatch);
  19         62810  
  19         101  
11              
12 19     19   1276 use Carp qw(carp croak confess cluck longmess shortmess);
  19         48  
  19         1398  
13 19     19   141 use File::Basename;
  19         43  
  19         1797  
14 19     19   10279 use IO::Scalar;
  19         232513  
  19         878  
15 19     19   11853 use Storable;
  19         57812  
  19         1091  
16              
17 19     19   8667 use Data::UUID;
  19         11251  
  19         1262  
18 19     19   505 use File::Path;
  19         42  
  19         1294  
19 19     19   13347 use JSON::XS;
  19         94831  
  19         1090  
20 19     19   12565 use LWP::UserAgent;
  19         819482  
  19         797  
21 19     19   10127 use POSIX qw(strftime);
  19         123747  
  19         123  
22 19     19   27653 use URI::Escape;
  19         55  
  19         1090  
23              
24 19     19   12264 use Data::Dumper;
  19         128897  
  19         1282  
25              
26 19     19   147 use base 'Exporter';
  19         54  
  19         91054  
27              
28             our @EXPORT = qw(
29             $DEFAULT_SHAREPOINT_TOKEN_FILE $DEFAULT_SHAREPOINT_CONFIG_FILE
30             $DEFAULT_RETRIES $DEFAULT_CHUNK_SIZE $MAX_LOG_SIZE
31             );
32              
33             =head1 NAME
34              
35             Net::SharePoint::Basic - Basic interface to Microsoft SharePoint REST API
36              
37             =head1 VERSION
38              
39             Version 0.1
40              
41             =cut
42              
43             our $VERSION = '0.1';
44              
45             our %PATTERNS = (
46             payload => "grant_type=client_credentials&client_id=%1\$s\@%3\$s&client_secret=%2\$s&resource=%4\$s/%5\$s\@%3\$s&scope=%4\$s/%5\$s\@%3\$s",
47             upload => "https://%1\$s/%2\$s/_api/web/GetFolderByServerRelativeUrl('/%2\$s/Shared Documents/%4\$s')/files/add(overwrite=true,url='%3\$s')",
48             download => "https://%1\$s/%2\$s/_api/web/GetFolderByServerRelativeUrl('/%2\$s/Shared Documents/%4\$s')/files('%3\$s')/\$value",
49             makedir => "https://%1\$s/%2\$s/_api/SP.AppContextSite(\@target)/web/folders/add('Shared Documents/%4\$s')?\@target='https://%1\$s/%2\$s'",
50             delete => "https://%1\$s/%2\$s/_api/web/GetFolderByServerRelativeUrl('/%2\$s/Shared Documents/%3\$s')/recycle",
51             list => {
52             files => "https://%1\$s/%2\$s/_api/web/GetFolderByServerRelativeUrl('/%2\$s/Shared Documents/%3\$s')/Files",
53             folders => "https://%1\$s/%2\$s/_api/web/GetFolderByServerRelativeUrl('/%2\$s/Shared Documents/%3\$s')/Folders",
54             },
55             chunk => {
56             start => "https://%1\$s/%2\$s/_api/web/GetFileByServerRelativeUrl('/%2\$s/Shared Documents/%4\$s/%3\$s')/startupload(uploadId=guid'%5\$s')",
57             continue => "https://%1\$s/%2\$s/_api/SP.AppContextSite(\@target)/web/GetFileByServerRelativeUrl('/%2\$s/Shared Documents/%4\$s/%3\$s')/continueupload(uploadId=guid'%5\$s', fileOffset='%6\$s')?\@target='https://%1\$s/%2\$s'",
58             finish => "https://%1\$s/%2\$s/_api/SP.AppContextSite(\@target)/web/GetFileByServerRelativeUrl('/%2\$s/Shared Documents/%4\$s/%3\$s')/finishupload(uploadId=guid'%5\$s', fileOffset='%6\$s')?\@target='https://%1\$s/%2\$s'",
59             },
60             move => "https://%1\$s/%2\$s/_api/SP.AppContextSite(\@target)/web/GetFileByServerRelativeUrl('/%2\$s/Shared Documents/%3\$s')/moveto(newurl='/%2\$s/Shared Documents/%4\$s', flags=1)?\@target='https://%1\$s/%2\$s'",
61             copy => "https://%1\$s/%2\$s/_api/SP.AppContextSite(\@target)/web/GetFileByServerRelativeUrl('/%2\$s/Shared Documents/%3\$s')/copyto(strnewurl='/%2\$s/Shared Documents/%4\$s', boverwrite=true)?\@target='https://%1\$s/%2\$s'",
62             );
63             our $DEFAULT_SHAREPOINT_TOKEN_FILE = '/var/run/sharepoint.token';
64             our $DEFAULT_SHAREPOINT_CONFIG_FILE = '/etc/sharepoint.conf';
65              
66             our %DEFAULT_SHAREPOINT_POST_PARAMS = (
67             Accept => 'application/json;odata=verbose',
68             Content_Type => 'application/json;odata=verbose',
69             );
70             our $MAX_LOG_SIZE = 500000;
71             our $DEFAULT_CHUNK_SIZE = 200000000;
72             our $DEFAULT_RETRIES = 3;
73              
74             =head1 SYNOPSIS
75              
76             Net::SharePoint::Basic - Basic interface to Microsoft SharePoint REST API.
77              
78             This module provides a basic interface for managing the Shared Documents catalog in the Microsoft SharePoint site via its REST API. In the current version only the following actions are supported:
79              
80             * generating a connection token
81             * upload file or string
82             * download file content and save it
83             * list contents of folder
84             * create new folder
85             * delete file or folder
86              
87             More actions are expected to be added in the future as well as we plan to increase the versatility of the arguments accepted by this module and the sample implementation of a client, 'sp-client', that comes with it.
88              
89             The interface is object oriented. A few constants are exported.
90              
91             The full testing (and naturally the full usage) of the module requires a working SharePoint site configuration. The structure of the configuration file will be described in this manual as well. The sample configuration file provided in this distribution will not work against SharePoint and plays the role of a placeholder only.
92              
93             use Net::SharePoint::Basic;
94              
95             my $sp = Net::SharePoint::Basic->new({config_file => 'sharepoint.conf'});
96             # creates Shared Documents/test
97             my $response = $sp->makedir({retries => 1}, '/test');
98             # uploads a string as Shared Documents/test/teststring
99             $sp->upload({}, '/test/teststring', 'abcd');
100             # uploads a file 'testfile' into Shared Documents/test/
101             $sp->upload({type => 'file'}, '/test/', 'testfile');
102             # downloads contents of a file
103             $sp->download({}, '/test/teststring');
104             # downloads contents and saves it to a file
105             $sp->download({save_file => 'testfile'}, '/test/teststring');
106             # lists contents of a folder
107             $sp->list({}, '/test');
108             # deletes the folder
109             $sp->delete({}, '/test');
110              
111             This module was developed based on the MSDN SharePoint REST API at https://msdn.microsoft.com/en-us/library/office/jj860569.aspx .
112              
113             =head1 EXPORT
114              
115             The following constants (all can be overridden through either configuration file or constructor options) are exported:
116              
117             =over
118              
119             =item $DEFAULT_SHAREPOINT_TOKEN_FILE
120              
121             The default location of the authorization token file (/var/run/sharepoint.token'
122              
123             =item $DEFAULT_SHAREPOINT_CONFIG_FILE
124              
125             The default location of the SharePoint portal configuration (/etc/sharepoint.conf)
126              
127             =item $DEFAULT_RETRIES
128              
129             The default number of retries to perform a REST action (3)
130              
131             =item $DEFAULT_CHUNK_SIZE
132              
133             The default chunk size for uploading large items. (200000000 bytes)
134              
135             =item $MAX_LOG_SIZE
136              
137             The maxium number of logged actions to keep (see C method (500000)
138              
139             =back
140              
141             =head1 CONFIGURATION FILE
142              
143             The module can work with a configuration file of the following format:
144              
145             configuration_option value
146              
147             The lines starting with '#' are ignored. The multiline values can be broken by using a backslash '\' sign. A sample configuration file is provided in the 't/' directory. It will NOT work with a Microsoft SharePoint instance, it is a placeholder file, useful only for internal tests. The default location of the configuration file assumed by the module is /etc/sharepoint.conf . The recognized options in the configuration file are:
148              
149             * module configuration:
150             o token_file - where to store the SharePoint token
151             o max_log_size - maximum log size of $object->{log} (q.v.)
152             o retries - number of retries
153             o chunk_size - size of a chunk for upload in chunks of large files
154             * SharePoint configuration:
155             o sharepoint_client_id - UUID of this client for SharePoint
156             o sharepoint_client_secret - client secret for generating the access token
157             o sharepoint_tenant_id - UUID of the SharePoint tenant
158             o sharepoint_principal_id - UUID of the SharePoint principal
159             o sharepoint_host - the hostname of the SharePoint portal
160             o sharepoint_site - the site to work with in SharePoint
161             o sharepoint_access_url - URL to request the token from
162              
163             =cut
164              
165             =head1 ENVIROMENT VARIABLES
166              
167             The following environment variables control the SharePoint client's behavior, for the purpose of debugging output:
168              
169             * NET_SHAREPOINT_VERBOSE - enable verbose output
170             * NET_SHAREPOINT_DEBUG - enable debug output
171              
172             =cut
173              
174             =head1 SUBROUTINES/METHODS
175              
176             =head2 verbose ($)
177              
178             Utility function printing some extra messages. Newline is automatically appended.
179             Parameters: the message to print (if a verbosity setting is on).
180             Returns: void
181              
182             =cut
183              
184             sub verbose ($) {
185              
186 4     4 1 4318 my $message = shift;
187 4         16 binmode STDERR, ':utf8';
188 4 100 100     126 print STDERR "$message\n" if $ENV{NET_SHAREPOINT_VERBOSE} || $ENV{NET_SHAREPOINT_DEBUG};
189             }
190              
191             =head2 debug ($)
192              
193             Utility function printing some debug messages. Newline is automatically appended.
194             Parameters: the message to print (if a verbosity setting is on).
195             Returns: void
196              
197             =cut
198              
199             sub debug ($) {
200              
201 10     10 1 4713 my $message = shift;
202 10 100       74 print STDERR "$message\n" if $ENV{NET_SHAREPOINT_DEBUG};
203             }
204              
205             =head2 timedebug ($)
206              
207             Utility function printing some debug messages with timestamp prepended. Newline is automatically appended.
208             Parameters: the message to print (if a verbosity setting is on).
209             Returns: void
210              
211             =cut
212              
213             sub timedebug ($) {
214              
215 1     1 1 1503 my $message = shift;
216 1 50       77 (print STDERR localtime(time) . " $message\n") if $ENV{NET_SHAREPOINT_DEBUG};
217             }
218              
219             =head2 version (;$)
220              
221             Utility function returning the version of the package
222             Parameters: Do not exit after printing version (optional)
223             Returns: never.
224              
225             =cut
226              
227             sub version (;$) {
228              
229 1     1 1 1518 print $VERSION, "\n";
230 1 50       10 exit 0 unless shift;
231             }
232              
233             =head2 read_file ($)
234              
235             Utility function that reads file into a string or dies if the file is not available.
236             Parameters: the file to read
237             Returns: the contents of the file as a string
238              
239             =cut
240              
241             sub read_file ($) {
242              
243 2     2 1 1887 my $file = shift;
244              
245 2         383 local $/ = undef;
246 2         13 debug "Reading $file";
247 2 100       119 open(my $mail_fh, '<', $file) or die "Can't read file $file: $!";
248 1         25 my $content = <$mail_fh>;
249 1         10 close $mail_fh;
250              
251 1         9 $content;
252             }
253              
254             =head2 write_file ($$;$)
255              
256             Utility function thgat writes the given string into a file, creating the necessary directories above it if necessary. Dies if the write is unsuccessful.
257             Parameters: the contents
258             the file to write
259             [optional] force binary mode in writing
260             Returns: the file path that was written.
261              
262             =cut
263              
264             sub write_file ($$;$) {
265              
266 1     1 1 1067 my $content = shift;
267 1         3 my $file = shift;
268 1   50     7 my $binary = shift || 0;
269              
270 1         30 my $dir = dirname($file);
271 1 50       176 mkpath($dir) unless -d $dir;
272 1 50   1   38 open my $fh, $binary ? '>:raw' : '>:encoding(utf8)', $file or die "Couldn't open file $file for writing: $!";
  1 50       8  
  1         2  
  1         7  
273 1 50       11654 binmode $fh if $binary;
274 1         11 print $fh $content;
275 1         53 close $fh;
276 1         10 verbose "Wrote file $file";
277 1         7 $file;
278             }
279              
280             =head2 read_config ($)
281              
282             Utility function that reads the sharepoint configuration file of whitespace separated values. See the detailed description of C
283             Parameters: the configuration file
284             Returns: Hash of configuration parameters and their values.
285              
286             =cut
287              
288             sub read_config ($) {
289              
290 5     5 1 89 my $config_file = shift;
291 5         13 my $config = {};
292              
293 5 50       215 open(my $conf_fh, '<', $config_file) or return 0;
294 5         133 while (<$conf_fh>) {
295 40 100       100 next if /^\#/;
296 38 100       108 next unless /\S/;
297 36         78 s/^\s+//;
298 36         165 s/\s+$//;
299 36         135 my ($key, $value) = split(/\s+/, $_, 2);
300 36 100       80 unless ($value) {
301 2         6 $config->{$key} = undef;
302 2         7 next;
303             }
304 34         56 chomp $value;
305 34         89 while ($value =~ /\\$/) {
306 6         12 my $extra_value = <$conf_fh>;
307 6 50       16 next if $extra_value =~ /^\#/;
308 6 50       18 next unless $extra_value =~ /\S/;
309 6         17 $extra_value =~ s/^\s+//;
310 6         21 $extra_value =~ s/\s+$//;
311 6         13 chop $value;
312 6         17 $value =~ s/\s+$//;
313 6         21 $value .= " $extra_value";
314             }
315 34         160 $config->{$key} = $value;
316             }
317 5         53 close $conf_fh;
318 5         37 return $config;
319             }
320              
321             =head2 new ($;$)
322              
323             The constructor. Creates the Net::SharePoint::Basic object
324             Parameters: optional hash with keys corresponding to the configuration file fields. Will override even the given a specific configuration file.
325             Returns: Net::SharePoint::Basic object.
326              
327             =cut
328              
329             sub new ($;$) {
330              
331 8     8 1 2993 my $class = shift;
332 8   100     36 my $opts = shift || {};
333              
334 8         19 my $self = {};
335 8 100       28 if ($opts->{config_file}) {
336 4         19 $self->{config} = read_config($opts->{config_file});
337             }
338             else {
339 4 50       174 $self->{config} = read_config($DEFAULT_SHAREPOINT_CONFIG_FILE)
340             if -f $DEFAULT_SHAREPOINT_CONFIG_FILE;
341             }
342 8   100     52 $self->{config} ||= {};
343 8         16 for my $key (keys %{$opts}) {
  8         38  
344 9 100       30 next if $key eq 'config_file';
345 5 50       15 next unless defined $opts->{$key};
346 5         27 debug "Setting $key to $opts->{$key}";
347 5         15 $self->{config}{$key} = $opts->{$key};
348             }
349 8         28 $self->{token} = { ts => 0 };
350 8         20 $self->{next_guid} = 1;
351 8   66     40 $self->{config}{token_file} ||= $DEFAULT_SHAREPOINT_TOKEN_FILE;
352 8   66     48 $self->{config}{max_log_size} ||= $MAX_LOG_SIZE;
353 8   66     39 $self->{config}{chunk_size} ||= $DEFAULT_CHUNK_SIZE;
354 8   33     42 $self->{config}{retries} ||= $DEFAULT_RETRIES;
355 8   33     85 $ENV{NET_SHAREPOINT_DEBUG} ||= $opts->{debug};
356 8   33     52 $ENV{NET_SHAREPOINT_VERBOSE} ||= $opts->{verbose};
357 8         26 bless $self, $class;
358 8         45 $self;
359             }
360              
361             =head2 dump_config ($)
362              
363             Dumps the supplied config and exits
364             Arguments: the options hash
365             Returns: void
366             Caveat: will dump the credentials as well. Use with caution.
367              
368             =cut
369              
370             sub dump_config ($) {
371              
372 11     11 1 16115 my $self = shift;
373              
374 11         23 for my $opt (keys %{$self->{config}}) {
  11         47  
375 121   50     1780 printf "%-25s %s\n", $opt, $self->{config}{$opt} || 'undef';
376             }
377             }
378              
379             =head2 validate_config ($;@)
380              
381             Validates the configuration for the SharePoint client. Checks basic syntactic requirements for the key configuration parameters expected to make connection with the REST API.
382             Parameters: [optional] a list of extra options that would require to be defined by the application
383             Returns: Error string if there was an error, empty string otherwise.
384              
385             =cut
386              
387             sub validate_config ($;@) {
388              
389 2     2 1 1866 my $self = shift;
390 2         6 my @extra_opts = @_;
391              
392 2         5 my $opts = $self->{config};
393 2         4 my $validated = '';
394              
395 2 50       7 return "Config was not found\n" unless $opts;
396 2         5 for my $id (qw(sharepoint_client_id sharepoint_tenant_id sharepoint_principal_id)) {
397 6 100       28 if (! $opts->{$id}) {
    50          
398 3         10 $validated .= "Missing $id in configuration\n";
399             }
400             elsif ($opts->{$id} !~ /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/) {
401 0         0 $validated .= "Badly formatted $id $opts->{$id} in configuration\n";
402             }
403             }
404             $validated .= "SharePoint secret must be 44-char string ending with =\n"
405             unless $opts->{sharepoint_client_secret}
406 2 100 66     16 && $opts->{sharepoint_client_secret} =~ /^(\S){43}\=/;
407             $validated .= "SharePoint access URL must start with https://\n"
408             unless $opts->{sharepoint_access_url}
409 2 100 66     11 && $opts->{sharepoint_access_url} =~ m|^https://|;
410             $validated .= "SharePoint host must be defined\n"
411 2 100       8 unless $opts->{sharepoint_host};
412             $validated .= "SharePoint Site must include 'sites' in the URL\n"
413             unless $opts->{sharepoint_site}
414 2 100 66     13 && $opts->{sharepoint_site} =~ m|^sites/.+|;
415 2         7 for my $extra_opt (@extra_opts) {
416             $validated .= "Option $extra_opt must be set according to the app"
417 0 0       0 unless $opts->{$extra_opt};
418             }
419 2         10 return $validated;
420             }
421              
422             =head2 log_it ($$;$)
423              
424             Log a message into the object. The messages are stored in the $object->{log} array reference. If the amount of messages exceeds $MAX_LOG_SIZE or $self->{config}{max_log_size} (if set), the older messages are shifted out of the log.
425             Parameters: the message
426             [optional] the severity (default - 'info')
427             Returns: the shifted discarded message if any
428              
429             =cut
430              
431             sub log_it ($$;$) {
432              
433 3     3 1 4788 my $self = shift;
434 3         8 my $message = shift;
435 3   50     11 my $severity = shift || 'info';
436              
437 3   100     17 $self->{log} ||= [];
438 3         4 push(@{$self->{log}}, [
  3         12  
439             time, $severity, $message
440             ]);
441 3 100       6 shift @{$self->{log}} if @{$self->{log}} > $self->{config}{max_log_size};
  1         3  
  3         12  
442             }
443              
444             =head2 create_payload ($)
445              
446             Creates an authorization request payload
447             Arguments: the options hashref containing the sharepoint data
448             Returns: the escaped payload
449              
450             =cut
451              
452             sub create_payload ($) {
453              
454 1     1 1 8 my $self = shift;
455              
456             my $payload = sprintf(
457             $PATTERNS{payload},
458             $self->{config}{sharepoint_client_id},
459             $self->{config}{sharepoint_client_secret},
460             $self->{config}{sharepoint_tenant_id},
461             $self->{config}{sharepoint_principal_id},
462             $self->{config}{sharepoint_host}
463 1         15 );
464 1         5 uri_escape($payload, "^A-Za-z0-9\-\._~\&\=");
465             }
466              
467             =head2 update_token ($$)
468              
469             Updates the SharePoint Token
470             Arguments: the options hashref containing sharepoint data
471             Returns: 1 upon success
472             undef upon failure
473              
474             =cut
475              
476             sub update_token ($) {
477              
478 0     0 1 0 my $self = shift;
479              
480 0   0     0 $self->{ua} ||= LWP::UserAgent->new();
481 0   0     0 $self->{token}{payload} ||= $self->create_payload();
482 0         0 verbose "Getting a fresh token";
483             my $token_response = $self->{ua}->post(
484             $self->{config}{sharepoint_access_url},
485             Content_Type => 'application/x-www-form-urlencoded',
486             Content => $self->{token}{payload}
487 0         0 );
488 0 0       0 unless ($token_response->is_success) {
489 0         0 $self->log_it(
490             "Updating token failed: " .
491             Dumper decode_json($token_response->content),
492             'error'
493             );
494 0         0 return undef;
495             }
496 0         0 my $json = decode_json($token_response->content);
497 0         0 $self->{token}{ts} = $json->{expires_on};
498 0         0 $self->{token}{token} = $json->{access_token};
499 0         0 $self->{token}{type} = $json->{token_type};
500 0         0 1;
501             }
502              
503              
504             =head2 init_token ($)
505              
506             Initializes a SharePoint token ( by calling ->update_token() ) and stores it in the $self->{config}{token_file}
507             Parameters: none
508             Returns: 1 if success
509             undef if failure
510              
511             =cut
512              
513             sub init_token ($) {
514              
515 0     0 1 0 my $self = shift;
516              
517 0         0 my $token_file = $self->{config}{token_file};
518 0 0       0 if (-f $token_file) {
519 0         0 $self->log_it("Trying to use token file $token_file", 'debug');
520 0         0 $self->{token} = retrieve $token_file;
521             return undef if (
522 0 0 0     0 time > $self->{token}{ts} - 1200
523             ) and ! $self->update_token();
524             }
525             else {
526 0 0       0 return undef unless $self->update_token();
527             }
528 0         0 store $self->{token}, $token_file;
529             $DEFAULT_SHAREPOINT_POST_PARAMS{Authorization} =
530 0         0 "$self->{token}{type} $self->{token}{token}";
531 0         0 1;
532             }
533              
534             =head2 create_sharepoint_url ($$;@)
535              
536             Creates the SharePoint URL to operate against, filling the relevant pattern with the actual data.
537             Parameters: the options hashref of the following keys
538             pattern - the ready pattern (usually used in chunk upload) - or
539             type - the type of the pattern
540             (upload, download, list, makedir, delete) and
541             subtype (for list only) - "files" or "folders"
542             folder - the sharepoint folder/path to operate upon
543             object - the sharepoint object to operate upon
544             See %PATTERNS in the source code for more details on the URL construction.
545             Returns: the filled URL string
546              
547             =cut
548              
549             sub create_sharepoint_url ($$;@) {
550              
551 39     39 1 17345 my $self = shift;
552 39         61 my $opts = shift;
553              
554 39 100 33     237 return undef unless $opts && ($opts->{pattern} || $opts->{type});
      66        
555 38   100     108 my $folder = $opts->{folder} || '';
556 38 50       163 $folder = '.' if $folder eq '/';
557 38   100     103 my $object = $opts->{object} || '';
558 38         60 my @extra_args = @_;
559 38         58 my $pattern = $opts->{pattern};
560 38 50       79 if (! $pattern) {
561 38 100       94 if (! $PATTERNS{$opts->{type}}) {
562 1         22 warn "Unknown type $opts->{type} of URL requested";
563 1         9 return undef;
564             }
565 37 100       105 if (ref $PATTERNS{$opts->{type}}) {
566 21 100 66     95 if (! $opts->{subtype} || ! $PATTERNS{$opts->{type}}->{$opts->{subtype}}) {
567 1         14 warn "Pattern type $opts->{type} requires a valid subtype";
568 1         9 return undef;
569             }
570 20         44 $pattern = $PATTERNS{$opts->{type}}->{$opts->{subtype}};
571             }
572             else {
573 16         28 $pattern = $PATTERNS{$opts->{type}};
574             }
575             }
576             $pattern =~
577 0         0 s|Shared Documents|"Shared Documents/$self->{config}{base_subfolder}"|ge
578 36 50       88 if $self->{config}{base_subfolder};
579             my $url = sprintf(
580             $pattern,
581             $self->{config}{sharepoint_host},
582             $self->{config}{sharepoint_site},
583 36   33     246 $object // (),
584             $folder, @extra_args,
585             );
586              
587 36         115 $url;
588             }
589              
590             =head2 try ($$$%)
591              
592             Tries to execute a SharePoint REST API call
593             Parameters: the options hashref with the following parameters:
594             action - mandatory, one of upload, download, list, makedir, delete,
595             chunk_upload_start, chunk_upload_continue, chunk_upload_finish
596             retries - optional, number of retries defaults to $DEFAULT_RETRIES
597             the url to try
598             extra http header options hash:
599             Accept => 'application/json;odata=verbose',
600             Content_Type => 'application/json;odata=verbose',
601             Authorization => 'Bearer TOKEN_STRING',
602             and for upload also:
603             Content_Length => length($data),
604             Content => $data,
605             Returns: the HTTP response if the API call was successful
606             undef otherwise
607              
608             =cut
609              
610             sub try ($$$%) {
611              
612 0     0 1 0 my $self = shift;
613 0         0 my $opts = shift;
614 0         0 my $url = shift;
615 0         0 my %http_opts = @_;
616              
617 0   0     0 $self->{ua} ||= LWP::UserAgent->new();
618 0   0     0 $opts->{retries} ||= $DEFAULT_RETRIES;
619              
620 0 0 0     0 my $method = $opts->{action} eq 'download' || $opts->{action} eq 'list' ?
621             'get' : 'post';
622 0         0 while ($opts->{retries}) {
623 0 0       0 unless ($self->init_token()) {
624 0         0 $self->log_it("Failed to initialize token", "error");
625 0         0 die "Failed to initialize token: $self->{log}[-2][2]";
626             }
627 0         0 debug "Trying url $url";
628 0   0     0 $http_opts{Authorization} ||= $DEFAULT_SHAREPOINT_POST_PARAMS{Authorization};
629 0         0 my $response = $self->{ua}->$method($url, %http_opts);
630 0 0       0 if ($response->is_success) {
631 0         0 $self->log_it("Item $opts->{action} successful");
632 0         0 return $response;
633             }
634 0         0 $opts->{retries}--;
635 0         0 $self->log_it(
636             "Couldn't $opts->{action} item ($opts->{retries} attempts remaining).\n" . $response->content, 'error'
637             );
638             }
639 0         0 return undef;
640             }
641              
642             =head2 get_chunk_pattern ($$)
643              
644             Selects correct pattern for uploading a chunk, depending on the stage of the upload.
645             Parameters: the chunk's number (0..N)
646             the number of total chunks (N)
647             Returns: the start upload pattern for the first chunk
648             the finish upload pattern for the last chunk
649             the continue upload pattern for the rest
650             Caveat: uses when() feature.
651              
652             =cut
653              
654             sub get_chunk_pattern ($$) {
655              
656 0     0 1 0 my $chunk_n = shift;
657 0         0 my $total_chunks = shift;
658              
659 0         0 my $pattern;
660 0         0 for ($chunk_n) {
661 0         0 when (0) { $pattern = 'start'; }
  0         0  
662 0         0 when ($total_chunks) { $pattern = 'finish'; }
  0         0  
663 0         0 default { $pattern = 'continue'; }
  0         0  
664             }
665              
666 0         0 $PATTERNS{chunk}->{$pattern};
667             }
668              
669             =head2 upload_in_chunks ($$$$$;$)
670              
671             Uploads a string in chunks, useful for uploading large volumes of data above the default SharePoint limit, usually 250000000 bytes.
672             Parameters: the options hash with the number of retries as the only used key.
673             the string scalar of data to upload
674             the SharePoint object basename to create
675             the SharePoint path to put the object in
676             (optional) the chunk size. Defaults to either configured chunk size or to $DEFAULT_CHUNK_SIZE.
677             Returns: the cumulative C response. If any of the tries fails aborts and returns 0.
678             Caveat: the object must already be exist in SharePoint, even with zero size. See C.
679              
680             =cut
681              
682             sub upload_in_chunks ($$$$$;$) {
683              
684 0     0 1 0 my $self = shift;
685 0         0 my $opts = shift;
686 0         0 my $item = shift;
687 0         0 my $object = shift;
688 0         0 my $folder = shift;
689 0   0     0 my $chunk = shift || $self->{config}{chunk_size} || $DEFAULT_CHUNK_SIZE;
690              
691 0 0       0 my $size = length(ref $item ? $$item : $item);
692 0         0 my $total_chunks = int($size/$chunk);
693              
694 0         0 my $ug = Data::UUID->new();
695 0         0 my $guid = $ug->to_string($ug->create());
696 0         0 my $r;
697 0         0 for my $chunk_n (0..$total_chunks) {
698 0 0       0 my $data = substr(
699             (ref $item ? $$item : $item),
700             $chunk_n*$chunk, $chunk
701             );
702 0         0 my $pattern = get_chunk_pattern($chunk_n, $total_chunks);
703 0 0       0 my $upload_url = $self->create_sharepoint_url({
704             pattern => $pattern,
705             object => $object,
706             folder => $folder,
707             }, $guid, $chunk_n ? $chunk_n * $chunk : ());
708 0         0 $self->log_it("Chunk upload ($pattern) to $upload_url", 'debug');
709             $r = $self->try(
710             {
711             action => "chunk_upload_$pattern",
712             retries => $opts->{retries} || $self->{config}{retries},
713             },
714 0   0     0 $upload_url, (
715             %DEFAULT_SHAREPOINT_POST_PARAMS,
716             Content_Length => length($data),
717             Content => $data,
718             )
719             );
720 0 0       0 return 0 unless $r;
721             }
722 0         0 $r;
723             }
724              
725             =head2 upload ($$$$)
726              
727             Uploads a file or a string to SharePoint. Initiates the upload in chunks if necessary, generating the zero sized file for it before calling C
728             Parameters: the options hash with
729             type - "file" means we're uploading a file
730             retries - the number of retries
731             the SharePoint target. If it's a path ending with '/', basename of the file is being appended.
732             the item - file or data string
733             Returns: the HTTP response object if successful.
734             0 when the upload fails or
735             when the file is unreadable or
736             when the upload is of a string and no target filename is specified.
737              
738             =cut
739              
740             sub upload ($$$$) {
741              
742 0     0 1 0 my $self = shift;
743 0         0 my $opts = shift;
744 0         0 my $target = shift;
745 0         0 my $item = shift;
746              
747 0         0 my ($object, $folder) = fileparse($target);
748 0   0     0 $opts->{type} ||= '';
749 0 0 0     0 if (! $object && $opts->{type} ne 'file') {
750 0         0 warn "Cannot upload without target filename";
751 0         0 return 0;
752             }
753 0   0     0 $object ||= basename($item);
754 0 0       0 if ($opts->{type} eq 'file') {
755 0 0       0 if (! -f $item) {
756 0         0 warn "File $item does not exist, ignoring";
757 0         0 return 0;
758             }
759 0         0 $item = read_file($item);
760             }
761 0         0 my $upload_url = $self->create_sharepoint_url({
762             type => 'upload',
763             object => $object,
764             folder => $folder,
765             });
766 0 0       0 if (length(ref $item ? $$item : $item) > $self->{config}{chunk_size}) {
    0          
767             my $r = $self->try(
768             {
769             action => 'upload',
770             retries => $opts->{retries} || $self->{config}{retries},
771             },
772 0   0     0 $upload_url, (
773             %DEFAULT_SHAREPOINT_POST_PARAMS,
774             Content_Length => 0,
775             Content => '',
776             )
777             );
778 0 0       0 return 0 unless $r;
779 0         0 $self->upload_in_chunks($opts, $item, $object, $folder);
780             }
781             else {
782 0         0 $self->log_it("Upload to $upload_url", 'debug');
783             return $self->try(
784             {
785             action => 'upload',
786             retries => $opts->{retries} || $self->{config}{retries},
787             },
788 0   0     0 $upload_url, (
789             %DEFAULT_SHAREPOINT_POST_PARAMS,
790             Content_Length => length($item),
791             Content => ref $item ? $$item : $item,
792             )
793             ) || 0;
794             }
795             }
796              
797             =head2 download ($$$;$)
798              
799             Downloads an object from SharePoint, optionally saving it into a file.
800             Parameters: the options hashref
801             save_file - the local path to save (or see target below)
802             retries - the number of retries
803             the SharePoint path to download
804             (optional) the target local path to save. If target (or save_file value) is a directory, use basename of the SharePoint path for filename. The directory tree is created via C.
805             Returns: 0 if download failed
806             path contents as a string scalar if string is requested
807             saved path if a file save is requested
808              
809             =cut
810              
811             sub download ($$$;$) {
812              
813 0     0 1 0 my $self = shift;
814 0         0 my $opts = shift;
815 0         0 my $item = shift;
816 0   0     0 my $target = shift || '';
817              
818 0 0       0 $opts->{save_file} = $target if $target;
819 0         0 my $download_url = $self->create_sharepoint_url({
820             type => 'download',
821             object => basename($item),
822             folder => dirname($item),
823             });
824 0         0 $self->log_it("Download from $download_url", 'debug');
825             my $response = $self->try(
826             {
827             action => 'download',
828             retries => $opts->{retries} || $self->{config}{retries},
829             },
830 0   0     0 $download_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
831             );
832 0 0       0 return 0 if ! defined $response;
833 0 0       0 return $response->content unless $opts->{save_file};
834 0 0       0 $opts->{save_file} .= "/" . basename($item) if -d $opts->{save_file};
835 0         0 write_file($response->content, $opts->{save_file}, 1);
836 0         0 return $opts->{save_file};
837             }
838              
839             =head2 list ($$;$)
840              
841             Gets the contents of a given SharePoint folder. Note that you cannot list a file, you need to provide its path (event if it is root), and filter the results. Two API calls are issued, one to list files in the folders, one to list subfolders.
842             Parameters: the options hashref
843             path - the path to list
844             retries - the number of retries
845             path - (optional) - alternative way to specify path to list
846             Returns: a decoded JSON structure of the REST API response or
847             an empty list in case of failure.
848             For the interpretation of the results for an actual listing, see the print_list_reports subroutine in the example SharePoint client provided in the package.
849              
850             =cut
851              
852             sub list ($$;$) {
853              
854 0     0 1 0 my $self = shift;
855 0         0 my $opts = shift;
856 0   0     0 my $path = shift || $opts->{path};
857              
858 0         0 my @results = ();
859 0         0 for my $list_type (qw(files folders)) {
860             my $list_url = $self->create_sharepoint_url({
861 0         0 pattern => $PATTERNS{list}->{$list_type},
862             object => $path,
863             });
864 0         0 $self->log_it("Listing $list_url", 'debug');
865             my $list_response = $self->try(
866             {
867             action => 'list',
868             retries => $opts->{retries} || $self->{config}{retries},
869             },
870 0   0     0 $list_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
871             );
872 0 0       0 next unless defined $list_response;
873 0         0 my $json = decode_json($list_response->content);
874 0         0 push(@results, @{$json->{d}{results}});
  0         0  
875             }
876 0         0 return \@results;
877             }
878              
879             =head2 makedir ($$$)
880              
881             Creates a new folder in SharePoint.
882             Parameters: the options hashref
883             retries - the number of retries
884             the folder to create
885             Returns: the REST API response as returned by C
886              
887             =cut
888              
889             sub makedir ($$$) {
890              
891 0     0 1 0 my $self = shift;
892 0         0 my $opts = shift;
893 0         0 my $folder = shift;
894              
895 0         0 my $mkdir_url = $self->create_sharepoint_url({
896             type => 'makedir',
897             folder => $folder,
898             });
899 0         0 $self->log_it("Creating folder $mkdir_url", 'debug');
900             $self->try(
901             {
902             action => 'makedir',
903             retries => $opts->{retries} || $self->{config}{retries},
904             },
905 0   0     0 $mkdir_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
906             );
907             }
908              
909             =head2 delete ($$$)
910              
911             Deletes an item in SharePoint.
912             Parameters: the options hashref
913             retries - the number of retries
914             the item to delete
915             Returns: the REST API response as returned by C
916             Note: any item will be deleted (put to Recycle Bin), even a non-empty folder. If a non-existent item is requested for deletion, the deletion will still return success, but the resulting response will have field $json->{d}{Recycle} set to "00000000-0000-0000-0000-000000000000"
917              
918             =cut
919              
920             sub delete ($$$) {
921              
922 0     0 1 0 my $self = shift;
923 0         0 my $opts = shift;
924 0         0 my $item = shift;
925              
926 0         0 my $delete_url = $self->create_sharepoint_url({
927             type => 'delete',
928             object => $item,
929             });
930 0         0 $self->log_it("Deleting item $delete_url", 'debug');
931             $self->try(
932             {
933             action => 'delete',
934             retries => $opts->{retries} || $self->{config}{retries},
935             },
936 0   0     0 $delete_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
937             );
938             }
939              
940             =head2 move ($$$$)
941              
942             Moves an item in SharePoint
943             Parameters: the options hashref
944             retries - the number of retries
945             the item to move
946             the destination to move to
947             Returns: the REST API response as returned by C
948              
949             =cut
950              
951             sub move ($$$$) {
952              
953 0     0 1 0 my $self = shift;
954 0         0 my $opts = shift;
955 0         0 my $item = shift;
956 0         0 my $target = shift;
957              
958 0         0 my $move_url = $self->create_sharepoint_url({
959             type => 'move',
960             object => $item,
961             folder => $target,
962             });
963 0         0 $self->log_it("Moving item $item to $target", 'debug');
964             $self->try(
965             {
966             action => 'move',
967             retries => $opts->{retries} || $self->{config}{retries},
968             },
969 0   0     0 $move_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
970             );
971             }
972              
973             =head2 copy ($$$$)
974              
975             Copies an item in SharePoint
976             Parameters: the options hashref
977             retries - the number of retries
978             the item to copy
979             the destination to copy to
980             Returns: the REST API response as returned by C
981              
982             =cut
983              
984             sub copy ($$$$) {
985              
986 0     0 1 0 my $self = shift;
987 0         0 my $opts = shift;
988 0         0 my $item = shift;
989 0         0 my $target = shift;
990              
991 0         0 my $copy_url = $self->create_sharepoint_url({
992             type => 'copy',
993             object => $item,
994             folder => $target,
995             });
996 0         0 $self->log_it("Copying item $item to $target", 'debug');
997             $self->try(
998             {
999             action => 'copy',
1000             retries => $opts->{retries} || $self->{config}{retries},
1001             },
1002 0   0     0 $copy_url, %DEFAULT_SHAREPOINT_POST_PARAMS,
1003             );
1004             }
1005              
1006             =head1 AUTHOR
1007              
1008             Roman Parparov, C<< >>
1009              
1010             =head1 BUGS
1011              
1012             Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
1013              
1014              
1015             =head1 SUPPORT
1016              
1017             You can find documentation for this module with the perldoc command.
1018              
1019             perldoc Net::SharePoint::Basic
1020              
1021              
1022             You can also look for information at:
1023              
1024             =over 4
1025              
1026             =item * RT: CPAN's request tracker (report bugs here)
1027              
1028             L
1029              
1030             =item * AnnoCPAN: Annotated CPAN documentation
1031              
1032             L
1033              
1034             =item * CPAN Ratings
1035              
1036             L
1037              
1038             =item * Search CPAN
1039              
1040             L
1041              
1042             =item * Repository
1043              
1044             L
1045              
1046             =item * MSDN SharePoint REST API
1047              
1048             L
1049              
1050             =back
1051              
1052             =head1 ACKNOWLEDGEMENTS
1053              
1054             Special thanks to Andre Abramenko L for helping me figure out the REST API when I initially implemented it at VMware.
1055              
1056             =head1 LICENSE AND COPYRIGHT
1057              
1058             Copyright 2018 VMware.com
1059              
1060             This program is free software; you can redistribute it and/or modify it
1061             under the terms of either: the GNU General Public License as published
1062             by the Free Software Foundation; or the Artistic License.
1063              
1064             See L for more information.
1065              
1066              
1067             =cut
1068              
1069             1; # End of Net::SharePoint::Basic