1<?php 2 3 4// must be run within Dokuwiki 5if(!defined('DOKU_INC')) die(); 6 7 8class BugzillaException extends Exception {} 9class BugNumberInvalidException extends BugzillaException {} 10 11 12class helper_plugin_bugzillaint_bugzillaclient extends DokuWiki_Plugin { 13 14 15 private $login; 16 private $password; 17 18 private $includeFields = array('id', 'summary', 'status', 'resolution', 'deadline', 'priority', 'severity'); 19 20 21 22 public function setCredentials( $login, $password ) { 23 $this->login = $login; 24 $this->password = $password; 25 } 26 27 28 /** 29 * Fetch dependency tree 30 * 31 * May throw a BugzillaException, for example if the credentials are invalid. 32 * 33 * @param $id 34 * @return associative array of bugs, with id as key and an array of bugs 35 */ 36 public function getBugDependencyTrees( $ids, $depth, $extras ) { 37 $f_ids = is_array($ids) ? $ids : explode(',', $ids); 38 return $this->getDependenciesRecursive( $f_ids, $depth == -1 ? $this->getConf('tree_depth') : $depth, $extras ); 39 } 40 private function getDependenciesRecursive( $ids, $depth, $extras ) { 41 $includeFields = array_merge( $this->includeFields, $this->extrasToIncludeFields($extras), array('depends_on') ); 42 $response = $this->bugzillaRPC('Bug.get', array('ids' => $ids, 'include_fields' => $includeFields )); 43 $result = array(); 44 foreach ($response['bugs'] as $bug) { 45 $result[ $bug['id'] ] = $bug; 46 } 47 foreach ( $result as &$bug ) { 48 if ( isset($bug['depends_on']) && count($bug['depends_on']) > 0 ) { 49 $bug['depends_on'] = $this->getDependenciesRecursive( $bug['depends_on'], $depth--, $extras ); 50 } else { 51 unset($bug['depends_on']); 52 } 53 } 54 return $result; 55 } 56 57 58 /** 59 * May throw a BugzillaException, for example if the credentials are invalid. 60 * 61 */ 62 public function quicksearch( $query, $extras, $groupBy ) { 63 64 65 // XXX should use bugzilla 5 quicksearch 66 67 68 // defaults 69 $options = array( 70 'status' => explode(',', 'NEW,OPEN,UNCO,REOP,ASSI') 71 ); 72 73 // parse query 74 if ( strpos($query, 'ALL') === 0 ) { 75 $options['status'] = explode(',', 'OPEN,REOP,UNCO,RESO,VERI,NEW,CONFIRMED,IN_PROGRESS,ASSIGNED'); 76 } 77 if ( strpos($query, 'OPEN') === 0 ) { 78 $options['status'] = explode(',', 'OPEN,REOP,UNCO,NEW,CONFIRMED,IN_PROGRESS,ASSIGNED'); 79 } 80 if ( strpos($query, 'UNCO') === 0 ) { 81 $options['status'] = explode(',', 'UNCO'); 82 } 83 if ( strpos($query, 'RESO') === 0 ) { 84 $options['status'] = explode(',', 'RESO'); 85 } 86 if ( strpos($query, 'VERI') === 0 ) { 87 $options['status'] = explode(',', 'VERI'); 88 } 89 if ( strpos($query, 'CLO') === 0 ) { 90 $options['status'] = explode(',', 'CLO'); 91 } 92 if ( strpos($query, 'FIXED') === 0 ) { 93 $options['resolution'] = explode(',', 'FIXED'); 94 } 95 if ( strpos($query, 'INVA') === 0 ) { 96 $options['resolution'] = explode(',', 'INVA'); 97 } 98 if ( strpos($query, 'WONT') === 0 ) { 99 $options['resolution'] = explode(',', 'WONTFIX'); 100 } 101 if ( strpos($query, 'DUP') === 0 ) { 102 $options['resolution'] = explode(',', 'DUPLICATE'); 103 } 104 if ( strpos($query, 'WORKS') === 0 ) { 105 $options['resolution'] = explode(',', 'WORKSFORME'); 106 } 107 if ( strpos($query, 'MOVED') === 0 ) { 108 $options['resolution'] = explode(',', 'MOVED'); 109 } 110 111 if ( preg_match('/product:([A-Za-z0-9_,]+)/', $query, $m) ) { 112 $options['product'] = explode(',', $m[1]); 113 } 114 if ( preg_match('/component:([A-Za-z0-9_,]+)/', $query, $m) ) { 115 $options['component'] = explode(',', $m[1]); 116 } 117 if ( preg_match('/classification:([A-Za-z0-9_,]+)/', $query, $m) ) { 118 if ( isset($options['product']) == false || count($options['product']) == 0 ) { 119 $c = explode(',', $m[1]); 120 $r = $this->bugzillaRPC('Classification.get', array('names' => $c ) ); 121 $pa = array(); 122 foreach ( $r['classifications'] as $c ) { 123 foreach ($c['products'] as $p) { 124 $pa[] = $p['name']; 125 } 126 } 127 $options['product'] = $pa; 128 } 129 } 130 if ( preg_match('/status:([A-Za-z0-9,]+)/', $query, $m) ) { 131 $options['status'] = explode(',', $m[1]); 132 } 133 if ( preg_match('/resolution:([A-Za-z0-9,]+)/', $query, $m) ) { 134 $options['resolution'] = explode(',', $m[1]); 135 } 136 if ( preg_match('/summary:([A-Za-z0-9,]+)/', $query, $m) ) { 137 $options['summary'] = explode(',', $m[1]); 138 } 139 if ( preg_match('/assigned_to:([A-Za-z0-9_,@.]+)/', $query, $m) ) { 140 $options['assigned_to'] = explode(',', $m[1]); 141 } 142 if ( preg_match('/creator:([A-Za-z0-9_,@.]+)/', $query, $m) ) { 143 $options['creator'] = explode(',', $m[1]); 144 } 145 if ( preg_match('/version:([A-Za-z0-9_\-\.]+)/', $query, $m) ) { 146 $options['version'] = explode(',', $m[1]); 147 } 148 if ( preg_match('/target_milestone:([A-Za-z0-9_.-]+)/', $query, $m) ) { 149 $options['target_milestone'] = explode(',', $m[1]); 150 } 151 152 153 // fix status 154 $options['status'] = array_map(function($value) { 155 $v = trim($value); 156 $p = substr($v, 0, 3); 157 $t = array( 158 'ASS' => 'ASSIGNED', 159 'UNC' => 'UNCONFIRMED', 160 'REO' => 'REOPENED', 161 'RES' => 'RESOLVED', 162 'VER' => 'VERIFIED', 163 'CLO' => 'CLOSED', 164 'INV' => 'INVALID', 165 'WON' => 'WONTFIX', 166 'DUP' => 'DUPLICATE', 167 'WOR' => 'WORKSFORME' 168 ); 169 if ( isset($t[$p]) ) { 170 return $t[$p]; 171 } 172 return $v; 173 }, $options['status'] ); 174 175 176 // add fixed params 177 $options['limit'] = 100; 178 $options['include_fields'] = array_merge( $this->includeFields, $this->extrasToIncludeFields($extras)); 179 if ( isset($groupBy) ) { 180 $options['include_fields'][] = $groupBy; 181 } 182 183 184 // run search 185 $response = $this->bugzillaRPC('Bug.search', $options ); 186 $result = in_array('dependencies', $extras) ? $this->fetchDependencies( $response['bugs'] ) : $response['bugs']; 187 188 189 // group by 190 if ( isset($groupBy) ) { 191 usort($result, function ($a, $b) use ($groupBy) { 192 $c = strcmp( $a[$groupBy], $b[$groupBy] ); 193 if ( $c == 0 ) return $a['id'] < $b['id'] ? -1 : 1; 194 return $c; 195 }); 196 } 197 198 199 return $result; 200 } 201 202 203 204 /** 205 * Fetch info about several bugs. 206 * 207 * May throw a BugzillaException, for example if the credentials are invalid. 208 * 209 * @param $ids 210 * @return associative array of bugs, with id as key and an array of bugs 211 */ 212 public function getBugsInfos( $ids, $extras ) { 213 214 // setup fields to include 215 $includeFields = array_merge( $this->includeFields, $this->extrasToIncludeFields($extras) ); 216 217 // normalize param 218 $f_ids = is_array($ids) ? $ids : explode(',', $ids); 219 220 // fetch bug infos 221 try { 222 $response = $this->bugzillaRPC('Bug.get', array('ids' => $f_ids, 'include_fields' => $includeFields)); 223 $result = array(); 224 foreach ($response['bugs'] as $bug) { 225 $result[ $bug['id'] ] = $bug; 226 } 227 228 } catch (BugNumberInvalidException $e) { 229 // a bug does not exist, but we still need info about all the other bugs 230 $result = array(); 231 foreach ($f_ids as $f_id) { 232 try { 233 $response = $this->bugzillaRPC('Bug.get', array('ids' => $f_id, 'include_fields' => $includeFields)); 234 $result["$f_id"] = $response['bugs'][0]; 235 } catch (BugNumberInvalidException $ee) { 236 $result["$f_id"] = array( 'id' => "$f_id", 'error' => $ee->getMessage() ); 237 } 238 } 239 } 240 241 // fetch extra dependency info? 242 return in_array('dependencies', $extras) ? $this->fetchDependencies( $result ) : $result; 243 } 244 245 246 private function extrasToIncludeFields($extras) { 247 $fields = array(); 248 foreach ($extras as $e) { 249 if ( $e == 'dependencies' ) { 250 $fields[] = 'depends_on'; 251 $fields[] = 'blocks'; 252 } else if ( $e == 'assigned_to' ) { 253 $fields[] = 'assigned_to'; 254 } else if ( $e == 'lastchange' ) { 255 $fields[] = 'last_change_time'; 256 } else if ( $e == 'deadline' ) { 257 $fields[] = 'deadline'; 258 } else if ( $e == 'status' ) { 259 $fields[] = 'status'; 260 $fields[] = 'resolution'; 261 } else if ( $e == 'version' ) { 262 $fields[] = 'version'; 263 } else if ( $e == 'priority' ) { 264 $fields[] = 'priority'; 265 } else if ( $e == 'severity' ) { 266 $fields[] = 'severity'; 267 } else if ( $e == 'time' ) { 268 $fields[] = 'estimated_time'; 269 $fields[] = 'remaining_time'; 270 $fields[] = 'actual_time'; 271 } else if ( $e == 'classification' ) { 272 $fields[] = 'classification'; 273 } else if ( $e == 'product' ) { 274 $fields[] = 'product'; 275 } else if ( $e == 'component' ) { 276 $fields[] = 'component'; 277 } 278 } 279 return $fields; 280 } 281 282 283 /** 284 * Takes an array of bugs and adds the properties "depends_on_resolved" and "blocks_resolved". 285 * 286 * @param array $bugs - associative array with bug id as key 287 */ 288 protected function fetchDependencies( &$bugs ) { 289 290 // collect all dependencies, blocks and depends_on 291 $dependencies = array(); 292 foreach ($bugs as $bug) { 293 if (count($bug['blocks']) > 0) { 294 $dependencies = array_merge($dependencies, $bug['blocks']); 295 } 296 if (count($bug['depends_on']) > 0) { 297 $dependencies = array_merge($dependencies, $bug['depends_on']); 298 } 299 } 300 301 // fetch status of all dependencies 302 $response = $this->bugzillaRPC('Bug.get', array('ids' => $dependencies, 'include_fields' => array('id', 'status'))); 303 304 // collect resolved dependencies 305 $resolved_dependencies = array(); 306 foreach ($response['bugs'] as $bug) { 307 if ( $bug['status'] == 'RESOLVED' ) { 308 $resolved_dependencies[] = $bug['id']; 309 } 310 } 311 312 // add properties 313 foreach ($bugs as &$bug) { 314 if ( isset($bug['depends_on']) ) { 315 $bug['depends_on_resolved'] = array(); 316 foreach ( $bug['depends_on'] as $id ) { 317 if ( in_array($id, $resolved_dependencies) ) $bug['depends_on_resolved'][] = $id; 318 } 319 } 320 if ( isset($bug['blocks']) ) { 321 $bug['blocks_resolved'] = array(); 322 foreach ( $bug['blocks'] as $id ) { 323 if ( in_array($id, $resolved_dependencies) ) $bug['blocks_resolved'][] = $id; 324 } 325 } 326 } 327 328 return $bugs; 329 } 330 331 332 protected function bugzillaRPC( $method, $parameters ) { 333 334 // prep params 335 $params = array(); 336 if ( !!$this->login && !!$this->password ) { 337 $params["Bugzilla_login"] = $this->login; 338 $params["Bugzilla_password"] = $this->password; 339 } 340 foreach ($parameters as $k => $v) { 341 $params[ $k ] = $v; 342 } 343 344 // make request 345 $context = stream_context_create(array('http' => array( 346 'method' => "POST", 347 'header' => array("Content-Type: text/xml"), 348 'content' => $this->xmlrpc_encode_request($method, $params) 349 ))); 350 $response = @file_get_contents($this->getConf('bugzilla_baseurl').'/xmlrpc.cgi', false, $context); 351 352 // check response and parse result 353 if ( $response === false ) { 354 $err = error_get_last(); 355 throw new Exception($err['message']); 356 } 357 $result = $this->xmlrpc_decode( $response ); 358 359 // check result for errors 360 if ( $this->xmlrpc_is_fault($result) ) { 361 if ( $result['faultCode'] == 101) { 362 throw new BugNumberInvalidException($result['faultString'], $result['faultCode']); 363 } else { 364 throw new BugzillaException($result['faultString'], $result['faultCode']); 365 } 366 } else if ( count( $result['faults'] ) > 0 ) { 367 throw new BugzillaException("Error: " . print_r($result['faults']) ); 368 } 369 370 // return result 371 return $result; 372 } 373 374 375 376 protected function xmlrpc_encode_request($method, $params) { 377 $x = '<?xml version="1.0" encoding="iso-8859-1"?>' . "\n"; 378 $x .= '<methodCall>'; 379 $x .= '<methodName>' . htmlspecialchars($method) . '</methodName>'; 380 $x .= '<params><param><value><struct>'; 381 foreach ( $params as $k => $v ) { 382 $x .= '<member>'; 383 $x .= '<name>' . htmlspecialchars( $k ) . '</name>'; 384 $x .= '<value>'; 385 if ( is_array($v) ) { 386 $x .= '<array><data>'; 387 foreach ( $v as $i ) { 388 $x .= '<value>'; 389 $x .= '<string>' . htmlspecialchars( $i ) . '</string>'; 390 $x .= '</value>'; 391 } 392 $x .= '</data></array>'; 393 } else { 394 $x .= '<string>' . htmlspecialchars( $v ) . '</string>'; 395 } 396 $x .= '</value>'; 397 $x .= '</member>'; 398 } 399 $x .= '</struct></value></param></params>'; 400 $x .= '</methodCall>'; 401 return $x; 402 } 403 404 405 protected function xmlrpc_is_fault($result) { 406 return isset($result['faultString']) && isset($result['faultCode']); 407 } 408 409 410 protected function xmlrpc_decode($text) { 411 $x = simplexml_load_string($text); 412 if ( $x->fault->value->struct ) { 413 return $this->xmlrpc_decode_node( $x->fault->value->struct ); 414 } 415 return $this->xmlrpc_decode_node( $x->params->param->value->struct ); 416 } 417 418 419 private function xmlrpc_decode_node($node) { 420 if ( $node->getName() == 'int' ) { 421 return (int) $node->__toString(); 422 } 423 else if ( $node->getName() == 'string' ) { 424 return $node->__toString(); 425 } 426 else if ( $node->getName() == 'array' ) { 427 $a = array(); 428 foreach ( $node->data->value as $i ) { 429 $a[] = $this->xmlrpc_decode_node( $i->children()[0] ); 430 } 431 return $a; 432 } 433 else if ( $node->getName() == 'struct' ) { 434 $a = array(); 435 foreach ( $node->member as $i ) { 436 $k = $i->name->__toString(); 437 $v = $this->xmlrpc_decode_node( $i->value->children()[0] ); 438 $a[ $k ] = $v; 439 } 440 return $a; 441 } 442 } 443 444 445}