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}