Bug.pm 124 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518
  1. # -*- Mode: perl; indent-tabs-mode: nil -*-
  2. #
  3. # The contents of this file are subject to the Mozilla Public
  4. # License Version 1.1 (the "License"); you may not use this file
  5. # except in compliance with the License. You may obtain a copy of
  6. # the License at http://www.mozilla.org/MPL/
  7. #
  8. # Software distributed under the License is distributed on an "AS
  9. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  10. # implied. See the License for the specific language governing
  11. # rights and limitations under the License.
  12. #
  13. # The Original Code is the Bugzilla Bug Tracking System.
  14. #
  15. # The Initial Developer of the Original Code is Netscape Communications
  16. # Corporation. Portions created by Netscape are
  17. # Copyright (C) 1998 Netscape Communications Corporation. All
  18. # Rights Reserved.
  19. #
  20. # Contributor(s): Dawn Endico <endico@mozilla.org>
  21. # Terry Weissman <terry@mozilla.org>
  22. # Chris Yeh <cyeh@bluemartini.com>
  23. # Bradley Baetz <bbaetz@acm.org>
  24. # Dave Miller <justdave@bugzilla.org>
  25. # Max Kanat-Alexander <mkanat@bugzilla.org>
  26. # Frédéric Buclin <LpSolit@gmail.com>
  27. # Lance Larsh <lance.larsh@oracle.com>
  28. package Bugzilla::Bug;
  29. use strict;
  30. use Bugzilla::Attachment;
  31. use Bugzilla::Constants;
  32. use Bugzilla::Field;
  33. use Bugzilla::Flag;
  34. use Bugzilla::FlagType;
  35. use Bugzilla::Hook;
  36. use Bugzilla::Keyword;
  37. use Bugzilla::User;
  38. use Bugzilla::Util;
  39. use Bugzilla::Error;
  40. use Bugzilla::Product;
  41. use Bugzilla::Component;
  42. use Bugzilla::Group;
  43. use Bugzilla::Status;
  44. use List::Util qw(min);
  45. use Storable qw(dclone);
  46. use base qw(Bugzilla::Object Exporter);
  47. @Bugzilla::Bug::EXPORT = qw(
  48. bug_alias_to_id ValidateBugID
  49. RemoveVotes CheckIfVotedConfirmed
  50. LogActivityEntry
  51. editable_bug_fields
  52. SPECIAL_STATUS_WORKFLOW_ACTIONS
  53. );
  54. #####################################################################
  55. # Constants
  56. #####################################################################
  57. use constant DB_TABLE => 'bugs';
  58. use constant ID_FIELD => 'bug_id';
  59. use constant NAME_FIELD => 'alias';
  60. use constant LIST_ORDER => ID_FIELD;
  61. # This is a sub because it needs to call other subroutines.
  62. sub DB_COLUMNS {
  63. my $dbh = Bugzilla->dbh;
  64. my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
  65. Bugzilla->active_custom_fields;
  66. my @custom_names = map {$_->name} @custom;
  67. return qw(
  68. alias
  69. assigned_to
  70. bug_file_loc
  71. bug_id
  72. bug_severity
  73. bug_status
  74. cclist_accessible
  75. component_id
  76. delta_ts
  77. estimated_time
  78. everconfirmed
  79. op_sys
  80. priority
  81. product_id
  82. qa_contact
  83. remaining_time
  84. rep_platform
  85. reporter_accessible
  86. resolution
  87. short_desc
  88. status_whiteboard
  89. target_milestone
  90. version
  91. ),
  92. 'reporter AS reporter_id',
  93. $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
  94. $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
  95. @custom_names;
  96. }
  97. use constant REQUIRED_CREATE_FIELDS => qw(
  98. component
  99. product
  100. short_desc
  101. version
  102. );
  103. # There are also other, more complex validators that are called
  104. # from run_create_validators.
  105. sub VALIDATORS {
  106. my $validators = {
  107. alias => \&_check_alias,
  108. bug_file_loc => \&_check_bug_file_loc,
  109. bug_severity => \&_check_bug_severity,
  110. comment => \&_check_comment,
  111. commentprivacy => \&_check_commentprivacy,
  112. deadline => \&_check_deadline,
  113. estimated_time => \&_check_estimated_time,
  114. op_sys => \&_check_op_sys,
  115. priority => \&_check_priority,
  116. product => \&_check_product,
  117. remaining_time => \&_check_remaining_time,
  118. rep_platform => \&_check_rep_platform,
  119. short_desc => \&_check_short_desc,
  120. status_whiteboard => \&_check_status_whiteboard,
  121. };
  122. # Set up validators for custom fields.
  123. foreach my $field (Bugzilla->active_custom_fields) {
  124. my $validator;
  125. if ($field->type == FIELD_TYPE_SINGLE_SELECT) {
  126. $validator = \&_check_select_field;
  127. }
  128. elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
  129. $validator = \&_check_multi_select_field;
  130. }
  131. elsif ($field->type == FIELD_TYPE_DATETIME) {
  132. $validator = \&_check_datetime_field;
  133. }
  134. elsif ($field->type == FIELD_TYPE_FREETEXT) {
  135. $validator = \&_check_freetext_field;
  136. }
  137. else {
  138. $validator = \&_check_default_field;
  139. }
  140. $validators->{$field->name} = $validator;
  141. }
  142. return $validators;
  143. };
  144. use constant UPDATE_VALIDATORS => {
  145. assigned_to => \&_check_assigned_to,
  146. bug_status => \&_check_bug_status,
  147. cclist_accessible => \&Bugzilla::Object::check_boolean,
  148. dup_id => \&_check_dup_id,
  149. qa_contact => \&_check_qa_contact,
  150. reporter_accessible => \&Bugzilla::Object::check_boolean,
  151. resolution => \&_check_resolution,
  152. target_milestone => \&_check_target_milestone,
  153. version => \&_check_version,
  154. };
  155. sub UPDATE_COLUMNS {
  156. my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
  157. Bugzilla->active_custom_fields;
  158. my @custom_names = map {$_->name} @custom;
  159. my @columns = qw(
  160. alias
  161. assigned_to
  162. bug_file_loc
  163. bug_severity
  164. bug_status
  165. cclist_accessible
  166. component_id
  167. deadline
  168. estimated_time
  169. everconfirmed
  170. op_sys
  171. priority
  172. product_id
  173. qa_contact
  174. remaining_time
  175. rep_platform
  176. reporter_accessible
  177. resolution
  178. short_desc
  179. status_whiteboard
  180. target_milestone
  181. version
  182. );
  183. push(@columns, @custom_names);
  184. return @columns;
  185. };
  186. use constant NUMERIC_COLUMNS => qw(
  187. estimated_time
  188. remaining_time
  189. );
  190. sub DATE_COLUMNS {
  191. my @fields = Bugzilla->get_fields(
  192. { custom => 1, type => FIELD_TYPE_DATETIME });
  193. return map { $_->name } @fields;
  194. }
  195. # This is used by add_comment to know what we validate before putting in
  196. # the DB.
  197. use constant UPDATE_COMMENT_COLUMNS => qw(
  198. thetext
  199. work_time
  200. type
  201. extra_data
  202. isprivate
  203. );
  204. # Used in LogActivityEntry(). Gives the max length of lines in the
  205. # activity table.
  206. use constant MAX_LINE_LENGTH => 254;
  207. use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
  208. none
  209. duplicate
  210. change_resolution
  211. clearresolution
  212. );
  213. #####################################################################
  214. sub new {
  215. my $invocant = shift;
  216. my $class = ref($invocant) || $invocant;
  217. my $param = shift;
  218. # If we get something that looks like a word (not a number),
  219. # make it the "name" param.
  220. if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
  221. # But only if aliases are enabled.
  222. if (Bugzilla->params->{'usebugaliases'} && $param) {
  223. $param = { name => $param };
  224. }
  225. else {
  226. # Aliases are off, and we got something that's not a number.
  227. my $error_self = {};
  228. bless $error_self, $class;
  229. $error_self->{'bug_id'} = $param;
  230. $error_self->{'error'} = 'InvalidBugId';
  231. return $error_self;
  232. }
  233. }
  234. unshift @_, $param;
  235. my $self = $class->SUPER::new(@_);
  236. # Bugzilla::Bug->new always returns something, but sets $self->{error}
  237. # if the bug wasn't found in the database.
  238. if (!$self) {
  239. my $error_self = {};
  240. bless $error_self, $class;
  241. $error_self->{'bug_id'} = ref($param) ? $param->{name} : $param;
  242. $error_self->{'error'} = 'NotFound';
  243. return $error_self;
  244. }
  245. return $self;
  246. }
  247. # Docs for create() (there's no POD in this file yet, but we very
  248. # much need this documented right now):
  249. #
  250. # The same as Bugzilla::Object->create. Parameters are only required
  251. # if they say so below.
  252. #
  253. # Params:
  254. #
  255. # C<product> - B<Required> The name of the product this bug is being
  256. # filed against.
  257. # C<component> - B<Required> The name of the component this bug is being
  258. # filed against.
  259. #
  260. # C<bug_severity> - B<Required> The severity for the bug, a string.
  261. # C<creation_ts> - B<Required> A SQL timestamp for when the bug was created.
  262. # C<short_desc> - B<Required> A summary for the bug.
  263. # C<op_sys> - B<Required> The OS the bug was found against.
  264. # C<priority> - B<Required> The initial priority for the bug.
  265. # C<rep_platform> - B<Required> The platform the bug was found against.
  266. # C<version> - B<Required> The version of the product the bug was found in.
  267. #
  268. # C<alias> - An alias for this bug. Will be ignored if C<usebugaliases>
  269. # is off.
  270. # C<target_milestone> - When this bug is expected to be fixed.
  271. # C<status_whiteboard> - A string.
  272. # C<bug_status> - The initial status of the bug, a string.
  273. # C<bug_file_loc> - The URL field.
  274. #
  275. # C<assigned_to> - The full login name of the user who the bug is
  276. # initially assigned to.
  277. # C<qa_contact> - The full login name of the QA Contact for this bug.
  278. # Will be ignored if C<useqacontact> is off.
  279. #
  280. # C<estimated_time> - For time-tracking. Will be ignored if
  281. # C<timetrackinggroup> is not set, or if the current
  282. # user is not a member of the timetrackinggroup.
  283. # C<deadline> - For time-tracking. Will be ignored for the same
  284. # reasons as C<estimated_time>.
  285. sub create {
  286. my ($class, $params) = @_;
  287. my $dbh = Bugzilla->dbh;
  288. $dbh->bz_start_transaction();
  289. # These fields have default values which we can use if they are undefined.
  290. $params->{bug_severity} = Bugzilla->params->{defaultseverity}
  291. unless defined $params->{bug_severity};
  292. $params->{priority} = Bugzilla->params->{defaultpriority}
  293. unless defined $params->{priority};
  294. $params->{op_sys} = Bugzilla->params->{defaultopsys}
  295. unless defined $params->{op_sys};
  296. $params->{rep_platform} = Bugzilla->params->{defaultplatform}
  297. unless defined $params->{rep_platform};
  298. # Make sure a comment is always defined.
  299. $params->{comment} = '' unless defined $params->{comment};
  300. $class->check_required_create_fields($params);
  301. $params = $class->run_create_validators($params);
  302. # These are not a fields in the bugs table, so we don't pass them to
  303. # insert_create_data.
  304. my $cc_ids = delete $params->{cc};
  305. my $groups = delete $params->{groups};
  306. my $depends_on = delete $params->{dependson};
  307. my $blocked = delete $params->{blocked};
  308. my ($comment, $privacy) = ($params->{comment}, $params->{commentprivacy});
  309. delete $params->{comment};
  310. delete $params->{commentprivacy};
  311. # Set up the keyword cache for bug creation.
  312. my $keywords = $params->{keywords};
  313. $params->{keywords} = join(', ', sort {lc($a) cmp lc($b)}
  314. map($_->name, @$keywords));
  315. # We don't want the bug to appear in the system until it's correctly
  316. # protected by groups.
  317. my $timestamp = delete $params->{creation_ts};
  318. my $ms_values = $class->_extract_multi_selects($params);
  319. my $bug = $class->insert_create_data($params);
  320. # Add the group restrictions
  321. my $sth_group = $dbh->prepare(
  322. 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
  323. foreach my $group_id (@$groups) {
  324. $sth_group->execute($bug->bug_id, $group_id);
  325. }
  326. $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef,
  327. $timestamp, $bug->bug_id);
  328. # Update the bug instance as well
  329. $bug->{creation_ts} = $timestamp;
  330. # Add the CCs
  331. my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)');
  332. foreach my $user_id (@$cc_ids) {
  333. $sth_cc->execute($bug->bug_id, $user_id);
  334. }
  335. # Add in keywords
  336. my $sth_keyword = $dbh->prepare(
  337. 'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)');
  338. foreach my $keyword_id (map($_->id, @$keywords)) {
  339. $sth_keyword->execute($bug->bug_id, $keyword_id);
  340. }
  341. # Set up dependencies (blocked/dependson)
  342. my $sth_deps = $dbh->prepare(
  343. 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
  344. my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
  345. foreach my $depends_on_id (@$depends_on) {
  346. $sth_deps->execute($bug->bug_id, $depends_on_id);
  347. # Log the reverse action on the other bug.
  348. LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
  349. $bug->{reporter_id}, $timestamp);
  350. $sth_bug_time->execute($timestamp, $depends_on_id);
  351. }
  352. foreach my $blocked_id (@$blocked) {
  353. $sth_deps->execute($blocked_id, $bug->bug_id);
  354. # Log the reverse action on the other bug.
  355. LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
  356. $bug->{reporter_id}, $timestamp);
  357. $sth_bug_time->execute($timestamp, $blocked_id);
  358. }
  359. # Insert the values into the multiselect value tables
  360. foreach my $field (keys %$ms_values) {
  361. $dbh->do("DELETE FROM bug_$field where bug_id = ?",
  362. undef, $bug->bug_id);
  363. foreach my $value ( @{$ms_values->{$field}} ) {
  364. $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)",
  365. undef, $bug->bug_id, $value);
  366. }
  367. }
  368. # And insert the comment. We always insert a comment on bug creation,
  369. # but sometimes it's blank.
  370. my @columns = qw(bug_id who bug_when thetext);
  371. my @values = ($bug->bug_id, $bug->{reporter_id}, $timestamp, $comment);
  372. # We don't include the "isprivate" column unless it was specified.
  373. # This allows it to fall back to its database default.
  374. if (defined $privacy) {
  375. push(@columns, 'isprivate');
  376. push(@values, $privacy);
  377. }
  378. my $qmarks = "?," x @columns;
  379. chop($qmarks);
  380. $dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
  381. VALUES ($qmarks)", undef, @values);
  382. $dbh->bz_commit_transaction();
  383. # Because MySQL doesn't support transactions on the fulltext table,
  384. # we do this after we've committed the transaction. That way we're
  385. # sure we're inserting a good Bug ID.
  386. $bug->_sync_fulltext('new bug');
  387. return $bug;
  388. }
  389. sub run_create_validators {
  390. my $class = shift;
  391. my $params = $class->SUPER::run_create_validators(@_);
  392. my $product = $params->{product};
  393. $params->{product_id} = $product->id;
  394. delete $params->{product};
  395. ($params->{bug_status}, $params->{everconfirmed})
  396. = $class->_check_bug_status($params->{bug_status}, $product,
  397. $params->{comment});
  398. $params->{target_milestone} = $class->_check_target_milestone(
  399. $params->{target_milestone}, $product);
  400. $params->{version} = $class->_check_version($params->{version}, $product);
  401. $params->{keywords} = $class->_check_keywords($params->{keywords}, $product);
  402. $params->{groups} = $class->_check_groups($product,
  403. $params->{groups});
  404. my $component = $class->_check_component($params->{component}, $product);
  405. $params->{component_id} = $component->id;
  406. delete $params->{component};
  407. $params->{assigned_to} =
  408. $class->_check_assigned_to($params->{assigned_to}, $component);
  409. $params->{qa_contact} =
  410. $class->_check_qa_contact($params->{qa_contact}, $component);
  411. $params->{cc} = $class->_check_cc($component, $params->{cc});
  412. # Callers cannot set Reporter, currently.
  413. $params->{reporter} = $class->_check_reporter();
  414. $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
  415. $params->{delta_ts} = $params->{creation_ts};
  416. if ($params->{estimated_time}) {
  417. $params->{remaining_time} = $params->{estimated_time};
  418. }
  419. $class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
  420. $params->{qa_contact}, $product);
  421. ($params->{dependson}, $params->{blocked}) =
  422. $class->_check_dependencies($params->{dependson}, $params->{blocked},
  423. $product);
  424. # You can't set these fields on bug creation (or sometimes ever).
  425. delete $params->{resolution};
  426. delete $params->{votes};
  427. delete $params->{lastdiffed};
  428. delete $params->{bug_id};
  429. return $params;
  430. }
  431. sub update {
  432. my $self = shift;
  433. my $dbh = Bugzilla->dbh;
  434. # XXX This is just a temporary hack until all updating happens
  435. # inside this function.
  436. my $delta_ts = shift || $dbh->selectrow_array("SELECT NOW()");
  437. my $old_bug = $self->new($self->id);
  438. my $changes = $self->SUPER::update(@_);
  439. # Certain items in $changes have to be fixed so that they hold
  440. # a name instead of an ID.
  441. foreach my $field (qw(product_id component_id)) {
  442. my $change = delete $changes->{$field};
  443. if ($change) {
  444. my $new_field = $field;
  445. $new_field =~ s/_id$//;
  446. $changes->{$new_field} =
  447. [$self->{"_old_${new_field}_name"}, $self->$new_field];
  448. }
  449. }
  450. foreach my $field (qw(qa_contact assigned_to)) {
  451. if ($changes->{$field}) {
  452. my ($from, $to) = @{ $changes->{$field} };
  453. $from = $old_bug->$field->login if $from;
  454. $to = $self->$field->login if $to;
  455. $changes->{$field} = [$from, $to];
  456. }
  457. }
  458. # CC
  459. my @old_cc = map {$_->id} @{$old_bug->cc_users};
  460. my @new_cc = map {$_->id} @{$self->cc_users};
  461. my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc);
  462. if (scalar @$removed_cc) {
  463. $dbh->do('DELETE FROM cc WHERE bug_id = ? AND '
  464. . $dbh->sql_in('who', $removed_cc), undef, $self->id);
  465. }
  466. foreach my $user_id (@$added_cc) {
  467. $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)',
  468. undef, $self->id, $user_id);
  469. }
  470. # If any changes were found, record it in the activity log
  471. if (scalar @$removed_cc || scalar @$added_cc) {
  472. my $removed_users = Bugzilla::User->new_from_list($removed_cc);
  473. my $added_users = Bugzilla::User->new_from_list($added_cc);
  474. my $removed_names = join(', ', (map {$_->login} @$removed_users));
  475. my $added_names = join(', ', (map {$_->login} @$added_users));
  476. $changes->{cc} = [$removed_names, $added_names];
  477. }
  478. # Keywords
  479. my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects};
  480. my @new_kw_ids = map { $_->id } @{$self->keyword_objects};
  481. my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids);
  482. if (scalar @$removed_kw) {
  483. $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND '
  484. . $dbh->sql_in('keywordid', $removed_kw), undef, $self->id);
  485. }
  486. foreach my $keyword_id (@$added_kw) {
  487. $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)',
  488. undef, $self->id, $keyword_id);
  489. }
  490. $dbh->do('UPDATE bugs SET keywords = ? WHERE bug_id = ?', undef,
  491. $self->keywords, $self->id);
  492. # If any changes were found, record it in the activity log
  493. if (scalar @$removed_kw || scalar @$added_kw) {
  494. my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw);
  495. my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw);
  496. my $removed_names = join(', ', (map {$_->name} @$removed_keywords));
  497. my $added_names = join(', ', (map {$_->name} @$added_keywords));
  498. $changes->{keywords} = [$removed_names, $added_names];
  499. }
  500. # Dependencies
  501. foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) {
  502. my ($type, $other) = @$pair;
  503. my $old = $old_bug->$type;
  504. my $new = $self->$type;
  505. my ($removed, $added) = diff_arrays($old, $new);
  506. foreach my $removed_id (@$removed) {
  507. $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?",
  508. undef, $removed_id, $self->id);
  509. # Add an activity entry for the other bug.
  510. LogActivityEntry($removed_id, $other, $self->id, '',
  511. Bugzilla->user->id, $delta_ts);
  512. # Update delta_ts on the other bug so that we trigger mid-airs.
  513. $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
  514. undef, $delta_ts, $removed_id);
  515. }
  516. foreach my $added_id (@$added) {
  517. $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
  518. undef, $added_id, $self->id);
  519. # Add an activity entry for the other bug.
  520. LogActivityEntry($added_id, $other, '', $self->id,
  521. Bugzilla->user->id, $delta_ts);
  522. # Update delta_ts on the other bug so that we trigger mid-airs.
  523. $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
  524. undef, $delta_ts, $added_id);
  525. }
  526. if (scalar(@$removed) || scalar(@$added)) {
  527. $changes->{$type} = [join(', ', @$removed), join(', ', @$added)];
  528. }
  529. }
  530. # Groups
  531. my %old_groups = map {$_->id => $_} @{$old_bug->groups_in};
  532. my %new_groups = map {$_->id => $_} @{$self->groups_in};
  533. my ($removed_gr, $added_gr) = diff_arrays([keys %old_groups],
  534. [keys %new_groups]);
  535. if (scalar @$removed_gr || scalar @$added_gr) {
  536. if (@$removed_gr) {
  537. my $qmarks = join(',', ('?') x @$removed_gr);
  538. $dbh->do("DELETE FROM bug_group_map
  539. WHERE bug_id = ? AND group_id IN ($qmarks)", undef,
  540. $self->id, @$removed_gr);
  541. }
  542. my $sth_insert = $dbh->prepare(
  543. 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)');
  544. foreach my $gid (@$added_gr) {
  545. $sth_insert->execute($self->id, $gid);
  546. }
  547. my @removed_names = map { $old_groups{$_}->name } @$removed_gr;
  548. my @added_names = map { $new_groups{$_}->name } @$added_gr;
  549. $changes->{'bug_group'} = [join(', ', @removed_names),
  550. join(', ', @added_names)];
  551. }
  552. # Comments
  553. foreach my $comment (@{$self->{added_comments} || []}) {
  554. my $columns = join(',', keys %$comment);
  555. my @values = values %$comment;
  556. my $qmarks = join(',', ('?') x @values);
  557. $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, $columns)
  558. VALUES (?,?,?,$qmarks)", undef,
  559. $self->bug_id, Bugzilla->user->id, $delta_ts, @values);
  560. if ($comment->{work_time}) {
  561. LogActivityEntry($self->id, "work_time", "", $comment->{work_time},
  562. Bugzilla->user->id, $delta_ts);
  563. }
  564. }
  565. foreach my $comment_id (keys %{$self->{comment_isprivate} || {}}) {
  566. $dbh->do("UPDATE longdescs SET isprivate = ? WHERE comment_id = ?",
  567. undef, $self->{comment_isprivate}->{$comment_id}, $comment_id);
  568. # XXX It'd be nice to track this in the bug activity.
  569. }
  570. # Insert the values into the multiselect value tables
  571. my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT}
  572. Bugzilla->active_custom_fields;
  573. foreach my $field (@multi_selects) {
  574. my $name = $field->name;
  575. my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name);
  576. if (scalar @$removed || scalar @$added) {
  577. $changes->{$name} = [join(', ', @$removed), join(', ', @$added)];
  578. $dbh->do("DELETE FROM bug_$name where bug_id = ?",
  579. undef, $self->id);
  580. foreach my $value (@{$self->$name}) {
  581. $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)",
  582. undef, $self->id, $value);
  583. }
  584. }
  585. }
  586. # Log bugs_activity items
  587. # XXX Eventually, when bugs_activity is able to track the dupe_id,
  588. # this code should go below the duplicates-table-updating code below.
  589. foreach my $field (keys %$changes) {
  590. my $change = $changes->{$field};
  591. my $from = defined $change->[0] ? $change->[0] : '';
  592. my $to = defined $change->[1] ? $change->[1] : '';
  593. LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id,
  594. $delta_ts);
  595. }
  596. # Check if we have to update the duplicates table and the other bug.
  597. my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0);
  598. if ($old_dup != $cur_dup) {
  599. $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id);
  600. if ($cur_dup) {
  601. $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)',
  602. undef, $self->id, $cur_dup);
  603. if (my $update_dup = delete $self->{_dup_for_update}) {
  604. $update_dup->update();
  605. }
  606. }
  607. $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
  608. }
  609. Bugzilla::Hook::process('bug-end_of_update', { bug => $self,
  610. timestamp => $delta_ts,
  611. changes => $changes,
  612. });
  613. # If any change occurred, refresh the timestamp of the bug.
  614. if (scalar(keys %$changes) || $self->{added_comments}) {
  615. $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
  616. undef, ($delta_ts, $self->id));
  617. $self->{delta_ts} = $delta_ts;
  618. }
  619. # The only problem with this here is that update() is often called
  620. # in the middle of a transaction, and if that transaction is rolled
  621. # back, this change will *not* be rolled back. As we expect rollbacks
  622. # to be extremely rare, that is OK for us.
  623. $self->_sync_fulltext()
  624. if $self->{added_comments} || $changes->{short_desc};
  625. # Remove obsolete internal variables.
  626. delete $self->{'_old_assigned_to'};
  627. delete $self->{'_old_qa_contact'};
  628. return $changes;
  629. }
  630. # Used by create().
  631. # We need to handle multi-select fields differently than normal fields,
  632. # because they're arrays and don't go into the bugs table.
  633. sub _extract_multi_selects {
  634. my ($invocant, $params) = @_;
  635. my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT}
  636. Bugzilla->active_custom_fields;
  637. my %ms_values;
  638. foreach my $field (@multi_selects) {
  639. my $name = $field->name;
  640. if (exists $params->{$name}) {
  641. my $array = delete($params->{$name}) || [];
  642. $ms_values{$name} = $array;
  643. }
  644. }
  645. return \%ms_values;
  646. }
  647. # Should be called any time you update short_desc or change a comment.
  648. sub _sync_fulltext {
  649. my ($self, $new_bug) = @_;
  650. my $dbh = Bugzilla->dbh;
  651. if ($new_bug) {
  652. $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc)
  653. SELECT bug_id, short_desc FROM bugs WHERE bug_id = ?',
  654. undef, $self->id);
  655. }
  656. else {
  657. $dbh->do('UPDATE bugs_fulltext SET short_desc = ? WHERE bug_id = ?',
  658. undef, $self->short_desc, $self->id);
  659. }
  660. my $comments = $dbh->selectall_arrayref(
  661. 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
  662. undef, $self->id);
  663. my $all = join("\n", map { $_->[0] } @$comments);
  664. my @no_private = grep { !$_->[1] } @$comments;
  665. my $nopriv_string = join("\n", map { $_->[0] } @no_private);
  666. $dbh->do('UPDATE bugs_fulltext SET comments = ?, comments_noprivate = ?
  667. WHERE bug_id = ?', undef, $all, $nopriv_string, $self->id);
  668. }
  669. # This is the correct way to delete bugs from the DB.
  670. # No bug should be deleted from anywhere else except from here.
  671. #
  672. sub remove_from_db {
  673. my ($self) = @_;
  674. my $dbh = Bugzilla->dbh;
  675. if ($self->{'error'}) {
  676. ThrowCodeError("bug_error", { bug => $self });
  677. }
  678. my $bug_id = $self->{'bug_id'};
  679. # tables having 'bugs.bug_id' as a foreign key:
  680. # - attachments
  681. # - bug_group_map
  682. # - bugs
  683. # - bugs_activity
  684. # - bugs_fulltext
  685. # - cc
  686. # - dependencies
  687. # - duplicates
  688. # - flags
  689. # - keywords
  690. # - longdescs
  691. # - votes
  692. # Also included are custom multi-select fields.
  693. # Also, the attach_data table uses attachments.attach_id as a foreign
  694. # key, and so indirectly depends on a bug deletion too.
  695. $dbh->bz_start_transaction();
  696. $dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id);
  697. $dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id);
  698. $dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
  699. $dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
  700. undef, ($bug_id, $bug_id));
  701. $dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
  702. undef, ($bug_id, $bug_id));
  703. $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
  704. $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
  705. $dbh->do("DELETE FROM votes WHERE bug_id = ?", undef, $bug_id);
  706. # The attach_data table doesn't depend on bugs.bug_id directly.
  707. my $attach_ids =
  708. $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
  709. WHERE bug_id = ?", undef, $bug_id);
  710. if (scalar(@$attach_ids)) {
  711. $dbh->do("DELETE FROM attach_data WHERE "
  712. . $dbh->sql_in('id', $attach_ids));
  713. }
  714. # Several of the previous tables also depend on attach_id.
  715. $dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id);
  716. $dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
  717. $dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
  718. # Delete entries from custom multi-select fields.
  719. my @multi_selects = Bugzilla->get_fields({custom => 1, type => FIELD_TYPE_MULTI_SELECT});
  720. foreach my $field (@multi_selects) {
  721. $dbh->do("DELETE FROM bug_" . $field->name . " WHERE bug_id = ?", undef, $bug_id);
  722. }
  723. $dbh->bz_commit_transaction();
  724. # The bugs_fulltext table doesn't support transactions.
  725. $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id);
  726. # Now this bug no longer exists
  727. $self->DESTROY;
  728. return $self;
  729. }
  730. #####################################################################
  731. # Validators
  732. #####################################################################
  733. sub _check_alias {
  734. my ($invocant, $alias) = @_;
  735. $alias = trim($alias);
  736. return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
  737. # Make sure the alias isn't too long.
  738. if (length($alias) > 20) {
  739. ThrowUserError("alias_too_long");
  740. }
  741. # Make sure the alias isn't just a number.
  742. if ($alias =~ /^\d+$/) {
  743. ThrowUserError("alias_is_numeric", { alias => $alias });
  744. }
  745. # Make sure the alias has no commas or spaces.
  746. if ($alias =~ /[, ]/) {
  747. ThrowUserError("alias_has_comma_or_space", { alias => $alias });
  748. }
  749. # Make sure the alias is unique, or that it's already our alias.
  750. my $other_bug = new Bugzilla::Bug($alias);
  751. if (!$other_bug->{error}
  752. && (!ref $invocant || $other_bug->id != $invocant->id))
  753. {
  754. ThrowUserError("alias_in_use", { alias => $alias,
  755. bug_id => $other_bug->id });
  756. }
  757. return $alias;
  758. }
  759. sub _check_assigned_to {
  760. my ($invocant, $assignee, $component) = @_;
  761. my $user = Bugzilla->user;
  762. # Default assignee is the component owner.
  763. my $id;
  764. # If this is a new bug, you can only set the assignee if you have editbugs.
  765. # If you didn't specify the assignee, we use the default assignee.
  766. if (!ref $invocant
  767. && (!$user->in_group('editbugs', $component->product_id) || !$assignee))
  768. {
  769. $id = $component->default_assignee->id;
  770. } else {
  771. if (!ref $assignee) {
  772. $assignee = trim($assignee);
  773. # When updating a bug, assigned_to can't be empty.
  774. ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee;
  775. $assignee = Bugzilla::User->check($assignee);
  776. }
  777. $id = $assignee->id;
  778. # create() checks this another way, so we don't have to run this
  779. # check during create().
  780. $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant;
  781. }
  782. return $id;
  783. }
  784. sub _check_bug_file_loc {
  785. my ($invocant, $url) = @_;
  786. $url = '' if !defined($url);
  787. # On bug entry, if bug_file_loc is "http://", the default, use an
  788. # empty value instead. However, on bug editing people can set that
  789. # back if they *really* want to.
  790. if (!ref $invocant && $url eq 'http://') {
  791. $url = '';
  792. }
  793. return $url;
  794. }
  795. sub _check_bug_severity {
  796. my ($invocant, $severity) = @_;
  797. $severity = trim($severity);
  798. check_field('bug_severity', $severity);
  799. return $severity;
  800. }
  801. sub _check_bug_status {
  802. my ($invocant, $new_status, $product, $comment) = @_;
  803. my $user = Bugzilla->user;
  804. my @valid_statuses;
  805. my $old_status; # Note that this is undef for new bugs.
  806. if (ref $invocant) {
  807. @valid_statuses = @{$invocant->status->can_change_to};
  808. $product = $invocant->product_obj;
  809. $old_status = $invocant->status;
  810. my $comments = $invocant->{added_comments} || [];
  811. $comment = $comments->[-1];
  812. }
  813. else {
  814. @valid_statuses = @{Bugzilla::Status->can_change_to()};
  815. }
  816. if (!$product->votes_to_confirm) {
  817. # UNCONFIRMED becomes an invalid status if votes_to_confirm is 0,
  818. # even if you are in editbugs.
  819. @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses;
  820. }
  821. # Check permissions for users filing new bugs.
  822. if (!ref $invocant) {
  823. if ($user->in_group('editbugs', $product->id)
  824. || $user->in_group('canconfirm', $product->id)) {
  825. # If the user with privs hasn't selected another status,
  826. # select the first one of the list.
  827. unless ($new_status) {
  828. if (scalar(@valid_statuses) == 1) {
  829. $new_status = $valid_statuses[0];
  830. }
  831. else {
  832. $new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ?
  833. $valid_statuses[0] : $valid_statuses[1];
  834. }
  835. }
  836. }
  837. else {
  838. # A user with no privs cannot choose the initial status.
  839. # If UNCONFIRMED is valid for this product, use it; else
  840. # use the first bug status available.
  841. if (grep {$_->name eq 'UNCONFIRMED'} @valid_statuses) {
  842. $new_status = 'UNCONFIRMED';
  843. }
  844. else {
  845. $new_status = $valid_statuses[0];
  846. }
  847. }
  848. }
  849. # Time to validate the bug status.
  850. $new_status = Bugzilla::Status->check($new_status) unless ref($new_status);
  851. if (!grep {$_->name eq $new_status->name} @valid_statuses) {
  852. ThrowUserError('illegal_bug_status_transition',
  853. { old => $old_status, new => $new_status });
  854. }
  855. # Check if a comment is required for this change.
  856. if ($new_status->comment_required_on_change_from($old_status) && !$comment)
  857. {
  858. ThrowUserError('comment_required', { old => $old_status,
  859. new => $new_status });
  860. }
  861. if (ref $invocant && $new_status->name eq 'ASSIGNED'
  862. && Bugzilla->params->{"usetargetmilestone"}
  863. && Bugzilla->params->{"musthavemilestoneonaccept"}
  864. # musthavemilestoneonaccept applies only if at least two
  865. # target milestones are defined for the product.
  866. && scalar(@{ $product->milestones }) > 1
  867. && $invocant->target_milestone eq $product->default_milestone)
  868. {
  869. ThrowUserError("milestone_required", { bug => $invocant });
  870. }
  871. return $new_status->name if ref $invocant;
  872. return ($new_status->name, $new_status->name eq 'UNCONFIRMED' ? 0 : 1);
  873. }
  874. sub _check_cc {
  875. my ($invocant, $component, $ccs) = @_;
  876. return [map {$_->id} @{$component->initial_cc}] unless $ccs;
  877. my %cc_ids;
  878. foreach my $person (@$ccs) {
  879. next unless $person;
  880. my $id = login_to_id($person, THROW_ERROR);
  881. $cc_ids{$id} = 1;
  882. }
  883. # Enforce Default CC
  884. $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
  885. return [keys %cc_ids];
  886. }
  887. sub _check_comment {
  888. my ($invocant, $comment) = @_;
  889. $comment = '' unless defined $comment;
  890. # Remove any trailing whitespace. Leading whitespace could be
  891. # a valid part of the comment.
  892. $comment =~ s/\s*$//s;
  893. $comment =~ s/\r\n?/\n/g; # Get rid of \r.
  894. ThrowUserError('comment_too_long') if length($comment) > MAX_COMMENT_LENGTH;
  895. return $comment;
  896. }
  897. sub _check_commentprivacy {
  898. my ($invocant, $comment_privacy) = @_;
  899. my $insider_group = Bugzilla->params->{"insidergroup"};
  900. return ($insider_group && Bugzilla->user->in_group($insider_group)
  901. && $comment_privacy) ? 1 : 0;
  902. }
  903. sub _check_comment_type {
  904. my ($invocant, $type) = @_;
  905. detaint_natural($type)
  906. || ThrowCodeError('bad_arg', { argument => 'type',
  907. function => caller });
  908. return $type;
  909. }
  910. sub _check_component {
  911. my ($invocant, $name, $product) = @_;
  912. $name = trim($name);
  913. $name || ThrowUserError("require_component");
  914. ($product = $invocant->product_obj) if ref $invocant;
  915. my $obj = Bugzilla::Component->check({ product => $product, name => $name });
  916. return $obj;
  917. }
  918. sub _check_deadline {
  919. my ($invocant, $date) = @_;
  920. # Check time-tracking permissions.
  921. my $tt_group = Bugzilla->params->{"timetrackinggroup"};
  922. # deadline() returns '' instead of undef if no deadline is set.
  923. my $current = ref $invocant ? ($invocant->deadline || undef) : undef;
  924. return $current unless $tt_group && Bugzilla->user->in_group($tt_group);
  925. # Validate entered deadline
  926. $date = trim($date);
  927. return undef if !$date;
  928. validate_date($date)
  929. || ThrowUserError('illegal_date', { date => $date,
  930. format => 'YYYY-MM-DD' });
  931. return $date;
  932. }
  933. # Takes two comma/space-separated strings and returns arrayrefs
  934. # of valid bug IDs.
  935. sub _check_dependencies {
  936. my ($invocant, $depends_on, $blocks, $product) = @_;
  937. if (!ref $invocant) {
  938. # Only editbugs users can set dependencies on bug entry.
  939. return ([], []) unless Bugzilla->user->in_group('editbugs',
  940. $product->id);
  941. }
  942. my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
  943. foreach my $type qw(dependson blocked) {
  944. my @bug_ids = split(/[\s,]+/, $deps_in{$type});
  945. # Eliminate nulls.
  946. @bug_ids = grep {$_} @bug_ids;
  947. # We do Validate up here to make sure all aliases are converted to IDs.
  948. ValidateBugID($_, $type) foreach @bug_ids;
  949. my @check_access = @bug_ids;
  950. # When we're updating a bug, only added or removed bug_ids are
  951. # checked for whether or not we can see/edit those bugs.
  952. if (ref $invocant) {
  953. my $old = $invocant->$type;
  954. my ($removed, $added) = diff_arrays($old, \@bug_ids);
  955. @check_access = (@$added, @$removed);
  956. # Check field permissions if we've changed anything.
  957. if (@check_access) {
  958. my $privs;
  959. if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) {
  960. ThrowUserError('illegal_change', { field => $type,
  961. privs => $privs });
  962. }
  963. }
  964. }
  965. my $user = Bugzilla->user;
  966. foreach my $modified_id (@check_access) {
  967. ValidateBugID($modified_id);
  968. # Under strict isolation, you can't modify a bug if you can't
  969. # edit it, even if you can see it.
  970. if (Bugzilla->params->{"strict_isolation"}) {
  971. my $delta_bug = new Bugzilla::Bug($modified_id);
  972. if (!$user->can_edit_product($delta_bug->{'product_id'})) {
  973. ThrowUserError("illegal_change_deps", {field => $type});
  974. }
  975. }
  976. }
  977. $deps_in{$type} = \@bug_ids;
  978. }
  979. # And finally, check for dependency loops.
  980. my $bug_id = ref($invocant) ? $invocant->id : 0;
  981. my %deps = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id);
  982. return ($deps{'dependson'}, $deps{'blocked'});
  983. }
  984. sub _check_dup_id {
  985. my ($self, $dupe_of) = @_;
  986. my $dbh = Bugzilla->dbh;
  987. $dupe_of = trim($dupe_of);
  988. $dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' });
  989. # Validate the bug ID. The second argument will force ValidateBugID() to
  990. # only make sure that the bug exists, and convert the alias to the bug ID
  991. # if a string is passed. Group restrictions are checked below.
  992. ValidateBugID($dupe_of, 'dup_id');
  993. # If the dupe is unchanged, we have nothing more to check.
  994. return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of);
  995. # If we come here, then the duplicate is new. We have to make sure
  996. # that we can view/change it (issue A on bug 96085).
  997. check_is_visible($dupe_of);
  998. # Make sure a loop isn't created when marking this bug
  999. # as duplicate.
  1000. my %dupes;
  1001. my $this_dup = $dupe_of;
  1002. my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?');
  1003. while ($this_dup) {
  1004. if ($this_dup == $self->id) {
  1005. ThrowUserError('dupe_loop_detected', { bug_id => $self->id,
  1006. dupe_of => $dupe_of });
  1007. }
  1008. # If $dupes{$this_dup} is already set to 1, then a loop
  1009. # already exists which does not involve this bug.
  1010. # As the user is not responsible for this loop, do not
  1011. # prevent him from marking this bug as a duplicate.
  1012. last if exists $dupes{$this_dup};
  1013. $dupes{$this_dup} = 1;
  1014. $this_dup = $dbh->selectrow_array($sth, undef, $this_dup);
  1015. }
  1016. my $cur_dup = $self->dup_id || 0;
  1017. if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'}
  1018. && !$self->{added_comments})
  1019. {
  1020. ThrowUserError('comment_required');
  1021. }
  1022. # Should we add the reporter to the CC list of the new bug?
  1023. # If he can see the bug...
  1024. if ($self->reporter->can_see_bug($dupe_of)) {
  1025. my $dupe_of_bug = new Bugzilla::Bug($dupe_of);
  1026. # We only add him if he's not the reporter of the other bug.
  1027. $self->{_add_dup_cc} = 1
  1028. if $dupe_of_bug->reporter->id != $self->reporter->id;
  1029. }
  1030. # What if the reporter currently can't see the new bug? In the browser
  1031. # interface, we prompt the user. In other interfaces, we default to
  1032. # not adding the user, as the safest option.
  1033. elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
  1034. # If we've already confirmed whether the user should be added...
  1035. my $cgi = Bugzilla->cgi;
  1036. my $add_confirmed = $cgi->param('confirm_add_duplicate');
  1037. if (defined $add_confirmed) {
  1038. $self->{_add_dup_cc} = $add_confirmed;
  1039. }
  1040. else {
  1041. # Note that here we don't check if he user is already the reporter
  1042. # of the dupe_of bug, since we already checked if he can *see*
  1043. # the bug, above. People might have reporter_accessible turned
  1044. # off, but cclist_accessible turned on, so they might want to
  1045. # add the reporter even though he's already the reporter of the
  1046. # dup_of bug.
  1047. my $vars = {};
  1048. my $template = Bugzilla->template;
  1049. # Ask the user what they want to do about the reporter.
  1050. $vars->{'cclist_accessible'} = $dbh->selectrow_array(
  1051. q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
  1052. undef, $dupe_of);
  1053. $vars->{'original_bug_id'} = $dupe_of;
  1054. $vars->{'duplicate_bug_id'} = $self->id;
  1055. print $cgi->header();
  1056. $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
  1057. || ThrowTemplateError($template->error());
  1058. exit;
  1059. }
  1060. }
  1061. return $dupe_of;
  1062. }
  1063. sub _check_estimated_time {
  1064. return $_[0]->_check_time($_[1], 'estimated_time');
  1065. }
  1066. sub _check_groups {
  1067. my ($invocant, $product, $group_ids) = @_;
  1068. my $user = Bugzilla->user;
  1069. my %add_groups;
  1070. my $controls = $product->group_controls;
  1071. foreach my $id (@$group_ids) {
  1072. my $group = new Bugzilla::Group($id)
  1073. || ThrowUserError("invalid_group_ID");
  1074. # This can only happen if somebody hacked the enter_bug form.
  1075. ThrowCodeError("inactive_group", { name => $group->name })
  1076. unless $group->is_active;
  1077. my $membercontrol = $controls->{$id}
  1078. && $controls->{$id}->{membercontrol};
  1079. my $othercontrol = $controls->{$id}
  1080. && $controls->{$id}->{othercontrol};
  1081. my $permit = ($membercontrol && $user->in_group($group->name))
  1082. || $othercontrol;
  1083. $add_groups{$id} = 1 if $permit;
  1084. }
  1085. foreach my $id (keys %$controls) {
  1086. next unless $controls->{$id}->{'group'}->is_active;
  1087. my $membercontrol = $controls->{$id}->{membercontrol} || 0;
  1088. my $othercontrol = $controls->{$id}->{othercontrol} || 0;
  1089. # Add groups required
  1090. if ($membercontrol == CONTROLMAPMANDATORY
  1091. || ($othercontrol == CONTROLMAPMANDATORY
  1092. && !$user->in_group_id($id)))
  1093. {
  1094. # User had no option, bug needs to be in this group.
  1095. $add_groups{$id} = 1;
  1096. }
  1097. }
  1098. my @add_groups = keys %add_groups;
  1099. return \@add_groups;
  1100. }
  1101. sub _check_keywords {
  1102. my ($invocant, $keyword_string, $product) = @_;
  1103. $keyword_string = trim($keyword_string);
  1104. return [] if !$keyword_string;
  1105. # On creation, only editbugs users can set keywords.
  1106. if (!ref $invocant) {
  1107. return [] if !Bugzilla->user->in_group('editbugs', $product->id);
  1108. }
  1109. my %keywords;
  1110. foreach my $keyword (split(/[\s,]+/, $keyword_string)) {
  1111. next unless $keyword;
  1112. my $obj = new Bugzilla::Keyword({ name => $keyword });
  1113. ThrowUserError("unknown_keyword", { keyword => $keyword }) if !$obj;
  1114. $keywords{$obj->id} = $obj;
  1115. }
  1116. return [values %keywords];
  1117. }
  1118. sub _check_product {
  1119. my ($invocant, $name) = @_;
  1120. $name = trim($name);
  1121. # If we're updating the bug and they haven't changed the product,
  1122. # always allow it.
  1123. if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) {
  1124. return $invocant->product_obj;
  1125. }
  1126. # Check that the product exists and that the user
  1127. # is allowed to enter bugs into this product.
  1128. Bugzilla->user->can_enter_product($name, THROW_ERROR);
  1129. # can_enter_product already does everything that check_product
  1130. # would do for us, so we don't need to use it.
  1131. return new Bugzilla::Product({ name => $name });
  1132. }
  1133. sub _check_op_sys {
  1134. my ($invocant, $op_sys) = @_;
  1135. $op_sys = trim($op_sys);
  1136. check_field('op_sys', $op_sys);
  1137. return $op_sys;
  1138. }
  1139. sub _check_priority {
  1140. my ($invocant, $priority) = @_;
  1141. if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) {
  1142. $priority = Bugzilla->params->{'defaultpriority'};
  1143. }
  1144. $priority = trim($priority);
  1145. check_field('priority', $priority);
  1146. return $priority;
  1147. }
  1148. sub _check_qa_contact {
  1149. my ($invocant, $qa_contact, $component) = @_;
  1150. $qa_contact = trim($qa_contact) if !ref $qa_contact;
  1151. my $id;
  1152. if (!ref $invocant) {
  1153. # Bugs get no QA Contact on creation if useqacontact is off.
  1154. return undef if !Bugzilla->params->{useqacontact};
  1155. # Set the default QA Contact if one isn't specified or if the
  1156. # user doesn't have editbugs.
  1157. if (!Bugzilla->user->in_group('editbugs', $component->product_id)
  1158. || !$qa_contact)
  1159. {
  1160. $id = $component->default_qa_contact->id;
  1161. }
  1162. }
  1163. # If a QA Contact was specified or if we're updating, check
  1164. # the QA Contact for validity.
  1165. if (!defined $id && $qa_contact) {
  1166. $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact;
  1167. $id = $qa_contact->id;
  1168. # create() checks this another way, so we don't have to run this
  1169. # check during create().
  1170. # If there is no QA contact, this check is not required.
  1171. $invocant->_check_strict_isolation_for_user($qa_contact)
  1172. if (ref $invocant && $id);
  1173. }
  1174. # "0" always means "undef", for QA Contact.
  1175. return $id || undef;
  1176. }
  1177. sub _check_remaining_time {
  1178. return $_[0]->_check_time($_[1], 'remaining_time');
  1179. }
  1180. sub _check_rep_platform {
  1181. my ($invocant, $platform) = @_;
  1182. $platform = trim($platform);
  1183. check_field('rep_platform', $platform);
  1184. return $platform;
  1185. }
  1186. sub _check_reporter {
  1187. my $invocant = shift;
  1188. my $reporter;
  1189. if (ref $invocant) {
  1190. # You cannot change the reporter of a bug.
  1191. $reporter = $invocant->reporter->id;
  1192. }
  1193. else {
  1194. # On bug creation, the reporter is the logged in user
  1195. # (meaning that he must be logged in first!).
  1196. $reporter = Bugzilla->user->id;
  1197. $reporter || ThrowCodeError('invalid_user');
  1198. }
  1199. return $reporter;
  1200. }
  1201. sub _check_resolution {
  1202. my ($self, $resolution) = @_;
  1203. $resolution = trim($resolution);
  1204. # Throw a special error for resolving bugs without a resolution
  1205. # (or trying to change the resolution to '' on a closed bug without
  1206. # using clear_resolution).
  1207. ThrowUserError('missing_resolution', { status => $self->status->name })
  1208. if !$resolution && !$self->status->is_open;
  1209. # Make sure this is a valid resolution.
  1210. check_field('resolution', $resolution);
  1211. # Don't allow open bugs to have resolutions.
  1212. ThrowUserError('resolution_not_allowed') if $self->status->is_open;
  1213. # Check noresolveonopenblockers.
  1214. if (Bugzilla->params->{"noresolveonopenblockers"} && $resolution eq 'FIXED')
  1215. {
  1216. my @dependencies = CountOpenDependencies($self->id);
  1217. if (@dependencies) {
  1218. ThrowUserError("still_unresolved_bugs",
  1219. { dependencies => \@dependencies,
  1220. dependency_count => scalar @dependencies });
  1221. }
  1222. }
  1223. # Check if they're changing the resolution and need to comment.
  1224. if (Bugzilla->params->{'commentonchange_resolution'}
  1225. && $self->resolution && $resolution ne $self->resolution
  1226. && !$self->{added_comments})
  1227. {
  1228. ThrowUserError('comment_required');
  1229. }
  1230. return $resolution;
  1231. }
  1232. sub _check_short_desc {
  1233. my ($invocant, $short_desc) = @_;
  1234. # Set the parameter to itself, but cleaned up
  1235. $short_desc = clean_text($short_desc) if $short_desc;
  1236. if (!defined $short_desc || $short_desc eq '') {
  1237. ThrowUserError("require_summary");
  1238. }
  1239. return $short_desc;
  1240. }
  1241. sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; }
  1242. # Unlike other checkers, this one doesn't return anything.
  1243. sub _check_strict_isolation {
  1244. my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_;
  1245. return unless Bugzilla->params->{'strict_isolation'};
  1246. if (ref $invocant) {
  1247. my $original = $invocant->new($invocant->id);
  1248. # We only check people if they've been added. This way, if
  1249. # strict_isolation is turned on when there are invalid users
  1250. # on bugs, people can still add comments and so on.
  1251. my @old_cc = map { $_->id } @{$original->cc_users};
  1252. my @new_cc = map { $_->id } @{$invocant->cc_users};
  1253. my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc);
  1254. $ccs = Bugzilla::User->new_from_list($added);
  1255. $assignee = $invocant->assigned_to
  1256. if $invocant->assigned_to->id != $original->assigned_to->id;
  1257. if ($invocant->qa_contact
  1258. && (!$original->qa_contact
  1259. || $invocant->qa_contact->id != $original->qa_contact->id))
  1260. {
  1261. $qa_contact = $invocant->qa_contact;
  1262. }
  1263. $product = $invocant->product_obj;
  1264. }
  1265. my @related_users = @$ccs;
  1266. push(@related_users, $assignee) if $assignee;
  1267. if (Bugzilla->params->{'useqacontact'} && $qa_contact) {
  1268. push(@related_users, $qa_contact);
  1269. }
  1270. @related_users = @{Bugzilla::User->new_from_list(\@related_users)}
  1271. if !ref $invocant;
  1272. # For each unique user in @related_users...(assignee and qa_contact
  1273. # could be duplicates of users in the CC list)
  1274. my %unique_users = map {$_->id => $_} @related_users;
  1275. my @blocked_users;
  1276. foreach my $id (keys %unique_users) {
  1277. my $related_user = $unique_users{$id};
  1278. if (!$related_user->can_edit_product($product->id) ||
  1279. !$related_user->can_see_product($product->name)) {
  1280. push (@blocked_users, $related_user->login);
  1281. }
  1282. }
  1283. if (scalar(@blocked_users)) {
  1284. my %vars = ( users => \@blocked_users,
  1285. product => $product->name );
  1286. if (ref $invocant) {
  1287. $vars{'bug_id'} = $invocant->id;
  1288. }
  1289. else {
  1290. $vars{'new'} = 1;
  1291. }
  1292. ThrowUserError("invalid_user_group", \%vars);
  1293. }
  1294. }
  1295. # This is used by various set_ checkers, to make their code simpler.
  1296. sub _check_strict_isolation_for_user {
  1297. my ($self, $user) = @_;
  1298. return unless Bugzilla->params->{"strict_isolation"};
  1299. if (!$user->can_edit_product($self->{product_id})) {
  1300. ThrowUserError('invalid_user_group',
  1301. { users => $user->login,
  1302. product => $self->product,
  1303. bug_id => $self->id });
  1304. }
  1305. }
  1306. sub _check_target_milestone {
  1307. my ($invocant, $target, $product) = @_;
  1308. $product = $invocant->product_obj if ref $invocant;
  1309. $target = trim($target);
  1310. $target = $product->default_milestone if !defined $target;
  1311. check_field('target_milestone', $target,
  1312. [map($_->name, @{$product->milestones})]);
  1313. return $target;
  1314. }
  1315. sub _check_time {
  1316. my ($invocant, $time, $field) = @_;
  1317. my $current = 0;
  1318. if (ref $invocant && $field ne 'work_time') {
  1319. $current = $invocant->$field;
  1320. }
  1321. my $tt_group = Bugzilla->params->{"timetrackinggroup"};
  1322. return $current unless $tt_group && Bugzilla->user->in_group($tt_group);
  1323. $time = trim($time) || 0;
  1324. ValidateTime($time, $field);
  1325. return $time;
  1326. }
  1327. sub _check_version {
  1328. my ($invocant, $version, $product) = @_;
  1329. $version = trim($version);
  1330. ($product = $invocant->product_obj) if ref $invocant;
  1331. check_field('version', $version, [map($_->name, @{$product->versions})]);
  1332. return $version;
  1333. }
  1334. sub _check_work_time {
  1335. return $_[0]->_check_time($_[1], 'work_time');
  1336. }
  1337. # Custom Field Validators
  1338. sub _check_datetime_field {
  1339. my ($invocant, $date_time) = @_;
  1340. # Empty datetimes are empty strings or strings only containing
  1341. # 0's, whitespace, and punctuation.
  1342. if ($date_time =~ /^[\s0[:punct:]]*$/) {
  1343. return undef;
  1344. }
  1345. $date_time = trim($date_time);
  1346. my ($date, $time) = split(' ', $date_time);
  1347. if ($date && !validate_date($date)) {
  1348. ThrowUserError('illegal_date', { date => $date,
  1349. format => 'YYYY-MM-DD' });
  1350. }
  1351. if ($time && !validate_time($time)) {
  1352. ThrowUserError('illegal_time', { 'time' => $time,
  1353. format => 'HH:MM:SS' });
  1354. }
  1355. return $date_time
  1356. }
  1357. sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; }
  1358. sub _check_freetext_field {
  1359. my ($invocant, $text) = @_;
  1360. $text = (defined $text) ? trim($text) : '';
  1361. if (length($text) > MAX_FREETEXT_LENGTH) {
  1362. ThrowUserError('freetext_too_long', { text => $text });
  1363. }
  1364. return $text;
  1365. }
  1366. sub _check_multi_select_field {
  1367. my ($invocant, $values, $field) = @_;
  1368. return [] if !$values;
  1369. foreach my $value (@$values) {
  1370. $value = trim($value);
  1371. check_field($field, $value);
  1372. trick_taint($value);
  1373. }
  1374. return $values;
  1375. }
  1376. sub _check_select_field {
  1377. my ($invocant, $value, $field) = @_;
  1378. $value = trim($value);
  1379. check_field($field, $value);
  1380. return $value;
  1381. }
  1382. #####################################################################
  1383. # Class Accessors
  1384. #####################################################################
  1385. sub fields {
  1386. my $class = shift;
  1387. return (
  1388. # Standard Fields
  1389. # Keep this ordering in sync with bugzilla.dtd.
  1390. qw(bug_id alias creation_ts short_desc delta_ts
  1391. reporter_accessible cclist_accessible
  1392. classification_id classification
  1393. product component version rep_platform op_sys
  1394. bug_status resolution dup_id
  1395. bug_file_loc status_whiteboard keywords
  1396. priority bug_severity target_milestone
  1397. dependson blocked votes everconfirmed
  1398. reporter assigned_to cc estimated_time
  1399. remaining_time actual_time deadline),
  1400. # Conditional Fields
  1401. Bugzilla->params->{'useqacontact'} ? "qa_contact" : (),
  1402. # Custom Fields
  1403. map { $_->name } Bugzilla->active_custom_fields
  1404. );
  1405. }
  1406. #####################################################################
  1407. # Mutators
  1408. #####################################################################
  1409. # To run check_can_change_field.
  1410. sub _set_global_validator {
  1411. my ($self, $value, $field) = @_;
  1412. my $current = $self->$field;
  1413. my $privs;
  1414. if (ref $current && ref($current) ne 'ARRAY'
  1415. && $current->isa('Bugzilla::Object')) {
  1416. $current = $current->id ;
  1417. }
  1418. if (ref $value && ref($value) ne 'ARRAY'
  1419. && $value->isa('Bugzilla::Object')) {
  1420. $value = $value->id ;
  1421. }
  1422. my $can = $self->check_can_change_field($field, $current, $value, \$privs);
  1423. if (!$can) {
  1424. if ($field eq 'assigned_to' || $field eq 'qa_contact') {
  1425. $value = user_id_to_login($value);
  1426. $current = user_id_to_login($current);
  1427. }
  1428. ThrowUserError('illegal_change', { field => $field,
  1429. oldvalue => $current,
  1430. newvalue => $value,
  1431. privs => $privs });
  1432. }
  1433. }
  1434. #################
  1435. # "Set" Methods #
  1436. #################
  1437. sub set_alias { $_[0]->set('alias', $_[1]); }
  1438. sub set_assigned_to {
  1439. my ($self, $value) = @_;
  1440. $self->set('assigned_to', $value);
  1441. # Store the old assignee. check_can_change_field() needs it.
  1442. $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id;
  1443. delete $self->{'assigned_to_obj'};
  1444. }
  1445. sub reset_assigned_to {
  1446. my $self = shift;
  1447. if (Bugzilla->params->{'commentonreassignbycomponent'}
  1448. && !$self->{added_comments})
  1449. {
  1450. ThrowUserError('comment_required');
  1451. }
  1452. my $comp = $self->component_obj;
  1453. $self->set_assigned_to($comp->default_assignee);
  1454. }
  1455. sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
  1456. sub set_comment_is_private {
  1457. my ($self, $comment_id, $isprivate) = @_;
  1458. return unless Bugzilla->user->is_insider;
  1459. my ($comment) = grep($comment_id eq $_->{id}, @{$self->longdescs});
  1460. ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
  1461. if !$comment;
  1462. $isprivate = $isprivate ? 1 : 0;
  1463. if ($isprivate != $comment->{isprivate}) {
  1464. $self->{comment_isprivate} ||= {};
  1465. $self->{comment_isprivate}->{$comment_id} = $isprivate;
  1466. }
  1467. }
  1468. sub set_component {
  1469. my ($self, $name) = @_;
  1470. my $old_comp = $self->component_obj;
  1471. my $component = $self->_check_component($name);
  1472. if ($old_comp->id != $component->id) {
  1473. $self->{component_id} = $component->id;
  1474. $self->{component} = $component->name;
  1475. $self->{component_obj} = $component;
  1476. # For update()
  1477. $self->{_old_component_name} = $old_comp->name;
  1478. # Add in the Default CC of the new Component;
  1479. foreach my $cc (@{$component->initial_cc}) {
  1480. $self->add_cc($cc);
  1481. }
  1482. }
  1483. }
  1484. sub set_custom_field {
  1485. my ($self, $field, $value) = @_;
  1486. if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) {
  1487. $value = $value->[0];
  1488. }
  1489. ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom;
  1490. $self->set($field->name, $value);
  1491. }
  1492. sub set_deadline { $_[0]->set('deadline', $_[1]); }
  1493. sub set_dependencies {
  1494. my ($self, $dependson, $blocked) = @_;
  1495. ($dependson, $blocked) = $self->_check_dependencies($dependson, $blocked);
  1496. # These may already be detainted, but all setters are supposed to
  1497. # detaint their input if they've run a validator (just as though
  1498. # we had used Bugzilla::Object::set), so we do that here.
  1499. detaint_natural($_) foreach (@$dependson, @$blocked);
  1500. $self->{'dependson'} = $dependson;
  1501. $self->{'blocked'} = $blocked;
  1502. }
  1503. sub _clear_dup_id { $_[0]->{dup_id} = undef; }
  1504. sub set_dup_id {
  1505. my ($self, $dup_id) = @_;
  1506. my $old = $self->dup_id || 0;
  1507. $self->set('dup_id', $dup_id);
  1508. my $new = $self->dup_id;
  1509. return if $old == $new;
  1510. # Update the other bug.
  1511. my $dupe_of = new Bugzilla::Bug($self->dup_id);
  1512. if (delete $self->{_add_dup_cc}) {
  1513. $dupe_of->add_cc($self->reporter);
  1514. }
  1515. $dupe_of->add_comment("", { type => CMT_HAS_DUPE,
  1516. extra_data => $self->id });
  1517. $self->{_dup_for_update} = $dupe_of;
  1518. # Now make sure that we add a duplicate comment on *this* bug.
  1519. # (Change an existing comment into a dup comment, if there is one,
  1520. # or add an empty dup comment.)
  1521. if ($self->{added_comments}) {
  1522. my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL }
  1523. @{ $self->{added_comments} };
  1524. # Turn the last one into a dup comment.
  1525. $normal[-1]->{type} = CMT_DUPE_OF;
  1526. $normal[-1]->{extra_data} = $self->dup_id;
  1527. }
  1528. else {
  1529. $self->add_comment('', { type => CMT_DUPE_OF,
  1530. extra_data => $self->dup_id });
  1531. }
  1532. }
  1533. sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); }
  1534. sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
  1535. sub set_op_sys { $_[0]->set('op_sys', $_[1]); }
  1536. sub set_platform { $_[0]->set('rep_platform', $_[1]); }
  1537. sub set_priority { $_[0]->set('priority', $_[1]); }
  1538. sub set_product {
  1539. my ($self, $name, $params) = @_;
  1540. my $old_product = $self->product_obj;
  1541. my $product = $self->_check_product($name);
  1542. my $product_changed = 0;
  1543. if ($old_product->id != $product->id) {
  1544. $self->{product_id} = $product->id;
  1545. $self->{product} = $product->name;
  1546. $self->{product_obj} = $product;
  1547. # For update()
  1548. $self->{_old_product_name} = $old_product->name;
  1549. # Delete fields that depend upon the old Product value.
  1550. delete $self->{choices};
  1551. delete $self->{milestoneurl};
  1552. $product_changed = 1;
  1553. }
  1554. $params ||= {};
  1555. my $comp_name = $params->{component} || $self->component;
  1556. my $vers_name = $params->{version} || $self->version;
  1557. my $tm_name = $params->{target_milestone};
  1558. # This way, if usetargetmilestone is off and we've changed products,
  1559. # set_target_milestone will reset our target_milestone to
  1560. # $product->default_milestone. But if we haven't changed products,
  1561. # we don't reset anything.
  1562. if (!defined $tm_name
  1563. && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed))
  1564. {
  1565. $tm_name = $self->target_milestone;
  1566. }
  1567. if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
  1568. # Try to set each value with the new product.
  1569. # Have to set error_mode because Throw*Error calls exit() otherwise.
  1570. my $old_error_mode = Bugzilla->error_mode;
  1571. Bugzilla->error_mode(ERROR_MODE_DIE);
  1572. my $component_ok = eval { $self->set_component($comp_name); 1; };
  1573. my $version_ok = eval { $self->set_version($vers_name); 1; };
  1574. my $milestone_ok = 1;
  1575. # Reporters can move bugs between products but not set the TM.
  1576. if ($self->check_can_change_field('target_milestone', 0, 1)) {
  1577. $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; };
  1578. }
  1579. else {
  1580. # Have to set this directly to bypass the validators.
  1581. $self->{target_milestone} = $product->default_milestone;
  1582. }
  1583. # If there were any errors thrown, make sure we don't mess up any
  1584. # other part of Bugzilla that checks $@.
  1585. undef $@;
  1586. Bugzilla->error_mode($old_error_mode);
  1587. my $verified = $params->{change_confirmed};
  1588. my %vars;
  1589. if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) {
  1590. $vars{defaults} = {
  1591. # Note that because of the eval { set } above, these are
  1592. # already set correctly if they're valid, otherwise they're
  1593. # set to some invalid value which the template will ignore.
  1594. component => $self->component,
  1595. version => $self->version,
  1596. milestone => $milestone_ok ? $self->target_milestone
  1597. : $product->default_milestone
  1598. };
  1599. $vars{components} = [map { $_->name } @{$product->components}];
  1600. $vars{milestones} = [map { $_->name } @{$product->milestones}];
  1601. $vars{versions} = [map { $_->name } @{$product->versions}];
  1602. }
  1603. if (!$verified) {
  1604. $vars{verify_bug_groups} = 1;
  1605. my $dbh = Bugzilla->dbh;
  1606. my @idlist = ($self->id);
  1607. push(@idlist, map {$_->id} @{ $params->{other_bugs} })
  1608. if $params->{other_bugs};
  1609. # Get the ID of groups which are no longer valid in the new product.
  1610. my $gids = $dbh->selectcol_arrayref(
  1611. 'SELECT bgm.group_id
  1612. FROM bug_group_map AS bgm
  1613. WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ')
  1614. AND bgm.group_id NOT IN
  1615. (SELECT gcm.group_id
  1616. FROM group_control_map AS gcm
  1617. WHERE gcm.product_id = ?
  1618. AND ( (gcm.membercontrol != ?
  1619. AND gcm.group_id IN ('
  1620. . Bugzilla->user->groups_as_string . '))
  1621. OR gcm.othercontrol != ?) )',
  1622. undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
  1623. $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
  1624. }
  1625. if (%vars) {
  1626. $vars{product} = $product;
  1627. $vars{bug} = $self;
  1628. my $template = Bugzilla->template;
  1629. $template->process("bug/process/verify-new-product.html.tmpl",
  1630. \%vars) || ThrowTemplateError($template->error());
  1631. exit;
  1632. }
  1633. }
  1634. else {
  1635. # When we're not in the browser (or we didn't change the product), we
  1636. # just die if any of these are invalid.
  1637. $self->set_component($comp_name);
  1638. $self->set_version($vers_name);
  1639. if ($product_changed && !$self->check_can_change_field('target_milestone', 0, 1)) {
  1640. # Have to set this directly to bypass the validators.
  1641. $self->{target_milestone} = $product->default_milestone;
  1642. }
  1643. else {
  1644. $self->set_target_milestone($tm_name);
  1645. }
  1646. }
  1647. if ($product_changed) {
  1648. # Remove groups that aren't valid in the new product. This will also
  1649. # have the side effect of removing the bug from groups that aren't
  1650. # active anymore.
  1651. #
  1652. # We copy this array because the original array is modified while we're
  1653. # working, and that confuses "foreach".
  1654. my @current_groups = @{$self->groups_in};
  1655. foreach my $group (@current_groups) {
  1656. if (!grep($group->id == $_->id, @{$product->groups_valid})) {
  1657. $self->remove_group($group);
  1658. }
  1659. }
  1660. # Make sure the bug is in all the mandatory groups for the new product.
  1661. foreach my $group (@{$product->groups_mandatory_for(Bugzilla->user)}) {
  1662. $self->add_group($group);
  1663. }
  1664. }
  1665. # XXX This is temporary until all of process_bug uses update();
  1666. return $product_changed;
  1667. }
  1668. sub set_qa_contact {
  1669. my ($self, $value) = @_;
  1670. $self->set('qa_contact', $value);
  1671. # Store the old QA contact. check_can_change_field() needs it.
  1672. if ($self->{'qa_contact_obj'}) {
  1673. $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id;
  1674. }
  1675. delete $self->{'qa_contact_obj'};
  1676. }
  1677. sub reset_qa_contact {
  1678. my $self = shift;
  1679. if (Bugzilla->params->{'commentonreassignbycomponent'}
  1680. && !$self->{added_comments})
  1681. {
  1682. ThrowUserError('comment_required');
  1683. }
  1684. my $comp = $self->component_obj;
  1685. $self->set_qa_contact($comp->default_qa_contact);
  1686. }
  1687. sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); }
  1688. # Used only when closing a bug or moving between closed states.
  1689. sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; }
  1690. sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); }
  1691. sub set_resolution {
  1692. my ($self, $value, $params) = @_;
  1693. my $old_res = $self->resolution;
  1694. $self->set('resolution', $value);
  1695. my $new_res = $self->resolution;
  1696. if ($new_res ne $old_res) {
  1697. # MOVED has a special meaning and can only be used when
  1698. # really moving bugs to another installation.
  1699. ThrowCodeError('no_manual_moved') if ($new_res eq 'MOVED' && !$params->{moving});
  1700. # Clear the dup_id if we're leaving the dup resolution.
  1701. if ($old_res eq 'DUPLICATE') {
  1702. $self->_clear_dup_id();
  1703. }
  1704. # Duplicates should have no remaining time left.
  1705. elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) {
  1706. $self->_zero_remaining_time();
  1707. }
  1708. }
  1709. # We don't check if we're entering or leaving the dup resolution here,
  1710. # because we could be moving from being a dup of one bug to being a dup
  1711. # of another, theoretically. Note that this code block will also run
  1712. # when going between different closed states.
  1713. if ($self->resolution eq 'DUPLICATE') {
  1714. if ($params->{dupe_of}) {
  1715. $self->set_dup_id($params->{dupe_of});
  1716. }
  1717. elsif (!$self->dup_id) {
  1718. ThrowUserError('dupe_id_required');
  1719. }
  1720. }
  1721. }
  1722. sub clear_resolution {
  1723. my $self = shift;
  1724. if (!$self->status->is_open) {
  1725. ThrowUserError('resolution_cant_clear', { bug_id => $self->id });
  1726. }
  1727. if (Bugzilla->params->{'commentonclearresolution'}
  1728. && $self->resolution && !$self->{added_comments})
  1729. {
  1730. ThrowUserError('comment_required');
  1731. }
  1732. $self->{'resolution'} = '';
  1733. $self->_clear_dup_id;
  1734. }
  1735. sub set_severity { $_[0]->set('bug_severity', $_[1]); }
  1736. sub set_status {
  1737. my ($self, $status, $params) = @_;
  1738. my $old_status = $self->status;
  1739. $self->set('bug_status', $status);
  1740. delete $self->{'status'};
  1741. my $new_status = $self->status;
  1742. if ($new_status->is_open) {
  1743. # Check for the everconfirmed transition
  1744. $self->_set_everconfirmed(1) if $new_status->name ne 'UNCONFIRMED';
  1745. $self->clear_resolution();
  1746. }
  1747. else {
  1748. # We do this here so that we can make sure closed statuses have
  1749. # resolutions.
  1750. my $resolution = delete $params->{resolution} || $self->resolution;
  1751. $self->set_resolution($resolution, $params);
  1752. # Changing between closed statuses zeros the remaining time.
  1753. if ($new_status->id != $old_status->id && $self->remaining_time != 0) {
  1754. $self->_zero_remaining_time();
  1755. }
  1756. }
  1757. }
  1758. sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); }
  1759. sub set_summary { $_[0]->set('short_desc', $_[1]); }
  1760. sub set_target_milestone { $_[0]->set('target_milestone', $_[1]); }
  1761. sub set_url { $_[0]->set('bug_file_loc', $_[1]); }
  1762. sub set_version { $_[0]->set('version', $_[1]); }
  1763. ########################
  1764. # "Add/Remove" Methods #
  1765. ########################
  1766. # These are in alphabetical order by field name.
  1767. # Accepts a User object or a username. Adds the user only if they
  1768. # don't already exist as a CC on the bug.
  1769. sub add_cc {
  1770. my ($self, $user_or_name) = @_;
  1771. return if !$user_or_name;
  1772. my $user = ref $user_or_name ? $user_or_name
  1773. : Bugzilla::User->check($user_or_name);
  1774. $self->_check_strict_isolation_for_user($user);
  1775. my $cc_users = $self->cc_users;
  1776. push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users);
  1777. }
  1778. # Accepts a User object or a username. Removes the User if they exist
  1779. # in the list, but doesn't throw an error if they don't exist.
  1780. sub remove_cc {
  1781. my ($self, $user_or_name) = @_;
  1782. my $user = ref $user_or_name ? $user_or_name
  1783. : Bugzilla::User->check($user_or_name);
  1784. my $cc_users = $self->cc_users;
  1785. @$cc_users = grep { $_->id != $user->id } @$cc_users;
  1786. }
  1787. # $bug->add_comment("comment", {isprivate => 1, work_time => 10.5,
  1788. # type => CMT_NORMAL, extra_data => $data});
  1789. sub add_comment {
  1790. my ($self, $comment, $params) = @_;
  1791. $comment = $self->_check_comment($comment);
  1792. $params ||= {};
  1793. if (exists $params->{work_time}) {
  1794. $params->{work_time} = $self->_check_work_time($params->{work_time});
  1795. ThrowUserError('comment_required')
  1796. if $comment eq '' && $params->{work_time} != 0;
  1797. }
  1798. if (exists $params->{type}) {
  1799. $params->{type} = $self->_check_comment_type($params->{type});
  1800. }
  1801. if (exists $params->{isprivate}) {
  1802. $params->{isprivate} =
  1803. $self->_check_commentprivacy($params->{isprivate});
  1804. }
  1805. # XXX We really should check extra_data, too.
  1806. if ($comment eq '' && !($params->{type} || $params->{work_time})) {
  1807. return;
  1808. }
  1809. # So we really want to comment. Make sure we are allowed to do so.
  1810. my $privs;
  1811. $self->check_can_change_field('longdesc', 0, 1, \$privs)
  1812. || ThrowUserError('illegal_change', { field => 'longdesc', privs => $privs });
  1813. $self->{added_comments} ||= [];
  1814. my $add_comment = dclone($params);
  1815. $add_comment->{thetext} = $comment;
  1816. # We only want to trick_taint fields that we know about--we don't
  1817. # want to accidentally let somebody set some field that's not OK
  1818. # to set!
  1819. foreach my $field (UPDATE_COMMENT_COLUMNS) {
  1820. trick_taint($add_comment->{$field}) if defined $add_comment->{$field};
  1821. }
  1822. push(@{$self->{added_comments}}, $add_comment);
  1823. }
  1824. # There was a lot of duplicate code when I wrote this as three separate
  1825. # functions, so I just combined them all into one. This is also easier for
  1826. # process_bug to use.
  1827. sub modify_keywords {
  1828. my ($self, $keywords, $action) = @_;
  1829. $action ||= "makeexact";
  1830. if (!grep($action eq $_, qw(add delete makeexact))) {
  1831. $action = "makeexact";
  1832. }
  1833. $keywords = $self->_check_keywords($keywords);
  1834. my (@result, $any_changes);
  1835. if ($action eq 'makeexact') {
  1836. @result = @$keywords;
  1837. # Check if anything was added or removed.
  1838. my @old_ids = map { $_->id } @{$self->keyword_objects};
  1839. my @new_ids = map { $_->id } @result;
  1840. my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
  1841. $any_changes = scalar @$removed || scalar @$added;
  1842. }
  1843. else {
  1844. # We're adding or deleting specific keywords.
  1845. my %keys = map {$_->id => $_} @{$self->keyword_objects};
  1846. if ($action eq 'add') {
  1847. $keys{$_->id} = $_ foreach @$keywords;
  1848. }
  1849. else {
  1850. delete $keys{$_->id} foreach @$keywords;
  1851. }
  1852. @result = values %keys;
  1853. $any_changes = scalar @$keywords;
  1854. }
  1855. # Make sure we retain the sort order.
  1856. @result = sort {lc($a->name) cmp lc($b->name)} @result;
  1857. if ($any_changes) {
  1858. my $privs;
  1859. my $new = join(', ', (map {$_->name} @result));
  1860. my $check = $self->check_can_change_field('keywords', 0, 1, \$privs)
  1861. || ThrowUserError('illegal_change', { field => 'keywords',
  1862. oldvalue => $self->keywords,
  1863. newvalue => $new,
  1864. privs => $privs });
  1865. }
  1866. $self->{'keyword_objects'} = \@result;
  1867. return $any_changes;
  1868. }
  1869. sub add_group {
  1870. my ($self, $group) = @_;
  1871. # Invalid ids are silently ignored. (We can't tell people whether
  1872. # or not a group exists.)
  1873. $group = new Bugzilla::Group($group) unless ref $group;
  1874. return unless $group;
  1875. # Make sure that bugs in this product can actually be restricted
  1876. # to this group.
  1877. grep($group->id == $_->id, @{$self->product_obj->groups_valid})
  1878. || ThrowUserError('group_invalid_restriction',
  1879. { product => $self->product, group_id => $group->id });
  1880. # OtherControl people can add groups only during a product change,
  1881. # and only when the group is not NA for them.
  1882. if (!Bugzilla->user->in_group($group->name)) {
  1883. my $controls = $self->product_obj->group_controls->{$group->id};
  1884. if (!$self->{_old_product_name}
  1885. || $controls->{othercontrol} == CONTROLMAPNA)
  1886. {
  1887. ThrowUserError('group_change_denied',
  1888. { bug => $self, group_id => $group->id });
  1889. }
  1890. }
  1891. my $current_groups = $self->groups_in;
  1892. if (!grep($group->id == $_->id, @$current_groups)) {
  1893. push(@$current_groups, $group);
  1894. }
  1895. }
  1896. sub remove_group {
  1897. my ($self, $group) = @_;
  1898. $group = new Bugzilla::Group($group) unless ref $group;
  1899. return unless $group;
  1900. # First, check if this is a valid group for this product.
  1901. # You can *always* remove a group that is not valid for this product, so
  1902. # we don't do any other checks if that's the case. (set_product does this.)
  1903. #
  1904. # This particularly happens when isbuggroup is no longer 1, and we're
  1905. # moving a bug to a new product.
  1906. if (grep($_->id == $group->id, @{$self->product_obj->groups_valid})) {
  1907. my $controls = $self->product_obj->group_controls->{$group->id};
  1908. # Nobody can ever remove a Mandatory group.
  1909. if ($controls->{membercontrol} == CONTROLMAPMANDATORY) {
  1910. ThrowUserError('group_invalid_removal',
  1911. { product => $self->product, group_id => $group->id,
  1912. bug => $self });
  1913. }
  1914. # OtherControl people can remove groups only during a product change,
  1915. # and only when they are non-Mandatory and non-NA.
  1916. if (!Bugzilla->user->in_group($group->name)) {
  1917. if (!$self->{_old_product_name}
  1918. || $controls->{othercontrol} == CONTROLMAPMANDATORY
  1919. || $controls->{othercontrol} == CONTROLMAPNA)
  1920. {
  1921. ThrowUserError('group_change_denied',
  1922. { bug => $self, group_id => $group->id });
  1923. }
  1924. }
  1925. }
  1926. my $current_groups = $self->groups_in;
  1927. @$current_groups = grep { $_->id != $group->id } @$current_groups;
  1928. }
  1929. #####################################################################
  1930. # Instance Accessors
  1931. #####################################################################
  1932. # These subs are in alphabetical order, as much as possible.
  1933. # If you add a new sub, please try to keep it in alphabetical order
  1934. # with the other ones.
  1935. # Note: If you add a new method, remember that you must check the error
  1936. # state of the bug before returning any data. If $self->{error} is
  1937. # defined, then return something empty. Otherwise you risk potential
  1938. # security holes.
  1939. sub dup_id {
  1940. my ($self) = @_;
  1941. return $self->{'dup_id'} if exists $self->{'dup_id'};
  1942. $self->{'dup_id'} = undef;
  1943. return if $self->{'error'};
  1944. if ($self->{'resolution'} eq 'DUPLICATE') {
  1945. my $dbh = Bugzilla->dbh;
  1946. $self->{'dup_id'} =
  1947. $dbh->selectrow_array(q{SELECT dupe_of
  1948. FROM duplicates
  1949. WHERE dupe = ?},
  1950. undef,
  1951. $self->{'bug_id'});
  1952. }
  1953. return $self->{'dup_id'};
  1954. }
  1955. sub actual_time {
  1956. my ($self) = @_;
  1957. return $self->{'actual_time'} if exists $self->{'actual_time'};
  1958. if ( $self->{'error'} ||
  1959. !Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"}) ) {
  1960. $self->{'actual_time'} = undef;
  1961. return $self->{'actual_time'};
  1962. }
  1963. my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time)
  1964. FROM longdescs
  1965. WHERE longdescs.bug_id=?");
  1966. $sth->execute($self->{bug_id});
  1967. $self->{'actual_time'} = $sth->fetchrow_array();
  1968. return $self->{'actual_time'};
  1969. }
  1970. sub any_flags_requesteeble {
  1971. my ($self) = @_;
  1972. return $self->{'any_flags_requesteeble'}
  1973. if exists $self->{'any_flags_requesteeble'};
  1974. return 0 if $self->{'error'};
  1975. $self->{'any_flags_requesteeble'} =
  1976. grep($_->{'is_requesteeble'}, @{$self->flag_types});
  1977. return $self->{'any_flags_requesteeble'};
  1978. }
  1979. sub attachments {
  1980. my ($self) = @_;
  1981. return $self->{'attachments'} if exists $self->{'attachments'};
  1982. return [] if $self->{'error'};
  1983. my $attachments = Bugzilla::Attachment->get_attachments_by_bug($self->bug_id);
  1984. $_->{'flags'} = [] foreach @$attachments;
  1985. my %att = map { $_->id => $_ } @$attachments;
  1986. # Retrieve all attachment flags at once for this bug, and group them
  1987. # by attachment. We populate attachment flags here to avoid querying
  1988. # the DB for each attachment individually later.
  1989. my $flags = Bugzilla::Flag->match({ 'bug_id' => $self->bug_id,
  1990. 'target_type' => 'attachment' });
  1991. # Exclude flags for private attachments you cannot see.
  1992. @$flags = grep {exists $att{$_->attach_id}} @$flags;
  1993. push(@{$att{$_->attach_id}->{'flags'}}, $_) foreach @$flags;
  1994. $self->{'attachments'} = [sort {$a->id <=> $b->id} values %att];
  1995. return $self->{'attachments'};
  1996. }
  1997. sub assigned_to {
  1998. my ($self) = @_;
  1999. return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'};
  2000. $self->{'assigned_to'} = 0 if $self->{'error'};
  2001. $self->{'assigned_to_obj'} ||= new Bugzilla::User($self->{'assigned_to'});
  2002. return $self->{'assigned_to_obj'};
  2003. }
  2004. sub blocked {
  2005. my ($self) = @_;
  2006. return $self->{'blocked'} if exists $self->{'blocked'};
  2007. return [] if $self->{'error'};
  2008. $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id);
  2009. return $self->{'blocked'};
  2010. }
  2011. # Even bugs in an error state always have a bug_id.
  2012. sub bug_id { $_[0]->{'bug_id'}; }
  2013. sub cc {
  2014. my ($self) = @_;
  2015. return $self->{'cc'} if exists $self->{'cc'};
  2016. return [] if $self->{'error'};
  2017. my $dbh = Bugzilla->dbh;
  2018. $self->{'cc'} = $dbh->selectcol_arrayref(
  2019. q{SELECT profiles.login_name FROM cc, profiles
  2020. WHERE bug_id = ?
  2021. AND cc.who = profiles.userid
  2022. ORDER BY profiles.login_name},
  2023. undef, $self->bug_id);
  2024. $self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
  2025. return $self->{'cc'};
  2026. }
  2027. # XXX Eventually this will become the standard "cc" method used everywhere.
  2028. sub cc_users {
  2029. my $self = shift;
  2030. return $self->{'cc_users'} if exists $self->{'cc_users'};
  2031. return [] if $self->{'error'};
  2032. my $dbh = Bugzilla->dbh;
  2033. my $cc_ids = $dbh->selectcol_arrayref(
  2034. 'SELECT who FROM cc WHERE bug_id = ?', undef, $self->id);
  2035. $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids);
  2036. return $self->{'cc_users'};
  2037. }
  2038. sub component {
  2039. my ($self) = @_;
  2040. return $self->{component} if exists $self->{component};
  2041. return '' if $self->{error};
  2042. ($self->{component}) = Bugzilla->dbh->selectrow_array(
  2043. 'SELECT name FROM components WHERE id = ?',
  2044. undef, $self->{component_id});
  2045. return $self->{component};
  2046. }
  2047. # XXX Eventually this will replace component()
  2048. sub component_obj {
  2049. my ($self) = @_;
  2050. return $self->{component_obj} if defined $self->{component_obj};
  2051. return {} if $self->{error};
  2052. $self->{component_obj} = new Bugzilla::Component($self->{component_id});
  2053. return $self->{component_obj};
  2054. }
  2055. sub classification_id {
  2056. my ($self) = @_;
  2057. return $self->{classification_id} if exists $self->{classification_id};
  2058. return 0 if $self->{error};
  2059. ($self->{classification_id}) = Bugzilla->dbh->selectrow_array(
  2060. 'SELECT classification_id FROM products WHERE id = ?',
  2061. undef, $self->{product_id});
  2062. return $self->{classification_id};
  2063. }
  2064. sub classification {
  2065. my ($self) = @_;
  2066. return $self->{classification} if exists $self->{classification};
  2067. return '' if $self->{error};
  2068. ($self->{classification}) = Bugzilla->dbh->selectrow_array(
  2069. 'SELECT name FROM classifications WHERE id = ?',
  2070. undef, $self->classification_id);
  2071. return $self->{classification};
  2072. }
  2073. sub dependson {
  2074. my ($self) = @_;
  2075. return $self->{'dependson'} if exists $self->{'dependson'};
  2076. return [] if $self->{'error'};
  2077. $self->{'dependson'} =
  2078. EmitDependList("blocked", "dependson", $self->bug_id);
  2079. return $self->{'dependson'};
  2080. }
  2081. sub flag_types {
  2082. my ($self) = @_;
  2083. return $self->{'flag_types'} if exists $self->{'flag_types'};
  2084. return [] if $self->{'error'};
  2085. # The types of flags that can be set on this bug.
  2086. # If none, no UI for setting flags will be displayed.
  2087. my $flag_types = Bugzilla::FlagType::match(
  2088. {'target_type' => 'bug',
  2089. 'product_id' => $self->{'product_id'},
  2090. 'component_id' => $self->{'component_id'} });
  2091. $_->{'flags'} = [] foreach @$flag_types;
  2092. my %flagtypes = map { $_->id => $_ } @$flag_types;
  2093. # Retrieve all bug flags at once for this bug and group them
  2094. # by flag types.
  2095. my $flags = Bugzilla::Flag->match({ 'bug_id' => $self->bug_id,
  2096. 'target_type' => 'bug' });
  2097. # Call the internal 'type_id' variable instead of the method
  2098. # to not create a flagtype object.
  2099. push(@{$flagtypes{$_->{'type_id'}}->{'flags'}}, $_) foreach @$flags;
  2100. $self->{'flag_types'} =
  2101. [sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
  2102. return $self->{'flag_types'};
  2103. }
  2104. sub isopened {
  2105. my $self = shift;
  2106. return is_open_state($self->{bug_status}) ? 1 : 0;
  2107. }
  2108. sub isunconfirmed {
  2109. my $self = shift;
  2110. return ($self->bug_status eq 'UNCONFIRMED') ? 1 : 0;
  2111. }
  2112. sub keywords {
  2113. my ($self) = @_;
  2114. return join(', ', (map { $_->name } @{$self->keyword_objects}));
  2115. }
  2116. # XXX At some point, this should probably replace the normal "keywords" sub.
  2117. sub keyword_objects {
  2118. my $self = shift;
  2119. return $self->{'keyword_objects'} if defined $self->{'keyword_objects'};
  2120. return [] if $self->{'error'};
  2121. my $dbh = Bugzilla->dbh;
  2122. my $ids = $dbh->selectcol_arrayref(
  2123. "SELECT keywordid FROM keywords WHERE bug_id = ?", undef, $self->id);
  2124. $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids);
  2125. return $self->{'keyword_objects'};
  2126. }
  2127. sub longdescs {
  2128. my ($self) = @_;
  2129. return $self->{'longdescs'} if exists $self->{'longdescs'};
  2130. return [] if $self->{'error'};
  2131. $self->{'longdescs'} = GetComments($self->{bug_id});
  2132. return $self->{'longdescs'};
  2133. }
  2134. sub milestoneurl {
  2135. my ($self) = @_;
  2136. return $self->{'milestoneurl'} if exists $self->{'milestoneurl'};
  2137. return '' if $self->{'error'};
  2138. $self->{'milestoneurl'} = $self->product_obj->milestone_url;
  2139. return $self->{'milestoneurl'};
  2140. }
  2141. sub product {
  2142. my ($self) = @_;
  2143. return $self->{product} if exists $self->{product};
  2144. return '' if $self->{error};
  2145. ($self->{product}) = Bugzilla->dbh->selectrow_array(
  2146. 'SELECT name FROM products WHERE id = ?',
  2147. undef, $self->{product_id});
  2148. return $self->{product};
  2149. }
  2150. # XXX This should eventually replace the "product" subroutine.
  2151. sub product_obj {
  2152. my $self = shift;
  2153. return {} if $self->{error};
  2154. $self->{product_obj} ||= new Bugzilla::Product($self->{product_id});
  2155. return $self->{product_obj};
  2156. }
  2157. sub qa_contact {
  2158. my ($self) = @_;
  2159. return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'};
  2160. return undef if $self->{'error'};
  2161. if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
  2162. $self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
  2163. } else {
  2164. # XXX - This is somewhat inconsistent with the assignee/reporter
  2165. # methods, which will return an empty User if they get a 0.
  2166. # However, we're keeping it this way now, for backwards-compatibility.
  2167. $self->{'qa_contact_obj'} = undef;
  2168. }
  2169. return $self->{'qa_contact_obj'};
  2170. }
  2171. sub reporter {
  2172. my ($self) = @_;
  2173. return $self->{'reporter'} if exists $self->{'reporter'};
  2174. $self->{'reporter_id'} = 0 if $self->{'error'};
  2175. $self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'});
  2176. return $self->{'reporter'};
  2177. }
  2178. sub status {
  2179. my $self = shift;
  2180. return undef if $self->{'error'};
  2181. $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}});
  2182. return $self->{'status'};
  2183. }
  2184. sub show_attachment_flags {
  2185. my ($self) = @_;
  2186. return $self->{'show_attachment_flags'}
  2187. if exists $self->{'show_attachment_flags'};
  2188. return 0 if $self->{'error'};
  2189. # The number of types of flags that can be set on attachments to this bug
  2190. # and the number of flags on those attachments. One of these counts must be
  2191. # greater than zero in order for the "flags" column to appear in the table
  2192. # of attachments.
  2193. my $num_attachment_flag_types = Bugzilla::FlagType::count(
  2194. { 'target_type' => 'attachment',
  2195. 'product_id' => $self->{'product_id'},
  2196. 'component_id' => $self->{'component_id'} });
  2197. my $num_attachment_flags = Bugzilla::Flag->count(
  2198. { 'target_type' => 'attachment',
  2199. 'bug_id' => $self->bug_id });
  2200. $self->{'show_attachment_flags'} =
  2201. ($num_attachment_flag_types || $num_attachment_flags);
  2202. return $self->{'show_attachment_flags'};
  2203. }
  2204. sub use_votes {
  2205. my ($self) = @_;
  2206. return 0 if $self->{'error'};
  2207. return Bugzilla->params->{'usevotes'}
  2208. && $self->product_obj->votes_per_user > 0;
  2209. }
  2210. sub groups {
  2211. my $self = shift;
  2212. return $self->{'groups'} if exists $self->{'groups'};
  2213. return [] if $self->{'error'};
  2214. my $dbh = Bugzilla->dbh;
  2215. my @groups;
  2216. # Some of this stuff needs to go into Bugzilla::User
  2217. # For every group, we need to know if there is ANY bug_group_map
  2218. # record putting the current bug in that group and if there is ANY
  2219. # user_group_map record putting the user in that group.
  2220. # The LEFT JOINs are checking for record existence.
  2221. #
  2222. my $grouplist = Bugzilla->user->groups_as_string;
  2223. my $sth = $dbh->prepare(
  2224. "SELECT DISTINCT groups.id, name, description," .
  2225. " CASE WHEN bug_group_map.group_id IS NOT NULL" .
  2226. " THEN 1 ELSE 0 END," .
  2227. " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," .
  2228. " isactive, membercontrol, othercontrol" .
  2229. " FROM groups" .
  2230. " LEFT JOIN bug_group_map" .
  2231. " ON bug_group_map.group_id = groups.id" .
  2232. " AND bug_id = ?" .
  2233. " LEFT JOIN group_control_map" .
  2234. " ON group_control_map.group_id = groups.id" .
  2235. " AND group_control_map.product_id = ? " .
  2236. " WHERE isbuggroup = 1" .
  2237. " ORDER BY description");
  2238. $sth->execute($self->{'bug_id'},
  2239. $self->{'product_id'});
  2240. while (my ($groupid, $name, $description, $ison, $ingroup, $isactive,
  2241. $membercontrol, $othercontrol) = $sth->fetchrow_array()) {
  2242. $membercontrol ||= 0;
  2243. # For product groups, we only want to use the group if either
  2244. # (1) The bit is set and not required, or
  2245. # (2) The group is Shown or Default for members and
  2246. # the user is a member of the group.
  2247. if ($ison ||
  2248. ($isactive && $ingroup
  2249. && (($membercontrol == CONTROLMAPDEFAULT)
  2250. || ($membercontrol == CONTROLMAPSHOWN))
  2251. ))
  2252. {
  2253. my $ismandatory = $isactive
  2254. && ($membercontrol == CONTROLMAPMANDATORY);
  2255. push (@groups, { "bit" => $groupid,
  2256. "name" => $name,
  2257. "ison" => $ison,
  2258. "ingroup" => $ingroup,
  2259. "mandatory" => $ismandatory,
  2260. "description" => $description });
  2261. }
  2262. }
  2263. $self->{'groups'} = \@groups;
  2264. return $self->{'groups'};
  2265. }
  2266. sub groups_in {
  2267. my $self = shift;
  2268. return $self->{'groups_in'} if exists $self->{'groups_in'};
  2269. return [] if $self->{'error'};
  2270. my $group_ids = Bugzilla->dbh->selectcol_arrayref(
  2271. 'SELECT group_id FROM bug_group_map WHERE bug_id = ?',
  2272. undef, $self->id);
  2273. $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids);
  2274. return $self->{'groups_in'};
  2275. }
  2276. sub user {
  2277. my $self = shift;
  2278. return $self->{'user'} if exists $self->{'user'};
  2279. return {} if $self->{'error'};
  2280. my $user = Bugzilla->user;
  2281. my $canmove = Bugzilla->params->{'move-enabled'} && $user->is_mover;
  2282. my $prod_id = $self->{'product_id'};
  2283. my $unknown_privileges = $user->in_group('editbugs', $prod_id);
  2284. my $canedit = $unknown_privileges
  2285. || $user->id == $self->{'assigned_to'}
  2286. || (Bugzilla->params->{'useqacontact'}
  2287. && $self->{'qa_contact'}
  2288. && $user->id == $self->{'qa_contact'});
  2289. my $canconfirm = $unknown_privileges
  2290. || $user->in_group('canconfirm', $prod_id);
  2291. my $isreporter = $user->id
  2292. && $user->id == $self->{reporter_id};
  2293. $self->{'user'} = {canmove => $canmove,
  2294. canconfirm => $canconfirm,
  2295. canedit => $canedit,
  2296. isreporter => $isreporter};
  2297. return $self->{'user'};
  2298. }
  2299. sub choices {
  2300. my $self = shift;
  2301. return $self->{'choices'} if exists $self->{'choices'};
  2302. return {} if $self->{'error'};
  2303. $self->{'choices'} = {};
  2304. my @prodlist = map {$_->name} @{Bugzilla->user->get_enterable_products};
  2305. # The current product is part of the popup, even if new bugs are no longer
  2306. # allowed for that product
  2307. if (lsearch(\@prodlist, $self->product) < 0) {
  2308. push(@prodlist, $self->product);
  2309. @prodlist = sort @prodlist;
  2310. }
  2311. # Hack - this array contains "". See bug 106589.
  2312. my @res = grep ($_, @{get_legal_field_values('resolution')});
  2313. $self->{'choices'} =
  2314. {
  2315. 'product' => \@prodlist,
  2316. 'rep_platform' => get_legal_field_values('rep_platform'),
  2317. 'priority' => get_legal_field_values('priority'),
  2318. 'bug_severity' => get_legal_field_values('bug_severity'),
  2319. 'op_sys' => get_legal_field_values('op_sys'),
  2320. 'bug_status' => get_legal_field_values('bug_status'),
  2321. 'resolution' => \@res,
  2322. 'component' => [map($_->name, @{$self->product_obj->components})],
  2323. 'version' => [map($_->name, @{$self->product_obj->versions})],
  2324. 'target_milestone' => [map($_->name, @{$self->product_obj->milestones})],
  2325. };
  2326. return $self->{'choices'};
  2327. }
  2328. sub votes {
  2329. my ($self) = @_;
  2330. return 0 if $self->{error};
  2331. return $self->{votes} if defined $self->{votes};
  2332. my $dbh = Bugzilla->dbh;
  2333. $self->{votes} = $dbh->selectrow_array(
  2334. 'SELECT SUM(vote_count) FROM votes
  2335. WHERE bug_id = ? ' . $dbh->sql_group_by('bug_id'),
  2336. undef, $self->bug_id);
  2337. $self->{votes} ||= 0;
  2338. return $self->{votes};
  2339. }
  2340. # Convenience Function. If you need speed, use this. If you need
  2341. # other Bug fields in addition to this, just create a new Bug with
  2342. # the alias.
  2343. # Queries the database for the bug with a given alias, and returns
  2344. # the ID of the bug if it exists or the undefined value if it doesn't.
  2345. sub bug_alias_to_id {
  2346. my ($alias) = @_;
  2347. return undef unless Bugzilla->params->{"usebugaliases"};
  2348. my $dbh = Bugzilla->dbh;
  2349. trick_taint($alias);
  2350. return $dbh->selectrow_array(
  2351. "SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias);
  2352. }
  2353. #####################################################################
  2354. # Subroutines
  2355. #####################################################################
  2356. sub update_comment {
  2357. my ($self, $comment_id, $new_comment) = @_;
  2358. # Some validation checks.
  2359. if ($self->{'error'}) {
  2360. ThrowCodeError("bug_error", { bug => $self });
  2361. }
  2362. detaint_natural($comment_id)
  2363. || ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
  2364. # The comment ID must belong to this bug.
  2365. my @current_comment_obj = grep {$_->{'id'} == $comment_id} @{$self->longdescs};
  2366. scalar(@current_comment_obj)
  2367. || ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
  2368. # If the new comment is undefined, then there is nothing to update.
  2369. # To delete a comment, an empty string should be passed.
  2370. return unless defined $new_comment;
  2371. $new_comment =~ s/\s*$//s; # Remove trailing whitespaces.
  2372. $new_comment =~ s/\r\n?/\n/g; # Handle Windows and Mac-style line endings.
  2373. trick_taint($new_comment);
  2374. # We assume _check_comment() has already been called earlier.
  2375. Bugzilla->dbh->do('UPDATE longdescs SET thetext = ? WHERE comment_id = ?',
  2376. undef, ($new_comment, $comment_id));
  2377. $self->_sync_fulltext();
  2378. # Update the comment object with this new text.
  2379. $current_comment_obj[0]->{'body'} = $new_comment;
  2380. }
  2381. # Represents which fields from the bugs table are handled by process_bug.cgi.
  2382. sub editable_bug_fields {
  2383. my @fields = Bugzilla->dbh->bz_table_columns('bugs');
  2384. # Obsolete custom fields are not editable.
  2385. my @obsolete_fields = Bugzilla->get_fields({obsolete => 1, custom => 1});
  2386. @obsolete_fields = map { $_->name } @obsolete_fields;
  2387. foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", @obsolete_fields) {
  2388. my $location = lsearch(\@fields, $remove);
  2389. # Custom multi-select fields are not stored in the bugs table.
  2390. splice(@fields, $location, 1) if ($location > -1);
  2391. }
  2392. # Sorted because the old @::log_columns variable, which this replaces,
  2393. # was sorted.
  2394. return sort(@fields);
  2395. }
  2396. # XXX - When Bug::update() will be implemented, we should make this routine
  2397. # a private method.
  2398. sub EmitDependList {
  2399. my ($myfield, $targetfield, $bug_id) = (@_);
  2400. my $dbh = Bugzilla->dbh;
  2401. my $list_ref = $dbh->selectcol_arrayref(
  2402. "SELECT $targetfield FROM dependencies
  2403. WHERE $myfield = ? ORDER BY $targetfield",
  2404. undef, $bug_id);
  2405. return $list_ref;
  2406. }
  2407. sub ValidateTime {
  2408. my ($time, $field) = @_;
  2409. # regexp verifies one or more digits, optionally followed by a period and
  2410. # zero or more digits, OR we have a period followed by one or more digits
  2411. # (allow negatives, though, so people can back out errors in time reporting)
  2412. if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) {
  2413. ThrowUserError("number_not_numeric",
  2414. {field => "$field", num => "$time"});
  2415. }
  2416. # Only the "work_time" field is allowed to contain a negative value.
  2417. if ( ($time < 0) && ($field ne "work_time") ) {
  2418. ThrowUserError("number_too_small",
  2419. {field => "$field", num => "$time", min_num => "0"});
  2420. }
  2421. if ($time > 99999.99) {
  2422. ThrowUserError("number_too_large",
  2423. {field => "$field", num => "$time", max_num => "99999.99"});
  2424. }
  2425. }
  2426. sub GetComments {
  2427. my ($id, $comment_sort_order, $start, $end, $raw) = @_;
  2428. my $dbh = Bugzilla->dbh;
  2429. $comment_sort_order = $comment_sort_order ||
  2430. Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
  2431. my $sort_order = ($comment_sort_order eq "oldest_to_newest") ? 'asc' : 'desc';
  2432. my @comments;
  2433. my @args = ($id);
  2434. my $query = 'SELECT longdescs.comment_id AS id, profiles.userid, ' .
  2435. $dbh->sql_date_format('longdescs.bug_when', '%Y.%m.%d %H:%i:%s') .
  2436. ' AS time, longdescs.thetext AS body, longdescs.work_time,
  2437. isprivate, already_wrapped, type, extra_data
  2438. FROM longdescs
  2439. INNER JOIN profiles
  2440. ON profiles.userid = longdescs.who
  2441. WHERE longdescs.bug_id = ?';
  2442. if ($start) {
  2443. $query .= ' AND longdescs.bug_when > ?
  2444. AND longdescs.bug_when <= ?';
  2445. push(@args, ($start, $end));
  2446. }
  2447. $query .= " ORDER BY longdescs.bug_when $sort_order";
  2448. my $sth = $dbh->prepare($query);
  2449. $sth->execute(@args);
  2450. while (my $comment_ref = $sth->fetchrow_hashref()) {
  2451. my %comment = %$comment_ref;
  2452. $comment{'author'} = new Bugzilla::User($comment{'userid'});
  2453. # If raw data is requested, do not format 'special' comments.
  2454. $comment{'body'} = format_comment(\%comment) unless $raw;
  2455. push (@comments, \%comment);
  2456. }
  2457. if ($comment_sort_order eq "newest_to_oldest_desc_first") {
  2458. unshift(@comments, pop @comments);
  2459. }
  2460. return \@comments;
  2461. }
  2462. # Format language specific comments. This routine must not update
  2463. # $comment{'body'} itself, see BugMail::prepare_comments().
  2464. sub format_comment {
  2465. my $comment = shift;
  2466. my $body;
  2467. if ($comment->{'type'} == CMT_DUPE_OF) {
  2468. $body = $comment->{'body'} . "\n\n" .
  2469. get_text('bug_duplicate_of', { dupe_of => $comment->{'extra_data'} });
  2470. }
  2471. elsif ($comment->{'type'} == CMT_HAS_DUPE) {
  2472. $body = get_text('bug_has_duplicate', { dupe => $comment->{'extra_data'} });
  2473. }
  2474. elsif ($comment->{'type'} == CMT_POPULAR_VOTES) {
  2475. $body = get_text('bug_confirmed_by_votes');
  2476. }
  2477. elsif ($comment->{'type'} == CMT_MOVED_TO) {
  2478. $body = $comment->{'body'} . "\n\n" .
  2479. get_text('bug_moved_to', { login => $comment->{'extra_data'} });
  2480. }
  2481. else {
  2482. $body = $comment->{'body'};
  2483. }
  2484. return $body;
  2485. }
  2486. # Get the activity of a bug, starting from $starttime (if given).
  2487. # This routine assumes ValidateBugID has been previously called.
  2488. sub GetBugActivity {
  2489. my ($bug_id, $attach_id, $starttime) = @_;
  2490. my $dbh = Bugzilla->dbh;
  2491. # Arguments passed to the SQL query.
  2492. my @args = ($bug_id);
  2493. # Only consider changes since $starttime, if given.
  2494. my $datepart = "";
  2495. if (defined $starttime) {
  2496. trick_taint($starttime);
  2497. push (@args, $starttime);
  2498. $datepart = "AND bugs_activity.bug_when > ?";
  2499. }
  2500. my $attachpart = "";
  2501. if ($attach_id) {
  2502. push(@args, $attach_id);
  2503. $attachpart = "AND bugs_activity.attach_id = ?";
  2504. }
  2505. # Only includes attachments the user is allowed to see.
  2506. my $suppjoins = "";
  2507. my $suppwhere = "";
  2508. if (Bugzilla->params->{"insidergroup"}
  2509. && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'}))
  2510. {
  2511. $suppjoins = "LEFT JOIN attachments
  2512. ON attachments.attach_id = bugs_activity.attach_id";
  2513. $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0";
  2514. }
  2515. my $query = "
  2516. SELECT COALESCE(fielddefs.description, "
  2517. # This is a hack - PostgreSQL requires both COALESCE
  2518. # arguments to be of the same type, and this is the only
  2519. # way supported by both MySQL 3 and PostgreSQL to convert
  2520. # an integer to a string. MySQL 4 supports CAST.
  2521. . $dbh->sql_string_concat('bugs_activity.fieldid', q{''}) .
  2522. "), fielddefs.name, bugs_activity.attach_id, " .
  2523. $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
  2524. ", bugs_activity.removed, bugs_activity.added, profiles.login_name
  2525. FROM bugs_activity
  2526. $suppjoins
  2527. LEFT JOIN fielddefs
  2528. ON bugs_activity.fieldid = fielddefs.id
  2529. INNER JOIN profiles
  2530. ON profiles.userid = bugs_activity.who
  2531. WHERE bugs_activity.bug_id = ?
  2532. $datepart
  2533. $attachpart
  2534. $suppwhere
  2535. ORDER BY bugs_activity.bug_when";
  2536. my $list = $dbh->selectall_arrayref($query, undef, @args);
  2537. my @operations;
  2538. my $operation = {};
  2539. my $changes = [];
  2540. my $incomplete_data = 0;
  2541. foreach my $entry (@$list) {
  2542. my ($field, $fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
  2543. my %change;
  2544. my $activity_visible = 1;
  2545. # check if the user should see this field's activity
  2546. if ($fieldname eq 'remaining_time'
  2547. || $fieldname eq 'estimated_time'
  2548. || $fieldname eq 'work_time'
  2549. || $fieldname eq 'deadline')
  2550. {
  2551. $activity_visible =
  2552. Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'}) ? 1 : 0;
  2553. } else {
  2554. $activity_visible = 1;
  2555. }
  2556. if ($activity_visible) {
  2557. # This gets replaced with a hyperlink in the template.
  2558. $field =~ s/^Attachment\s*// if $attachid;
  2559. # Check for the results of an old Bugzilla data corruption bug
  2560. $incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
  2561. # An operation, done by 'who' at time 'when', has a number of
  2562. # 'changes' associated with it.
  2563. # If this is the start of a new operation, store the data from the
  2564. # previous one, and set up the new one.
  2565. if ($operation->{'who'}
  2566. && ($who ne $operation->{'who'}
  2567. || $when ne $operation->{'when'}))
  2568. {
  2569. $operation->{'changes'} = $changes;
  2570. push (@operations, $operation);
  2571. # Create new empty anonymous data structures.
  2572. $operation = {};
  2573. $changes = [];
  2574. }
  2575. $operation->{'who'} = $who;
  2576. $operation->{'when'} = $when;
  2577. $change{'field'} = $field;
  2578. $change{'fieldname'} = $fieldname;
  2579. $change{'attachid'} = $attachid;
  2580. $change{'removed'} = $removed;
  2581. $change{'added'} = $added;
  2582. push (@$changes, \%change);
  2583. }
  2584. }
  2585. if ($operation->{'who'}) {
  2586. $operation->{'changes'} = $changes;
  2587. push (@operations, $operation);
  2588. }
  2589. return(\@operations, $incomplete_data);
  2590. }
  2591. # Update the bugs_activity table to reflect changes made in bugs.
  2592. sub LogActivityEntry {
  2593. my ($i, $col, $removed, $added, $whoid, $timestamp) = @_;
  2594. my $dbh = Bugzilla->dbh;
  2595. # in the case of CCs, deps, and keywords, there's a possibility that someone
  2596. # might try to add or remove a lot of them at once, which might take more
  2597. # space than the activity table allows. We'll solve this by splitting it
  2598. # into multiple entries if it's too long.
  2599. while ($removed || $added) {
  2600. my ($removestr, $addstr) = ($removed, $added);
  2601. if (length($removestr) > MAX_LINE_LENGTH) {
  2602. my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH);
  2603. $removestr = substr($removed, 0, $commaposition);
  2604. $removed = substr($removed, $commaposition);
  2605. $removed =~ s/^[,\s]+//; # remove any comma or space
  2606. } else {
  2607. $removed = ""; # no more entries
  2608. }
  2609. if (length($addstr) > MAX_LINE_LENGTH) {
  2610. my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH);
  2611. $addstr = substr($added, 0, $commaposition);
  2612. $added = substr($added, $commaposition);
  2613. $added =~ s/^[,\s]+//; # remove any comma or space
  2614. } else {
  2615. $added = ""; # no more entries
  2616. }
  2617. trick_taint($addstr);
  2618. trick_taint($removestr);
  2619. my $fieldid = get_field_id($col);
  2620. $dbh->do("INSERT INTO bugs_activity
  2621. (bug_id, who, bug_when, fieldid, removed, added)
  2622. VALUES (?, ?, ?, ?, ?, ?)",
  2623. undef, ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr));
  2624. }
  2625. }
  2626. # CountOpenDependencies counts the number of open dependent bugs for a
  2627. # list of bugs and returns a list of bug_id's and their dependency count
  2628. # It takes one parameter:
  2629. # - A list of bug numbers whose dependencies are to be checked
  2630. sub CountOpenDependencies {
  2631. my (@bug_list) = @_;
  2632. my @dependencies;
  2633. my $dbh = Bugzilla->dbh;
  2634. my $sth = $dbh->prepare(
  2635. "SELECT blocked, COUNT(bug_status) " .
  2636. "FROM bugs, dependencies " .
  2637. "WHERE " . $dbh->sql_in('blocked', \@bug_list) .
  2638. "AND bug_id = dependson " .
  2639. "AND bug_status IN (" . join(', ', map {$dbh->quote($_)} BUG_STATE_OPEN) . ") " .
  2640. $dbh->sql_group_by('blocked'));
  2641. $sth->execute();
  2642. while (my ($bug_id, $dependencies) = $sth->fetchrow_array()) {
  2643. push(@dependencies, { bug_id => $bug_id,
  2644. dependencies => $dependencies });
  2645. }
  2646. return @dependencies;
  2647. }
  2648. # If a bug is moved to a product which allows less votes per bug
  2649. # compared to the previous product, extra votes need to be removed.
  2650. sub RemoveVotes {
  2651. my ($id, $who, $reason) = (@_);
  2652. my $dbh = Bugzilla->dbh;
  2653. my $whopart = ($who) ? " AND votes.who = $who" : "";
  2654. my $sth = $dbh->prepare("SELECT profiles.login_name, " .
  2655. "profiles.userid, votes.vote_count, " .
  2656. "products.votesperuser, products.maxvotesperbug " .
  2657. "FROM profiles " .
  2658. "LEFT JOIN votes ON profiles.userid = votes.who " .
  2659. "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
  2660. "LEFT JOIN products ON products.id = bugs.product_id " .
  2661. "WHERE votes.bug_id = ? " . $whopart);
  2662. $sth->execute($id);
  2663. my @list;
  2664. while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
  2665. push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
  2666. }
  2667. # @messages stores all emails which have to be sent, if any.
  2668. # This array is passed to the caller which will send these emails itself.
  2669. my @messages = ();
  2670. if (scalar(@list)) {
  2671. foreach my $ref (@list) {
  2672. my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
  2673. $maxvotesperbug = min($votesperuser, $maxvotesperbug);
  2674. # If this product allows voting and the user's votes are in
  2675. # the acceptable range, then don't do anything.
  2676. next if $votesperuser && $oldvotes <= $maxvotesperbug;
  2677. # If the user has more votes on this bug than this product
  2678. # allows, then reduce the number of votes so it fits
  2679. my $newvotes = $maxvotesperbug;
  2680. my $removedvotes = $oldvotes - $newvotes;
  2681. if ($newvotes) {
  2682. $dbh->do("UPDATE votes SET vote_count = ? " .
  2683. "WHERE bug_id = ? AND who = ?",
  2684. undef, ($newvotes, $id, $userid));
  2685. } else {
  2686. $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
  2687. undef, ($id, $userid));
  2688. }
  2689. # Notice that we did not make sure that the user fit within the $votesperuser
  2690. # range. This is considered to be an acceptable alternative to losing votes
  2691. # during product moves. Then next time the user attempts to change their votes,
  2692. # they will be forced to fit within the $votesperuser limit.
  2693. # Now lets send the e-mail to alert the user to the fact that their votes have
  2694. # been reduced or removed.
  2695. my $vars = {
  2696. 'to' => $name . Bugzilla->params->{'emailsuffix'},
  2697. 'bugid' => $id,
  2698. 'reason' => $reason,
  2699. 'votesremoved' => $removedvotes,
  2700. 'votesold' => $oldvotes,
  2701. 'votesnew' => $newvotes,
  2702. };
  2703. my $voter = new Bugzilla::User($userid);
  2704. my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'});
  2705. my $msg;
  2706. $template->process("email/votes-removed.txt.tmpl", $vars, \$msg);
  2707. push(@messages, $msg);
  2708. }
  2709. Bugzilla->template_inner("");
  2710. my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
  2711. "FROM votes WHERE bug_id = ?",
  2712. undef, $id) || 0;
  2713. $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
  2714. undef, ($votes, $id));
  2715. }
  2716. # Now return the array containing emails to be sent.
  2717. return \@messages;
  2718. }
  2719. # If a user votes for a bug, or the number of votes required to
  2720. # confirm a bug has been reduced, check if the bug is now confirmed.
  2721. sub CheckIfVotedConfirmed {
  2722. my ($id, $who) = (@_);
  2723. my $dbh = Bugzilla->dbh;
  2724. # XXX - Use bug methods to update the bug status and everconfirmed.
  2725. my $bug = new Bugzilla::Bug($id);
  2726. my ($votes, $status, $everconfirmed, $votestoconfirm, $timestamp) =
  2727. $dbh->selectrow_array("SELECT votes, bug_status, everconfirmed, " .
  2728. " votestoconfirm, NOW() " .
  2729. "FROM bugs INNER JOIN products " .
  2730. " ON products.id = bugs.product_id " .
  2731. "WHERE bugs.bug_id = ?",
  2732. undef, $id);
  2733. my $ret = 0;
  2734. if ($votes >= $votestoconfirm && !$everconfirmed) {
  2735. $bug->add_comment('', { type => CMT_POPULAR_VOTES });
  2736. $bug->update();
  2737. if ($status eq 'UNCONFIRMED') {
  2738. my $fieldid = get_field_id("bug_status");
  2739. $dbh->do("UPDATE bugs SET bug_status = 'NEW', everconfirmed = 1, " .
  2740. "delta_ts = ? WHERE bug_id = ?",
  2741. undef, ($timestamp, $id));
  2742. $dbh->do("INSERT INTO bugs_activity " .
  2743. "(bug_id, who, bug_when, fieldid, removed, added) " .
  2744. "VALUES (?, ?, ?, ?, ?, ?)",
  2745. undef, ($id, $who, $timestamp, $fieldid, 'UNCONFIRMED', 'NEW'));
  2746. }
  2747. else {
  2748. $dbh->do("UPDATE bugs SET everconfirmed = 1, delta_ts = ? " .
  2749. "WHERE bug_id = ?", undef, ($timestamp, $id));
  2750. }
  2751. my $fieldid = get_field_id("everconfirmed");
  2752. $dbh->do("INSERT INTO bugs_activity " .
  2753. "(bug_id, who, bug_when, fieldid, removed, added) " .
  2754. "VALUES (?, ?, ?, ?, ?, ?)",
  2755. undef, ($id, $who, $timestamp, $fieldid, '0', '1'));
  2756. $ret = 1;
  2757. }
  2758. return $ret;
  2759. }
  2760. ################################################################################
  2761. # check_can_change_field() defines what users are allowed to change. You
  2762. # can add code here for site-specific policy changes, according to the
  2763. # instructions given in the Bugzilla Guide and below. Note that you may also
  2764. # have to update the Bugzilla::Bug::user() function to give people access to the
  2765. # options that they are permitted to change.
  2766. #
  2767. # check_can_change_field() returns true if the user is allowed to change this
  2768. # field, and false if they are not.
  2769. #
  2770. # The parameters to this method are as follows:
  2771. # $field - name of the field in the bugs table the user is trying to change
  2772. # $oldvalue - what they are changing it from
  2773. # $newvalue - what they are changing it to
  2774. # $PrivilegesRequired - return the reason of the failure, if any
  2775. ################################################################################
  2776. sub check_can_change_field {
  2777. my $self = shift;
  2778. my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_);
  2779. my $user = Bugzilla->user;
  2780. $oldvalue = defined($oldvalue) ? $oldvalue : '';
  2781. $newvalue = defined($newvalue) ? $newvalue : '';
  2782. # Return true if they haven't changed this field at all.
  2783. if ($oldvalue eq $newvalue) {
  2784. return 1;
  2785. } elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') {
  2786. my ($removed, $added) = diff_arrays($oldvalue, $newvalue);
  2787. return 1 if !scalar(@$removed) && !scalar(@$added);
  2788. } elsif (trim($oldvalue) eq trim($newvalue)) {
  2789. return 1;
  2790. # numeric fields need to be compared using ==
  2791. } elsif (($field eq 'estimated_time' || $field eq 'remaining_time')
  2792. && $oldvalue == $newvalue)
  2793. {
  2794. return 1;
  2795. }
  2796. # Allow anyone to change comments.
  2797. if ($field =~ /^longdesc/) {
  2798. return 1;
  2799. }
  2800. # If the user isn't allowed to change a field, we must tell him who can.
  2801. # We store the required permission set into the $PrivilegesRequired
  2802. # variable which gets passed to the error template.
  2803. #
  2804. # $PrivilegesRequired = 0 : no privileges required;
  2805. # $PrivilegesRequired = 1 : the reporter, assignee or an empowered user;
  2806. # $PrivilegesRequired = 2 : the assignee or an empowered user;
  2807. # $PrivilegesRequired = 3 : an empowered user.
  2808. # Only users in the time-tracking group can change time-tracking fields.
  2809. if ( grep($_ eq $field, qw(deadline estimated_time remaining_time)) ) {
  2810. my $tt_group = Bugzilla->params->{timetrackinggroup};
  2811. if (!$tt_group || !$user->in_group($tt_group)) {
  2812. $$PrivilegesRequired = 3;
  2813. return 0;
  2814. }
  2815. }
  2816. # Allow anyone with (product-specific) "editbugs" privs to change anything.
  2817. if ($user->in_group('editbugs', $self->{'product_id'})) {
  2818. return 1;
  2819. }
  2820. # *Only* users with (product-specific) "canconfirm" privs can confirm bugs.
  2821. if ($field eq 'canconfirm'
  2822. || ($field eq 'bug_status'
  2823. && $oldvalue eq 'UNCONFIRMED'
  2824. && is_open_state($newvalue)))
  2825. {
  2826. $$PrivilegesRequired = 3;
  2827. return $user->in_group('canconfirm', $self->{'product_id'});
  2828. }
  2829. # Make sure that a valid bug ID has been given.
  2830. if (!$self->{'error'}) {
  2831. # Allow the assignee to change anything else.
  2832. if ($self->{'assigned_to'} == $user->id
  2833. || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id)
  2834. {
  2835. return 1;
  2836. }
  2837. # Allow the QA contact to change anything else.
  2838. if (Bugzilla->params->{'useqacontact'}
  2839. && (($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id)
  2840. || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id)))
  2841. {
  2842. return 1;
  2843. }
  2844. }
  2845. # At this point, the user is either the reporter or an
  2846. # unprivileged user. We first check for fields the reporter
  2847. # is not allowed to change.
  2848. # The reporter may not:
  2849. # - reassign bugs, unless the bugs are assigned to him;
  2850. # in that case we will have already returned 1 above
  2851. # when checking for the assignee of the bug.
  2852. if ($field eq 'assigned_to') {
  2853. $$PrivilegesRequired = 2;
  2854. return 0;
  2855. }
  2856. # - change the QA contact
  2857. if ($field eq 'qa_contact') {
  2858. $$PrivilegesRequired = 2;
  2859. return 0;
  2860. }
  2861. # - change the target milestone
  2862. if ($field eq 'target_milestone') {
  2863. $$PrivilegesRequired = 2;
  2864. return 0;
  2865. }
  2866. # - change the priority (unless he could have set it originally)
  2867. if ($field eq 'priority'
  2868. && !Bugzilla->params->{'letsubmitterchoosepriority'})
  2869. {
  2870. $$PrivilegesRequired = 2;
  2871. return 0;
  2872. }
  2873. # The reporter is allowed to change anything else.
  2874. if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) {
  2875. return 1;
  2876. }
  2877. # If we haven't returned by this point, then the user doesn't
  2878. # have the necessary permissions to change this field.
  2879. $$PrivilegesRequired = 1;
  2880. return 0;
  2881. }
  2882. #
  2883. # Field Validation
  2884. #
  2885. # Validates and verifies a bug ID, making sure the number is a
  2886. # positive integer, that it represents an existing bug in the
  2887. # database, and that the user is authorized to access that bug.
  2888. # We detaint the number here, too.
  2889. sub ValidateBugID {
  2890. my ($id, $field) = @_;
  2891. my $dbh = Bugzilla->dbh;
  2892. my $user = Bugzilla->user;
  2893. ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
  2894. # Get rid of leading '#' (number) mark, if present.
  2895. $id =~ s/^\s*#//;
  2896. # Remove whitespace
  2897. $id = trim($id);
  2898. # If the ID isn't a number, it might be an alias, so try to convert it.
  2899. my $alias = $id;
  2900. if (!detaint_natural($id)) {
  2901. $id = bug_alias_to_id($alias);
  2902. $id || ThrowUserError("improper_bug_id_field_value",
  2903. {'bug_id' => $alias,
  2904. 'field' => $field });
  2905. }
  2906. # Modify the calling code's original variable to contain the trimmed,
  2907. # converted-from-alias ID.
  2908. $_[0] = $id;
  2909. # First check that the bug exists
  2910. $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?", undef, $id)
  2911. || ThrowUserError("bug_id_does_not_exist", {'bug_id' => $id});
  2912. unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
  2913. check_is_visible($id);
  2914. }
  2915. }
  2916. sub check_is_visible {
  2917. my $id = shift;
  2918. my $user = Bugzilla->user;
  2919. return if $user->can_see_bug($id);
  2920. # The error the user sees depends on whether or not they are logged in
  2921. # (i.e. $user->id contains the user's positive integer ID).
  2922. if ($user->id) {
  2923. ThrowUserError("bug_access_denied", {'bug_id' => $id});
  2924. } else {
  2925. ThrowUserError("bug_access_query", {'bug_id' => $id});
  2926. }
  2927. }
  2928. # Validate and return a hash of dependencies
  2929. sub ValidateDependencies {
  2930. my $fields = {};
  2931. # These can be arrayrefs or they can be strings.
  2932. $fields->{'dependson'} = shift;
  2933. $fields->{'blocked'} = shift;
  2934. my $id = shift || 0;
  2935. unless (defined($fields->{'dependson'})
  2936. || defined($fields->{'blocked'}))
  2937. {
  2938. return;
  2939. }
  2940. my $dbh = Bugzilla->dbh;
  2941. my %deps;
  2942. my %deptree;
  2943. foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
  2944. my ($me, $target) = @{$pair};
  2945. $deptree{$target} = [];
  2946. $deps{$target} = [];
  2947. next unless $fields->{$target};
  2948. my %seen;
  2949. my $target_array = ref($fields->{$target}) ? $fields->{$target}
  2950. : [split(/[\s,]+/, $fields->{$target})];
  2951. foreach my $i (@$target_array) {
  2952. if ($id == $i) {
  2953. ThrowUserError("dependency_loop_single");
  2954. }
  2955. if (!exists $seen{$i}) {
  2956. push(@{$deptree{$target}}, $i);
  2957. $seen{$i} = 1;
  2958. }
  2959. }
  2960. # populate $deps{$target} as first-level deps only.
  2961. # and find remainder of dependency tree in $deptree{$target}
  2962. @{$deps{$target}} = @{$deptree{$target}};
  2963. my @stack = @{$deps{$target}};
  2964. while (@stack) {
  2965. my $i = shift @stack;
  2966. my $dep_list =
  2967. $dbh->selectcol_arrayref("SELECT $target
  2968. FROM dependencies
  2969. WHERE $me = ?", undef, $i);
  2970. foreach my $t (@$dep_list) {
  2971. # ignore any _current_ dependencies involving this bug,
  2972. # as they will be overwritten with data from the form.
  2973. if ($t != $id && !exists $seen{$t}) {
  2974. push(@{$deptree{$target}}, $t);
  2975. push @stack, $t;
  2976. $seen{$t} = 1;
  2977. }
  2978. }
  2979. }
  2980. }
  2981. my @deps = @{$deptree{'dependson'}};
  2982. my @blocks = @{$deptree{'blocked'}};
  2983. my %union = ();
  2984. my %isect = ();
  2985. foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
  2986. my @isect = keys %isect;
  2987. if (scalar(@isect) > 0) {
  2988. ThrowUserError("dependency_loop_multi", {'deps' => \@isect});
  2989. }
  2990. return %deps;
  2991. }
  2992. #####################################################################
  2993. # Autoloaded Accessors
  2994. #####################################################################
  2995. # Determines whether an attribute access trapped by the AUTOLOAD function
  2996. # is for a valid bug attribute. Bug attributes are properties and methods
  2997. # predefined by this module as well as bug fields for which an accessor
  2998. # can be defined by AUTOLOAD at runtime when the accessor is first accessed.
  2999. #
  3000. # XXX Strangely, some predefined attributes are on the list, but others aren't,
  3001. # and the original code didn't specify why that is. Presumably the only
  3002. # attributes that need to be on this list are those that aren't predefined;
  3003. # we should verify that and update the list accordingly.
  3004. #
  3005. sub _validate_attribute {
  3006. my ($attribute) = @_;
  3007. my @valid_attributes = (
  3008. # Miscellaneous properties and methods.
  3009. qw(error groups product_id component_id
  3010. longdescs milestoneurl attachments
  3011. isopened isunconfirmed
  3012. flag_types num_attachment_flag_types
  3013. show_attachment_flags any_flags_requesteeble),
  3014. # Bug fields.
  3015. Bugzilla::Bug->fields
  3016. );
  3017. return grep($attribute eq $_, @valid_attributes) ? 1 : 0;
  3018. }
  3019. sub AUTOLOAD {
  3020. use vars qw($AUTOLOAD);
  3021. my $attr = $AUTOLOAD;
  3022. $attr =~ s/.*:://;
  3023. return unless $attr=~ /[^A-Z]/;
  3024. if (!_validate_attribute($attr)) {
  3025. require Carp;
  3026. Carp::confess("invalid bug attribute $attr");
  3027. }
  3028. no strict 'refs';
  3029. *$AUTOLOAD = sub {
  3030. my $self = shift;
  3031. return $self->{$attr} if defined $self->{$attr};
  3032. $self->{_multi_selects} ||= [Bugzilla->get_fields(
  3033. {custom => 1, type => FIELD_TYPE_MULTI_SELECT })];
  3034. if ( grep($_->name eq $attr, @{$self->{_multi_selects}}) ) {
  3035. $self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
  3036. "SELECT value FROM bug_$attr WHERE bug_id = ? ORDER BY value",
  3037. undef, $self->id);
  3038. return $self->{$attr};
  3039. }
  3040. return '';
  3041. };
  3042. goto &$AUTOLOAD;
  3043. }
  3044. 1;