File Coverage

blib/lib/Fedora/Bugzilla.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             #############################################################################
2             #
3             # An interface to Fedora's Bugzilla instance.
4             #
5             # Author: Chris Weyl (cpan:RSRCHBOY), <cweyl@alumni.drew.edu>
6             # Company: No company, personal work
7             # Created: 12/29/2008 11:06:54 AM PST
8             #
9             # Copyright (c) 2008 Chris Weyl <cweyl@alumni.drew.edu>
10             #
11             # This library is free software; you can redistribute it and/or
12             # modify it under the terms of the GNU Lesser General Public
13             # License as published by the Free Software Foundation; either
14             # version 2.1 of the License, or (at your option) any later version.
15             #
16             #############################################################################
17              
18             package Fedora::Bugzilla;
19              
20             # moose core
21 3     3   98529 use Moose;
  3         2499855  
  3         30  
22 3     3   27733 use Moose::Util::TypeConstraints;
  3         8  
  3         31  
23              
24             # moose extensions
25 3     3   21918 use MooseX::Types::Path::Class qw{ File Dir };
  3         651210  
  3         48  
26 3     3   20724 use MooseX::Types::URI qw{ Uri };
  3         292986  
  3         26  
27 3     3   12421 use MooseX::AttributeHelpers;
  0            
  0            
28              
29             # other fedora bits
30             use Fedora::Bugzilla::Bug;
31             use Fedora::Bugzilla::NewBug;
32             use Fedora::Bugzilla::Bugs;
33             use Fedora::Bugzilla::QueriedBugs;
34             use Fedora::Bugzilla::Types ':all';
35             use Fedora::Bugzilla::XMLRPC;
36              
37             # cpan bits
38             use Path::Class qw{ file dir };
39             use Regexp::Common;
40             use HTTP::Cookies;
41              
42             # debugging
43             #use Smart::Comments '###';
44              
45             use namespace::clean -except => 'meta';
46              
47             our $VERSION = '0.13';
48              
49             ## not needed ATM
50             #subtype 'HTTP::Cookies'
51             # => as Object
52             # => where { $_->isa('HTTP::Cookies') }
53             # ;
54             #
55             #coerce 'HTTP::Cookies'
56             # => from 'Path::Class::File'
57             # => via { HTTP::Cookies->new(file => "$_") }
58             # ;
59              
60             # we could require one or the other be set, but there are many operations we
61             # can do against bugzilla that don't actually require we be logged in
62              
63             has site => (is => 'ro', lazy => 1, isa => Uri, coerce => 1, lazy_build => 1);
64              
65             has userid => (is => 'ro', isa => 'Str', lazy_build => 1);
66             has userid_cb => (is => 'ro', isa => 'CodeRef', lazy_build => 1);
67             has passwd => (is => 'ro', isa => 'Str', lazy_build => 1);
68             has passwd_cb => (is => 'ro', isa => 'CodeRef', lazy_build => 1);
69              
70             sub _build_site { 'https://bugzilla.redhat.com/xmlrpc.cgi' }
71              
72             sub _build_userid { shift->userid_cb->() }
73             sub _build_userid_cb { sub { die 'neither userid nor userid_cb set' } }
74             sub _build_passwd { shift->passwd_cb->() }
75             sub _build_passwd_cb { sub { die 'neither passwd nor userid_cb set' } }
76              
77             has new_bug_class => (is => 'rw', isa => 'Str', lazy_build => 1);
78             has default_bug_class => (is => 'rw', isa => 'Str', lazy_build => 1);
79              
80             sub _build_new_bug_class { 'Fedora::Bugzilla::NewBug' }
81             sub _build_default_bug_class { 'Fedora::Bugzilla::Bug' }
82              
83             has aggressive_fetch => (is => 'rw', isa => 'Bool', lazy_build => 1);
84             sub _build_aggressive_fetch { 1 }
85              
86             # hold our RPC::XML::Client instance
87             has rpc => (is => 'ro', isa => 'Fedora::Bugzilla::XMLRPC', lazy_build => 1);
88              
89             # create our RPC::XML::Client appropriately
90             sub _build_rpc {
91             my $self = shift @_;
92              
93             # twice to keep warnings from complaining...
94             #local $RPC::XML::ENCODING;
95             $RPC::XML::ENCODING = 'UTF-8';
96              
97             my $rpc = Fedora::Bugzilla::XMLRPC->new($self->site, sub { $self->login });
98              
99             # error bits
100             $rpc->error_handler($self->rpc_error_handler);
101             $rpc->fault_handler($self->rpc_fault_handler);
102             $rpc->useragent->cookie_jar($self->cookie_jar);
103             $rpc->useragent->agent($self->ua_agent);
104              
105             return $rpc;
106             }
107              
108             has ua_agent => (is => 'ro', isa => 'Str', lazy_build => 1);
109             has ua => (is => 'ro', isa => 'LWP::UserAgent', lazy_build => 1);
110              
111             sub _build_ua_agent { "Fedora::Bugzilla $VERSION" }
112              
113             sub _build_ua {
114             my $self = shift @_;
115              
116             return LWP::UserAgent->new(
117             cookie_jar => $self->cookie_jar,
118             agent => $self->ua_agent,
119             );
120             }
121              
122             has rpc_error_handler => (is => 'ro', isa => 'CodeRef', lazy_build => 1);
123             has rpc_fault_handler => (is => 'ro', isa => 'CodeRef', lazy_build => 1);
124              
125             sub _build_rpc_error_handler { sub { confess shift } }
126             sub _build_rpc_fault_handler { sub { confess shift->{faultString}->value } }
127            
128             has cookie_file => (is => 'ro', isa => File, coerce => 1, lazy_build => 1);
129             has cookie_jar => (is => 'ro', isa => 'HTTP::Cookies', lazy_build => 1);
130              
131             sub _build_cookie_file { file "$ENV{HOME}/.fedora.bz.cookies.txt" }
132              
133             sub _build_cookie_jar {
134             my $self = shift @_;
135             my $file = $self->cookie_file;
136              
137             # if set to undef, we don't want the cookies to be saved anywhere
138             return HTTP::Cookies->new if not defined $file;
139              
140             # if file exists and is writeable, or dir exists and is writeable, use it
141             return HTTP::Cookies->new(file => $file, autosave => 1)
142             if (-f $file && -w _) || (-d $file->dir && -w _);
143              
144             # otherwise, we have a file defined but we can't write to it / dir
145             warn "cookie_file ($file) is not usable (write errors)";
146             return HTTP::Cookies->new;
147             }
148              
149             # this seems a little magical, but really, makes sense to me :)
150             has login => (
151             is => 'ro',
152             lazy => 1,
153              
154             predicate => 'logged_in',
155              
156             clearer => 'logout',
157             trigger => sub { shift->_logout },
158              
159             default => sub {
160             my $self = shift @_;
161              
162             ### logging in...
163             my $ret = $self->rpc->simple_request(
164             'User.login',
165             {
166             login => $self->userid,
167             password => $self->passwd,
168             }
169             #)->{id};
170             );
171              
172             ### $ret
173             #die;
174              
175             return $ret->{id} if $ret;
176              
177             die 'Could not log in to bugzilla! (password problem?)';
178             },
179             );
180              
181             sub _logout { shift->rpc->simple_request('User.logout') }
182              
183             # Product.get_accessible_products
184             has accessible_products => (
185             is => 'ro',
186             isa => 'ArrayRef[Int]',
187             auto_deref => 1,
188             lazy_build => 1,
189             );
190              
191             ########################################################################
192             # misc bugzilla functionality
193              
194             # Bugzilla.version
195             has version => (is => 'ro', isa => 'Str', lazy_build => 1);
196             sub _build_version { shift->rpc->simple_request('Bugzilla.version')->{version} }
197              
198             # Bugzilla.timezone
199             has timezone => (is => 'ro', isa => 'Str', lazy_build => 1);
200              
201             sub _build_timezone {
202             shift->rpc->simple_request('Bugzilla.timezone')->{timezone}
203             }
204              
205             # User.offer_account_by_email
206             sub offer_account_by_email {
207             my $self = shift @_;
208             my $email = shift @_ || confess 'Must pass email address';
209              
210             my $r = $self->rpc->simple_request(
211             'User.offer_account_by_email',
212             { email => $email},
213             );
214             ### $r
215              
216             return;
217             }
218              
219             # User.create
220             sub create_user {
221             my $self = shift @_;
222             my %info = @_;
223              
224             # FIXME need checking for parameters here...
225             return $self->rpc->simple_request('User.create', \%info)->{id};
226             }
227              
228             ########################################################################
229             # products
230              
231             #has _products => ( ... );
232              
233             # a little sugar to get us to the same name as WWW::Bugzilla3/Bugzilla
234             # internals
235             sub get_accessible_products { shift->accesible_products }
236              
237             sub _build_get_accessible_products {
238             my $self = shift @_;
239              
240             $self->rpc->simple_request('Product.get_accessible_products')->{ids};
241             }
242              
243             ########################################################################
244             # fetch/create/etc bugs
245              
246             sub create_bug {
247             my $self = shift @_;
248             my $nb;
249              
250             if ( ! (blessed $_[0] && $_[0]->isa('Fedora::Bugzilla::NewBug')) ) {
251              
252             # we wern't passed a new bug object, so let's create one.
253             $nb = $self->new_bug_class->new(@_);
254             }
255             else {
256            
257             $nb = shift @_;
258             }
259              
260             # actually create the bug on the server
261             my $id = $self->_create_bug($nb->bughash);
262              
263             return Fedora::Bugzilla::Bug->new(bz => $self, id => $id);
264             }
265              
266              
267             # Bug.create
268             sub _create_bug {
269             my $self = shift @_;
270             my $bughash = shift @_;
271              
272             # FIXME this needs work
273              
274             #my $req = RPC::XML::request->new('Bug.create', $bughash);
275              
276             # no validation!
277             return $self->rpc->simple_request('Bug.create', $bughash)->{id};
278             }
279              
280             sub get_bug { shift->bug(@_) }
281              
282             sub bug {
283             my $self = shift @_;
284             my $bug = shift @_ || confess 'Must pass bug id or alias';
285             my $class = shift @_ || $self->default_bug_class;
286            
287             # invoke accordingly
288             return $class->new(bz => $self, id => $bug) if $bug =~ $RE{num}{int};
289             return $class->new(bz => $self, alias => $bug);
290             }
291              
292             sub get_bugs { shift->bugs(@_) }
293              
294             # Bug.get_bugs
295             sub bugs { Fedora::Bugzilla::Bugs->new(bz => shift, ids => [ @_ ]) };
296            
297             sub get_bug_fields { shift->all_legal_bug_fields }
298              
299             # bugzilla.getBugFields
300             has all_legal_bug_fields => (
301             metaclass => 'Collection::List',
302              
303             is => 'ro',
304             isa => 'ArrayRef[Str]',
305              
306             auto_deref => 1,
307             lazy_build => 1,
308              
309             # provides ...
310             );
311              
312             sub _build_all_legal_bug_fields {
313             my $self = shift @_;
314              
315             my $fields = $self
316             ->rpc
317             ->simple_request('bugzilla.getBugFields')
318             ;
319              
320             return [ sort @$fields ];
321             }
322              
323             ########################################################################
324             # Searching...
325              
326             # this is still pretty experimental, and seems to be specific to RHBZ at the
327             # moment.
328              
329             has queryinfo => (
330             is => 'ro',
331             isa => 'HashRef',
332             lazy_build => 1,
333             );
334              
335             sub _build_queryinfo {
336             my $self = shift @_;
337              
338             warn q{This probably won't work; if it does, please email the author};
339              
340             my $foo = $self->rpc->simple_request('bugzilla.getQueryInfo');
341              
342             return $foo;
343             }
344              
345             =begin comment
346              
347             https://fedorahosted.org/python-bugzilla/browser/bugzilla/rhbugzilla.py
348              
349             def _query(self,query):
350             '''Query bugzilla and return a list of matching bugs.
351             query must be a dict with fields like those in in querydata['fields'].
352             You can also pass in keys called 'quicksearch' or 'savedsearch' -
353             'quicksearch' will do a quick keyword search like the simple search
354             on the Bugzilla home page.
355             'savedsearch' should be the name of a previously-saved search to
356             execute. You need to be logged in for this to work.
357             Returns a dict like this: {'bugs':buglist,
358             'sql':querystring}
359             buglist is a list of dicts describing bugs, and 'sql' contains the SQL
360             generated by executing the search.
361             '''
362             return self._proxy.Bug.search(query)
363              
364             =end comment
365             =cut
366              
367             # Bug.search
368             sub search { shift->_query(@_) }
369             sub query { shift->_query(@_) }
370              
371             sub run_named_query { shift->_query(savedsearch => shift) }
372             sub run_savedsearch { shift->run_named_query(@_) }
373             sub run_quicksearch { shift->_query(quicksearch => join(' ', @_)) }
374              
375             sub _query {
376             my $self = shift @_;
377              
378             my $ret = $self->rpc->simple_request('Bug.search', { @_ });
379            
380             # FIXME nuke?
381             $self->last_sql($ret->{sql});
382              
383             return Fedora::Bugzilla::QueriedBugs->new(
384             bz => $self,
385             raw => $ret->{bugs},
386             sql => $ret->{sql},
387             display_columns => $ret->{displaycolumns},
388             );
389             }
390              
391             has last_sql => (
392             is => 'rw',
393             isa => 'Str',
394             predicate => 'has_last_sql',
395             clearer => 'clear_last_sql',
396             );
397              
398             ########################################################################
399             # magic end bits
400              
401             1;
402              
403             __END__
404              
405             =head1 NAME
406              
407             Fedora::Bugzilla - Interact with Fedora's bugzilla instance
408              
409             =head1 SYNOPSIS
410              
411             use Fedora::Bugzilla;
412              
413             =for author to fill in:
414             Brief code example(s) here showing commonest usage(s).
415             This section will be as far as many users bother reading
416             so make it as educational and exeplary as possible.
417              
418             =head1 DESCRIPTION
419              
420             The XML-RPC interface to bugzilla is a quite useful, and while bugzilla 3.x
421             is starting to flesh their interface out a bit more (see, e.g.,
422             L<WWW::Bugzilla3>), Fedora's bugzilla implementation has a large number of
423             custom methods. This module aims to expose them, in a kinder, gentler way.
424              
425             In addition to the XML-RPC methods Bugzilla makes available, there are also
426             some things we only seem to be able to access via the web/XML interfaces.
427             (See, e.g., the flags, attachments and comments functionality.) This package
428             works to expose those as well.
429              
430             Some functionality is more expensive to invoke than others, for a variety of
431             reasons. We strive to only access each bit as we need it, to minimize time
432             and effort while still making available as much as is possible.
433              
434             (And, yes, I know it's really RedHat's bugzilla. Some day... oh yes, some
435             day...)
436              
437             =head1 INTERFACE
438              
439             "Release Early, Release Often"
440              
441             I've tried to get at least the methods I use in here. I know I'm missing
442             some, and I bet there are others I don't even know about... I'll try not to,
443             but I won't guarantee that I won't change the api in some incompatable way.
444             If you'd like to see something here, please either drop me a line (see AUTHOR)
445             or better yet, open a ticket with a patch ;)
446              
447             Note also, the documentation is woefully incomplete.
448              
449             =head2 METHODS
450              
451             =over
452              
453             =item B<new>
454              
455             Standard constructor. Takes a number of arguments, two of which are
456             required; note that each of these arguments is also available through an
457             accessor of the same name once the object instance has been created.
458              
459             =over
460              
461             =item I<userid =E<gt> Str>
462              
463             B<Required.> Your bugzilla userid (generally your email address).
464              
465             =item I<passwd =E<gt> Str>
466              
467             B<Required.> Your bugzilla password.
468              
469             =item I<site =E<gt> Str|URI>
470              
471             The URI of the interface you're trying to access. Note this (correctly)
472             defaults to L<https://bugzilla.redhat.com/xmlrpc.cgi>.
473              
474             =item I<cookie_file =E<gt> Str|Path::Class::File>
475              
476             Takes a filename to give to the RPC's useragent instance as file to hold the
477             bugzilla cookies in. Set to undef to use no actual file, and just cache
478             cookies in-memory.
479              
480             Defaults to: "$ENV{HOME}/.fedora.bz.cookies.txt";
481              
482             =back
483              
484             =item B<login>
485              
486             Log in to the bugzilla service.
487              
488             =item B<logged_in>
489              
490             True if we're logged in, false otherwise.
491              
492             =item B<logout>
493              
494             Log out from the bugzilla service.
495              
496             =back
497              
498             =head2 BUG CREATION
499              
500             =over
501              
502             =item B<create_bug>
503              
504             Creates a new bug, passing @_ to the constructor of the default new bug class.
505             See L<Fedora::Bugzilla::NewBug>.
506              
507             =item B<new_bug_class>
508              
509             Gets/sets the class used to create new bugs with.
510              
511             =back
512              
513             =head2 FETCHING BUGS
514              
515             =over
516              
517             =item B<bug, get_bug (Int|Str)>
518              
519             Given a bug id/alias, returns a corresponding L<Fedora::Bugzilla::Bug>.
520              
521             =item B<bugs, get_bugs (Int|Str, ...)>
522              
523             Given a list of bug id/aliases, return a L<Fedora::Bugzilla::Bugs> object.
524              
525             =back
526              
527             =head2 SEARCHING AND QUERYING
528              
529             These functions return a L<Fedora::Bugzilla::Bugs> object representing the
530             results of the query.
531              
532             =over
533              
534             =item B<run_savedsearch(Str)>
535              
536             Given the name of a saved search, run it and return the bugs.
537              
538             =item B<run_named_query(Str)>
539              
540             Alias to run_savedsearch().
541              
542             =item B<run_quicksearch(Str, ...)>
543              
544             Given a number of search terms, submit to Bugzilla for a quicksearch (akin to
545             entering terms on the web UI).
546              
547             =back
548              
549             =head2 MISC SERVER METHODS
550              
551             =over
552              
553             =item B<accessible_products>
554              
555             FIXME. Returns an array of products the user can search or enter bugs against.
556              
557             =item B<version>
558              
559             Returns the version of the bugzilla server.
560              
561             =item B<timezone>
562              
563             Returns the timezone the bugzilla server is in.
564              
565             =item B<offer_account_by_email (email address)>
566              
567             Sends an offer of a Bugzilla account to the given email address.
568              
569             =back
570              
571             =head1 SPEED
572              
573             We've tried to take steps to make sure things are speedy: "non-changing"
574             values are cached and only pulled when needed, etc. While we don't
575             implement a multicall queued approach (yet), we do try to minimize the number
576             of queries required; e.g. by using Bug.get_bugs when multiple bugs are needed.
577              
578             Some of the functionality requires that the XML representation of the bug be
579             pulled (e.g. flags, comments, attachment listings, etc); in these cases we
580             don't do the actual pull until requested.
581              
582             For methods that return more than one bug wrapped in a
583             L<Fedora::Bugzilla::Bugs> object, we fetch all the bug data through one XMLRPC
584             call once someone tries to access any of the bug data in it (e.g. bugs(),
585             num_bugs(), etc). Additionally, if I<aggressive_fetch> is set in the parent
586             Fedora::Bugzilla object, we'll pull down the XML and any other data we need
587             for each bug. Pulling all the data at one time can result in significant time
588             savings over having each bug object pull their own.
589              
590             =head2 Updates
591              
592             Note that performing an action on a bug that changes any value will result in
593             all data (save the id) being discarded, and reloaded the next time the bug is
594             accessed. It's best to pull any information you may need _before_ updating
595             the bug, if the situation warrants it, to avoid the second call to the
596             Bugzilla server.
597              
598             =head1 DIAGNOSTICS
599              
600             At the moment, we generally die() or confess() any errors.
601              
602             =head1 BUGS, LIMITATIONS AND VERSION CONTROL
603              
604             Source, tickets, etc can be all accessed through the Camelus project at
605             fedorahosted.org. Please use the 'Fedora-Bugzilla' component when reporting
606             issues or making feature requests:
607              
608             L<http://camelus.fedorahosted.org>
609              
610             There are still many areas of functionality we do not handle yet. If you'd
611             like to see something in here, specific or otherwise, please make a feature
612             request through the trac ticketing interface.
613              
614             =head1 SEE ALSO
615              
616             L<http://www.bugzilla.org>, L<http://bugzilla.redhat.com>,
617             L<http://python-bugzilla.fedorahosted.org>, the L<WWW::Bugzilla3> module.
618              
619             =head1 AUTHOR
620              
621             Chris Weyl C<< <cweyl@alumni.drew.edu> >>
622              
623              
624             =head1 LICENCE AND COPYRIGHT
625              
626             Copyright (c) 2008, Chris Weyl <cweyl@alumni.drew.edu>
627              
628             This library is free software; you can redistribute it and/or modify it under
629             the terms of the GNU Lesser General Public License as published by the Free
630             Software Foundation; either version 2.1 of the License, or (at your option)
631             any later version.
632              
633             This library is distributed in the hope that it will be useful, but WITHOUT
634             ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
635             OR A PARTICULAR PURPOSE.
636              
637             See the GNU Lesser General Public License for more details.
638              
639             You should have received a copy of the GNU Lesser General Public License
640             along with this library; if not, write to the
641              
642             Free Software Foundation, Inc.,
643             59 Temple Place, Suite 330,
644             Boston, MA 02111-1307 USA
645              
646             =cut