File Coverage

blib/lib/WebService/Cmis/Client.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package WebService::Cmis::Client;
2              
3             =head1 NAME
4              
5             WebService::Cmis::Client - Transport layer
6              
7             =head1 DESCRIPTION
8              
9             A CMIS client is used to communicate with the document manangement server
10             by connecting to an exposed web service. It provides the initial access function
11             to the L.
12              
13             A client may use one of the user agents to authenticate against the CMIS backend,
14             as specified by the =useragent= parameter during object construction. By default
15             a user agent will be used performing HTTP basic auth as a fallback implemented
16             by most CMIS servers.
17              
18             Available user agents are:
19              
20             =over 4
21              
22             =item * L (default)
23              
24             =item * L
25              
26             =item * L
27              
28             =back
29              
30             use Cache::FileCache ();
31              
32             my $client = WebService::Cmis::getClient(
33             url => "http://cmis.alfresco.com/service/cmis",
34             cache => new Cache::FileCache({
35             cache_root => "/tmp/cmis_client"
36             },
37             useragent => new WebSercice::Cmis::Agent::BasicAuth(
38             user => "...",
39             password => "..."
40             )
41             )
42             )
43            
44             my $repo = $client->getRepository;
45              
46             Parent class: L
47              
48             =cut
49              
50 1     1   14976 use strict;
  1         3  
  1         46  
51 1     1   8 use warnings;
  1         2  
  1         40  
52              
53 1     1   6 use WebService::Cmis qw(:namespaces :utils);
  1         2  
  1         205  
54 1     1   69 use WebService::Cmis::Repository ();
  0            
  0            
55             use WebService::Cmis::ClientException ();
56             use WebService::Cmis::ServerException ();
57             use XML::LibXML ();
58             use REST::Client ();
59             use Data::Dumper ();
60             use Storable ();
61             use Digest::MD5 ();
62             use Error qw(:try);
63             use URI ();
64              
65             our @ISA = qw(REST::Client);
66              
67             our $CMIS_XPATH_REPOSITORIES = new XML::LibXML::XPathExpression('./*[local-name()="service" and namespace-uri()="'.APP_NS.'"]/*[local-name()="workspace" and namespace-uri()="'.APP_NS.'"]');
68              
69             =head1 METHODS
70              
71             =over 4
72              
73             =item new(%params)
74              
75             Create a new WebService::Cmis::Client. This requires
76             a url of the webservice api, as well as a valid useragent handler.
77              
78             See L for more options.
79              
80             Parameters:
81              
82             =over 4
83              
84             =item * useragent - handler to be used for authentication
85              
86             "WebService::Cmis::Agent::BasicAuth" (default)
87              
88             =item * url - repository url; example:
89              
90             "http://localhost:8080/alfresco/service/cmis"
91              
92             =item * cache - a Cache::Cache object to be used for caching
93              
94             =item * overrideCacheContrib - boolean flag to ignore any http cache control for more aggressive caching
95              
96             =back
97              
98             =cut
99              
100             sub new {
101             my ($class, %params) = @_;
102              
103             my $userAgent = delete $params{useragent};
104             my $repositoryUrl = delete $params{url} || '';
105             my $cache = delete $params{cache};
106             my $overrideCacheControl = delete $params{overrideCacheControl};
107              
108             if (!defined $userAgent) {
109             # default
110            
111             require WebService::Cmis::Agent::BasicAuth;
112             $userAgent = new WebService::Cmis::Agent::BasicAuth();
113              
114             } elsif (!ref $userAgent) {
115             # a scalar describing the user agent implementation
116              
117             my $agentClass = $userAgent;
118             eval "use $agentClass";
119             if ($@) {
120             throw Error::Simple($@);
121             }
122              
123             $userAgent = $agentClass->new();
124              
125             } elsif (!UNIVERSAL::can($userAgent, 'isa')) {
126             # unblessed reference
127              
128             my %params = %$userAgent;
129             my $agentClass = delete $params{impl} || "WebService::Cmis::Agent::BasicAuth";
130             #print STDERR "agentClass=$agentClass\n";
131             eval "use $agentClass";
132             if ($@) {
133             throw Error::Simple($@);
134             }
135              
136             $userAgent = $agentClass->new(%params);
137              
138             } else {
139             # some class to be used as a user agent as is
140             }
141              
142             $params{useragent} = $userAgent;
143             _writeCmisDebug("userAgent=$userAgent");
144              
145             my $this = $class->SUPER::new(%params);
146              
147             $this->{cache} = $cache;
148             $this->{overrideCacheControl} = $overrideCacheControl;
149             $this->{repositoryUrl} = $repositoryUrl;
150             $this->{_cacheHits} = 0;
151              
152             $this->setFollow(1);
153             $this->setUseragent($userAgent);
154             $this->getUseragent()->env_proxy();
155              
156             return $this;
157             }
158              
159             sub DESTROY {
160             my $this = shift;
161              
162             my $ua = $this->getUseragent;
163             $ua->{client} = undef if defined $ua; # break cyclic links
164             _writeCmisDebug($this->{_cacheHits}." cache hits found") if $this->{cache};
165             $this->_init;
166             }
167              
168              
169             sub _init {
170             my $this = shift;
171              
172             $this->{_res} = undef;
173             $this->{_cacheEntry} = undef;
174             $this->{_cacheHits} = undef;
175             $this->{repositories} = undef;
176             $this->{defaultRepository} = undef;
177             }
178              
179             =item setUseragent($userAgent)
180              
181             setter to assert the user agent to be used in the REST::Client
182              
183             =cut
184              
185             sub setUseragent {
186             my ($this, $userAgent) = @_;
187              
188             $this->SUPER::setUseragent($userAgent);
189             $userAgent->{client} = $this if defined $userAgent;
190             }
191              
192             =item toString
193              
194             return a string representation of this client
195              
196             =cut
197              
198             sub toString {
199             my $this = shift;
200             return "CMIS client connection to $this->{repositoryUrl}";
201             }
202              
203             # parse a resonse coming from alfresco
204             sub _parseResponse {
205             my $this = shift;
206              
207             #_writeCmisDebug("called _parseResponse");
208              
209             #print STDERR "response=".Data::Dumper->Dump([$this->{_res}])."\n";
210             my $content = $this->responseContent;
211             #_writeCmisDebug("content=$content");
212              
213             unless ($this->{xmlParser}) {
214             $this->{xmlParser} = XML::LibXML->new;
215             }
216              
217             return if !defined $content || $content eq '';
218             return $this->{xmlParser}->parse_string($content);
219             }
220              
221             =item clearCache
222              
223             nukes all of the cache. calling this method is sometimes required
224             to work around caching effects.
225              
226             =cut
227              
228             sub clearCache {
229             my $this = shift;
230             my $cache = $this->{cache};
231             return unless defined $cache;
232              
233             _writeCmisDebug("clearing cache");
234             return $cache->clear(@_);
235             }
236              
237             =item purgeCache
238              
239             purges outdated cache entries. call this method in case the
240             cache backend is able to do a kind of house keeping.
241              
242             =cut
243              
244             sub purgeCache {
245             my $this = shift;
246             my $cache = $this->{cache};
247             return unless defined $cache;
248              
249             return $cache->purge(@_);
250             }
251              
252             =item removeFromCache($path, %params)
253              
254             removes an item from the cache associated with the given path
255             and url parameters
256              
257             =cut
258              
259             sub removeFromCache {
260             my $this = shift;
261             my $path = shift;
262              
263             my $uri = _getUri($path, @_);
264             _writeCmisDebug("removing from cache $uri");
265             return $this->_cacheRemove($uri);
266             }
267              
268             # internal cache layer
269             sub _cacheGet {
270             my $this = shift;
271             my $cache = $this->{cache};
272             return unless defined $cache;
273              
274             my $key = $this->_cacheKey(shift);
275             my $val = $cache->get($key, @_);
276             return unless $val;
277             return ${Storable::thaw($val)};
278             }
279              
280             sub _cacheSet {
281             my $this = shift;
282             my $cache = $this->{cache};
283             return unless defined $cache;
284              
285             my $key = $this->_cacheKey(shift);
286             my $val = shift;
287             $val = Storable::freeze(\$val);
288             return $cache->set($key, $val, @_);
289             }
290              
291             sub _cacheRemove {
292             my $this = shift;
293             my $cache = $this->{cache};
294             return unless defined $cache;
295              
296             my $key = $this->_cacheKey(shift);
297             return $cache->remove($key, @_);
298             }
299              
300             sub _cacheKey {
301             my $this = shift;
302             local $Data::Dumper::Indent = 1;
303             local $Data::Dumper::Terse = 1;
304             local $Data::Dumper::Sortkeys = 1;
305              
306             my $agent = $this->getUseragent;
307             my $user = $agent->{user} || 'guest';
308              
309             # cache per user as data must not leak between users via the cache
310             return _untaint(Digest::MD5::md5_hex(Data::Dumper::Dumper($_[0]).'::'.$user));
311             }
312              
313             =item get($path, %params)
314              
315             does a get against the CMIS service. More than likely, you will not
316             need to call this method. Instead, let the other objects to it for you.
317              
318             =cut
319              
320             sub get {
321             my $this = shift;
322             my $path = shift;
323              
324             my $url;
325             if ($path) {
326             $path =~ s/^(http:\/\/[^\/]+?):80\//$1\//g; # remove bogus port
327             if ($path =~ /^http/) {
328             $url = $path;
329             } else {
330             $path =~ s/^\///g;
331             $url = $this->{repositoryUrl};
332             $url .= '/'.$path;
333             }
334             } else {
335             $url = $this->{repositoryUrl};
336             }
337              
338             my $uri = _getUri($url, @_);
339             _writeCmisDebug("called get($uri)");
340              
341             # do it
342             $this->GET($uri);
343              
344             #_writeCmisDebug("content=".$this->responseContent);
345              
346             my $code = $this->responseCode;
347              
348             return $this->_parseResponse if $code >= 200 && $code < 300;
349             $this->processErrors;
350             }
351              
352             sub _getUri {
353             my $url = shift;
354              
355             my $uri = new URI($url);
356             my %queryParams = ($uri->query_form, @_);
357             $uri->query_form(%queryParams);
358              
359             return $uri;
360             }
361              
362             =item request ( $method, $url, [$body_content, %$headers] )
363              
364             add a cache layer on top of all network connections of the rest client
365              
366             =cut
367              
368             sub request {
369             my $this = shift;
370             my $method = shift;
371             my $url = shift;
372              
373             if($this->{_cacheEntry} = $this->_cacheGet($url)) {
374             _writeCmisDebug("found in cache: $url");
375             $this->{_cacheHits}++;
376             return $this;
377             }
378             #_writeCmisDebug("request url=$url");
379              
380             my $result = $this->SUPER::request($method, $url, @_);
381              
382             # untaint
383             $this->{_res}->content(_untaint($this->{_res}->content));
384             $this->{_res}->code(_untaint($this->{_res}->code));
385             $this->{_res}->status_line(_untaint($this->{_res}->status_line));
386              
387             my $code = $this->responseCode;
388            
389             my $cacheControl = $this->{_res}->header("Cache-Control") || '';
390             #_writeCmisDebug("cacheControl = $cacheControl");
391             $cacheControl = '' if $this->{overrideCacheControl};
392             if ($cacheControl ne 'no-cache' && $code >= 200 && $code < 300 && $this->{cache}) {
393             my $cacheEntry = {
394             content => $this->{_res}->content,
395             code => $this->{_res}->code,
396             status_line => $this->{_res}->status_line,
397             base => $this->{_res}->base,
398             };
399             $this->_cacheSet($url, $cacheEntry);
400             }
401              
402             return $result;
403             }
404              
405             =item responseContent
406              
407             returns the full content of a response
408              
409             =cut
410              
411             sub responseContent {
412             my $this = shift;
413              
414             return $this->{_cacheEntry}{content} if $this->{_cacheEntry};
415             return $this->{_res}->content;
416             }
417              
418             =item responseCode
419              
420             returns the HTTP status code of the repsonse
421              
422             =cut
423              
424             sub responseCode {
425             my $this = shift;
426              
427             return $this->{_cacheEntry}{code} if $this->{_cacheEntry};
428             return $this->{_res}->code;
429             }
430              
431             =item responseStatusLine
432              
433             returns the "code message" of the response. (See HTTP::Status)
434              
435             =cut
436              
437             sub responseStatusLine {
438             my $this = shift;
439              
440             return $this->{_cacheEntry}{status_line} if $this->{_cacheEntry};
441             return $this->{_res}->status_line;
442             }
443              
444             =item responseBase -> $uri
445              
446             returns the base uri for this response
447              
448             =cut
449              
450             sub responseBase {
451             my $this = shift;
452             return $this->{_cacheEntry}{base} if $this->{_cacheEntry};
453             return $this->{_res}->base;
454             }
455              
456             sub _untaint {
457             my $content = shift;
458             if (defined $content && $content =~ /^(.*)$/s) {
459             $content = $1;
460             }
461             return $content;
462             }
463              
464             =item post($path, $payload, $contentType, %params)
465              
466             does a post against the CMIS service. More than likely, you will not
467             need to call this method. Instead, let the other objects to it for you.
468              
469             =cut
470              
471             sub post {
472             my $this = shift;
473             my $path = shift;
474             my $payload = shift;
475             my $contentType = shift;
476             my %params = @_;
477              
478             $path =~ s/^\///g;
479              
480             my $url;
481             if ($path) {
482             $path =~ s/^(http:\/\/[^\/]+?):80\//$1\//g; # remove bogus port
483             if ($path =~ /^$this->{repositoryUrl}/) {
484             $url = $path;
485             } else {
486             $path =~ s/^\///g;
487             $url = $this->{repositoryUrl};
488             $url .= '/'.$path;
489             }
490             } else {
491             $url = $this->{repositoryUrl};
492             }
493              
494             _writeCmisDebug("called post($url)");
495             $params{"Content-Type"} = $contentType;
496              
497             # if ($ENV{CMIS_DEBUG}) {
498             # _writeCmisDebug("post params:\n * ".join("\n * ", map {"$_=$params{$_}"} keys %params));
499             # }
500              
501             # do it
502             $this->POST($url, $payload, \%params);
503              
504             # auto clear the cache
505             $this->clearCache;
506              
507             my $code = $this->responseCode;
508             return $this->_parseResponse if $code >= 200 && $code < 300;
509             $this->processErrors;
510             }
511              
512             =item put($path, $payload, $contentType, %params)
513              
514             does a put against the CMIS service. More than likely, you will not
515             need to call this method. Instead, let the other objects to it for you.
516              
517             =cut
518              
519             sub put {
520             my $this = shift;
521             my $path = shift;
522             my $payload = shift;
523             my $contentType = shift;
524              
525             $path =~ s/^\///g;
526              
527             my $url;
528             if ($path) {
529             $path =~ s/^(http:\/\/[^\/]+?):80\//$1\//g; # remove bogus port
530             if ($path =~ /^$this->{repositoryUrl}/) {
531             $url = $path;
532             } else {
533             $path =~ s/^\///g;
534             $url = $this->{repositoryUrl};
535             $url .= '/'.$path;
536             }
537             } else {
538             $url = $this->{repositoryUrl};
539             }
540              
541             my $uri = _getUri($url, @_);
542             _writeCmisDebug("called put($uri)");
543             _writeCmisDebug("contentType: ".$contentType);
544             #_writeCmisDebug("payload: ".$payload);
545              
546             # auto clear the cache
547             $this->clearCache;
548              
549             # do it
550             $this->PUT($uri, $payload, {"Content-Type"=>$contentType});
551              
552             my $code = $this->responseCode;
553             return $this->_parseResponse if $code >= 200 && $code < 300;
554             $this->processErrors;
555             }
556              
557             =item delete($url, %params)
558              
559             does a delete against the CMIS service. More than likely, you will not
560             need to call this method. Instead, let the other objects to it for you.
561              
562             =cut
563              
564             sub delete {
565             my $this = shift;
566             my $url = shift;
567              
568             my $uri = _getUri($url, @_);
569             _writeCmisDebug("called delete($uri)");
570              
571             $this->DELETE($uri);
572              
573             # auto clear the cache
574             $this->clearCache;
575              
576             my $code = $this->responseCode;
577             return $this->_parseResponse if $code >= 200 && $code < 300;
578             $this->processErrors;
579             }
580              
581             =item processErrors
582              
583             throws a client or a server exception based on the http error code
584             of the last transaction.
585              
586             =cut
587              
588             sub processErrors {
589             my $this = shift;
590              
591             my $code = $this->responseCode;
592              
593             if ($ENV{CMIS_DEBUG}) {
594             _writeCmisDebug("processError($code)");
595             _writeCmisDebug($this->responseContent);
596             }
597              
598             #print STDERR "header:".$this->{_res}->as_string()."\n";
599              
600             if ($code >= 400 && $code < 500) {
601             # SMELL: there's no standardized way of reporting the error properly
602             my $reason = $this->{_res}->header("Title");
603             $reason = $this->responseStatusLine . ' - ' . $reason if defined $reason;
604             throw WebService::Cmis::ClientException($this, $reason);
605             }
606              
607             if ($code >= 500) {
608             throw WebService::Cmis::ServerException($this);
609             }
610              
611             # default
612             throw Error::Simple("unknown client error $code: ".$this->responseStatusLine);
613             }
614              
615             =item getRepositories() -> %repositories;
616              
617             returns a hash of L objects available at this
618             service.
619              
620             =cut
621              
622             sub getRepositories {
623             my $this = shift;
624              
625             _writeCmisDebug("called getRepositories");
626              
627             unless (defined $this->{repositories}) {
628             $this->{repositories} = ();
629              
630             my $doc = $this->get;
631             if (defined $doc) {
632             foreach my $node ($doc->findnodes($CMIS_XPATH_REPOSITORIES)) {
633             my $repo = new WebService::Cmis::Repository($this, $node);
634             $this->{repositories}{$repo->getRepositoryId} = $repo;
635              
636             #SMELL: not covered by the specs, might need a search which one actually is the default one
637             $this->{defaultRepository} = $repo unless defined $this->{defaultRepository};
638             }
639             }
640             }
641              
642             return $this->{repositories};
643             }
644              
645             =item getRepository($id) -> L<$repository|WebService::Cmis::Repository>
646              
647             returns a WebService::Cmis::Repository of the given ID. if
648             ID is undefined the default repository will be returned.
649              
650             =cut
651              
652             sub getRepository {
653             my ($this, $id) = @_;
654              
655             $this->getRepositories;
656             return $this->{defaultRepository} unless defined $id;
657             return $this->{repositories}{$id};
658             }
659              
660             =item getCacheHits() -> $num
661              
662             returns the number of times a result has been fetched from the cache
663             instead of accessing the CMIS backend. returns undefined when no cache
664             is configured
665              
666             =cut
667              
668             sub getCacheHits {
669             my $this = shift;
670              
671             return unless defined $this->{cache};
672             return $this->{_cacheHits};
673             }
674              
675             =item login(%params) -> $ticket
676              
677             Logs in to the web service. returns an identifier for the internal state
678             of the user agent that may be used to login again later on.
679              
680             my $ticket = $client->login(
681             user=> $user,
682             password => $password
683             );
684              
685             $client->login(
686             user => $user,
687             ticket => $ticket
688             );
689              
690             =cut
691              
692             sub login {
693             my $this = shift;
694             return $this->getUseragent->login(@_);
695             }
696              
697             =item logout()
698              
699             Logs out of the web service invalidating a stored state within the auth handler.
700              
701             =cut
702              
703             sub logout {
704             my $this = shift;
705              
706             my $userAgent = $this->getUseragent;
707             $userAgent->logout(@_) if $userAgent;
708             $this->setUseragent;
709             $this->_init;
710             }
711              
712              
713             =back
714              
715             =head1 COPYRIGHT AND LICENSE
716              
717             Copyright 2012-2013 Michael Daum
718              
719             This module is free software; you can redistribute it and/or modify it under
720             the same terms as Perl itself. See F.
721              
722             =cut
723              
724             1;