| <?php |
| /** |
| * ***************************************************************************** |
| * Copyright (c) 2013, 2014 Eclipse Foundation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Wayne Beaton (Eclipse Foundation)- initial API and implementation |
| * ***************************************************************************** |
| */ |
| |
| class BugzillaClient { |
| var $host; |
| var $cookieFile; |
| var $loggedIn = false; |
| |
| /** |
| * This is the pattern that we look for to verify that the |
| * attempt to login as succeeded. |
| * Since we're parsing web pages, |
| * we look for an occurance of "Log out" (assuming that we can |
| * only log out if we have previously logged in). |
| * |
| * @var string |
| */ |
| var $loggedInPattern = '/<a href="index.cgi\?logout=1">Log( | )out<\/a>/'; |
| var $bugCreatedPattern = '/(\d+)+ Submitted/'; |
| var $attachmentCreatedPattern = '/Attachment (\d+) added/'; |
| |
| function __construct($host) { |
| $this->host = $host; |
| $this->cookieFile = tempnam(sys_get_temp_dir(), 'bugzilla'); |
| if (!$this->cookieFile) |
| throw new Exception("The BugzillaClient cannot allocate a cookie file. Check temp directory settings."); |
| } |
| |
| function isLoggedIn() { |
| return $this->loggedIn; |
| } |
| |
| function login($user, $password) { |
| if ($this->isLoggedIn()) |
| return; |
| |
| $query = new BugzillaPostQuery($this, "index.cgi"); |
| $result = $query->setData('Bugzilla_login', $user)->setData('Bugzilla_password', $password)->setData('GoAheadAndLogIn', 'Login')->execute(); |
| |
| /* |
| * If the value that we get back from the server contains the |
| * "loggedInPattern", then we were successful. Otherwise, obviously, |
| * we were not successful. In this event, extract the error message |
| * from the title and pass it back as the message in an exception. |
| */ |
| if (!preg_match($this->loggedInPattern, $result->contents)) { |
| if (preg_match('/<title>(.*)<\/title>/', $result->contents, $matches)) { |
| $message = $matches[1]; |
| } |
| else |
| $message = 'Unknown error'; |
| throw new BugzillaInvalidCredentialsException($message); |
| } |
| |
| $this->loggedIn = true; |
| |
| return $this; |
| } |
| |
| function logout() { |
| // TODO Should we throw an exception? |
| if (!$this->isLoggedIn()) |
| return; |
| |
| $query = new BugzillaPostQuery($this, "index.cgi"); |
| $query->setData('logout', '1')->execute(); |
| |
| $this->loggedIn = false; |
| } |
| |
| /** |
| * |
| * @param string $product |
| * @param string $component |
| * @param string $summary |
| * @return BugBuilder |
| */ |
| function startBug($product, $component, $summary) { |
| return new BugBuilder($this, $product, $component, $summary); |
| } |
| |
| function findBugs() { |
| return new BugFinder($this); |
| } |
| |
| /** |
| * Answers a representation of the bug. |
| * Note that no check is done |
| * to ensure that this bug actually exists. |
| * |
| * @param int $id |
| * The bug id. |
| * @return Bug |
| */ |
| function getBug($id) { |
| return new Bug($this, $id); |
| } |
| |
| } |
| |
| class BugzillaInvalidCredentialsException extends Exception { |
| |
| } |
| |
| /** |
| * Instances of BugBuilder are used--oddly enough--to build bugs. |
| * You |
| * should never directly create an instance of this class. Instead, send |
| * the startAttachment() message to a Bug instance. |
| * |
| * All of the methods implemented by this class return the receiver. |
| * This lets you chain multiple calls against the instance using relatively |
| * compact and easy-to-read code (that's the intention, anyway). |
| * |
| * The final message sent is generally create() which does the actual work |
| * based on the information that's been provided. |
| * |
| * @internal |
| * |
| * @see Bug::startAttachment |
| */ |
| class BugBuilder { |
| var $client; |
| var $product; |
| var $component; |
| var $summary; |
| var $body = array(); |
| var $keywords = array(); |
| var $assignee; |
| var $cc = array(); |
| var $operatingSystem = 'All'; |
| var $platform = 'All'; |
| var $severity = 'normal'; |
| var $priority = 'P3'; |
| var $version = 'unspecified'; |
| var $url; |
| var $otherFields = array(); |
| |
| function __construct($client, $product, $component, $summary) { |
| $this->client = $client; |
| $this->product = $product; |
| $this->component = $component; |
| $this->summary = $summary; |
| } |
| |
| /** |
| * Add a paragraph to comment #0 of the new bug. |
| * Each paragraph |
| * is separated from the previous paragraph by a blank line. |
| * The second (optional) parameter is a boolean value, the paragraph |
| * is only added if that parameter's value is true. This provides |
| * a relatively convenient way to build up the content without |
| * having to include a whole bunch of if statements to chop up |
| * an otherwise very tidy code flow. |
| * |
| * Like every other method on this class, the return value is the |
| * receiver, so that additional messages can be sent. |
| * |
| * @param string $paragraph |
| * @param boolean $include |
| * @return AttachmentBuilder |
| */ |
| function addParagraph($paragraph, $include = true) { |
| if ($include) |
| $this->body[] = $paragraph; |
| return $this; |
| } |
| |
| function addKeyword($keyword) { |
| $this->keywords[] = $keyword; |
| return $this; |
| } |
| |
| function setAssignee($email) { |
| $this->assignee = $email; |
| return $this; |
| } |
| |
| function addCC($email) { |
| $this->cc[] = $email; |
| return $this; |
| } |
| |
| function setOperatingSystem($os) { |
| $this->operatingSystem = $os; |
| return $this; |
| } |
| |
| function setPlatform($platform) { |
| $this->platform = $platform; |
| return $this; |
| } |
| |
| function setSeverity($severity) { |
| $this->severity = $severity; |
| return $this; |
| } |
| |
| function setPriority($priority) { |
| $this->priority = $priority; |
| return $this; |
| } |
| |
| function setVersion($version) { |
| $this->version = $version; |
| return $this; |
| } |
| |
| function setUrl($url) { |
| $this->url = $url; |
| return $this; |
| } |
| |
| function set($field, $value) { |
| $this->otherFields[$field] = $value; |
| return $this; |
| } |
| |
| function create() { |
| // Recent versions of Bugzilla require a token when creating |
| // a bug. Attempt to get the token. |
| // FIXME Only do this if the Bugzilla version requires it. |
| $query = new BugzillaGetQuery($this->client, 'enter_bug.cgi'); |
| $queryFormResult = $query->setData('product', $this->product)->setData('component', $this->component)->execute(); |
| |
| $query = new BugzillaPostQuery($this->client, 'post_bug.cgi'); |
| $query->setData('form_name', 'enter_bug')->setData('token', $queryFormResult->getToken())->setData('product', $this->product)->setData('component', $this->component)->setData('assigned_to', $this->assignee)->setData('cc', join(',', $this->cc))->setData('short_desc', $this->summary)->setData('comment', join("\n\n", $this->body))->setData('op_sys', $this->operatingSystem)->setData('rep_platform', $this->platform)->setData('bug_severity', $this->severity)->setData('priority', $this->priority)->setData('version', $this->version)->setData('keywords', join(',', $this->keywords))->setData('bug_file_loc', $this->url); |
| |
| foreach ($this->otherFields as $key => $value) { |
| $query->setData($key, $value); |
| } |
| |
| $result = $query->execute(); |
| |
| if (preg_match($this->client->bugCreatedPattern, $result->contents, $matches)) { |
| return new Bug($this->client, $matches[1]); |
| } |
| |
| if (preg_match('/<title>(.*)<\/title>/ims', $result->contents, $matches)) { |
| $message = preg_replace('/\s+/', ' ', $matches[1]); |
| } |
| else |
| $message = 'Unknown error'; |
| |
| // TODO Make custom exception |
| throw new Exception($message); |
| } |
| |
| /** |
| * Answer a URL that will open a browser with a pre-populated |
| * create page. |
| */ |
| function getUrl() { |
| $parameters = array( |
| 'product' => $this->product, |
| 'component' => $this->component, |
| 'assigned_to' => $this->assignee, |
| 'cc' => join(',', $this->cc), |
| 'short_desc' => $this->summary, |
| 'comment' => join("\n\n", $this->body), |
| 'op_sys' => $this->operatingSystem, |
| 'rep_platform' => $this->platform, |
| 'bug_severity' => $this->severity, |
| 'priority' => $this->priority, |
| 'version' => $this->version, |
| 'keywords' => join(',', $this->keywords), |
| 'bug_file_loc' => $this->url |
| ); |
| |
| $url = $this->client->host . '/enter_bug.cgi?'; |
| foreach ($parameters as $key => $value) { |
| if ($value) |
| $url .= "$key=" . urlencode($value) . '&'; |
| } |
| return $url; |
| } |
| |
| } |
| |
| class BugFinder { |
| var $client; |
| var $parameters = array( |
| 'columnlist' => array( |
| 'bug_id' |
| ) |
| ); |
| |
| function __construct(BugzillaClient $client) { |
| $this->client = $client; |
| } |
| |
| function setProduct($product) { |
| $this->parameters['product'] = array( |
| $product |
| ); |
| return $this; |
| } |
| |
| function setComponent($component) { |
| $this->parameters['component'] = array( |
| $component |
| ); |
| return $this; |
| } |
| |
| function addColumn($column) { |
| $this->parameters['columnlist'][] = $column; |
| return $this; |
| } |
| |
| function setSummary($summary) { |
| $this->parameters['short_desc'] = array( |
| $summary |
| ); |
| $this->parameters['short_desc_type'] = array( |
| 'substring' |
| ); |
| return $this; |
| } |
| |
| function addKeyword($keyword) { |
| $this->parameters['keywords'][] = $keyword; |
| return $this; |
| } |
| |
| function addStatus($status) { |
| $this->parameters['bug_status'][] = $status; |
| return $this; |
| } |
| |
| /** |
| * Execute the search. |
| * The returned value is an array of |
| * arrays. |
| * |
| * e.g. array(array('bug_id' => 1234, 'short_desc' => 'Something', ...), ...) |
| * |
| * @see searchBugs |
| * @return mixed[][] |
| */ |
| function search() { |
| $query = new BugzillaGetQuery($this->client, 'buglist.cgi'); |
| foreach ($this->parameters as $key => $values) { |
| $query->setData($key, join(',', $values)); |
| } |
| |
| $result = $query->setData('ctype', 'csv')->execute(); |
| return $result->getCsv(); |
| } |
| |
| /** |
| * Execute the search but return a collection of instances of the |
| * bug class. |
| * |
| * @return Bug[] |
| */ |
| function searchBugs() { |
| $bugs = array(); |
| foreach ($this->search() as $row) { |
| $bugs[] = new Bug($this->client, $row['bug_id']); |
| } |
| return $bugs; |
| } |
| |
| } |
| |
| /** |
| * Instances of AttachmentBuilder are used--oddly enough--to build attachments. |
| * You |
| * should never directly create an instance of this class. Instead, send |
| * the startBug() message to a BugzillaClient instance. |
| * |
| * All of the methods implemented by this class return the receiver. |
| * This lets you chain multiple calls against the instance using relatively |
| * compact and easy-to-read code (that's the intention, anyway). |
| * |
| * The final message sent is generally create() which does the actual work |
| * based on the information that's been provided. |
| * |
| * @internal |
| * |
| * @see BugzillaClient::startBug |
| */ |
| class AttachmentBuilder { |
| var $bug; |
| var $file; |
| var $description = 'A file'; |
| var $body = array(); |
| |
| function __construct($bug) { |
| $this->bug = $bug; |
| } |
| |
| function setFile($file) { |
| $this->file = $file; |
| return $this; |
| } |
| |
| function setDescription($description) { |
| $this->description = $description; |
| return $this; |
| } |
| |
| /** |
| * Add a paragraph to the comment for the attachment. |
| * Each paragraph |
| * is separated from the previous paragraph by a blank line. |
| * The second (optional) parameter is a boolean value, the paragraph |
| * is only added if that parameter's value is true. This provides |
| * a relatively convenient way to build up the content without |
| * having to include a whole bunch of if statements to chop up |
| * an otherwise very tidy code flow. |
| * |
| * Like every other method on this class, the return value is the |
| * receiver, so that additional messages can be sent. |
| * |
| * @param string $paragraph |
| * @param boolean $include |
| * @return AttachmentBuilder |
| */ |
| function addParagraph($paragraph, $include = true) { |
| if ($include) |
| $this->body[] = $paragraph; |
| return $this; |
| } |
| |
| function create() { |
| $query = new BugzillaGetQuery($this->bug->client, 'attachment.cgi'); |
| $queryFormResult = $query->setData('bugid', $this->bug->id)->setData('action', 'enter')->execute(); |
| |
| $query = new BugzillaPostQuery($this->bug->client, 'attachment.cgi'); |
| $result = $query->setData('form_name', 'enter_bug')->setData('token', $queryFormResult->getToken())->setData('bugid', $this->bug->id)->setData('action', 'insert')->setData('data', "@$this->file")->setData('description', $this->description)->setData('comment', join("\n\n", $this->body))->setData('contenttypemethod', 'list')->setData('contenttypeselection', 'text/html')->execute(); |
| |
| if (preg_match($this->bug->client->attachmentCreatedPattern, $result->contents, $matches)) { |
| return new Attachment($this->bug, $matches[1]); |
| } |
| |
| if (preg_match('/<title>(.*)<\/title>/ims', $result->contents, $matches)) { |
| $message = preg_replace('/\s+/', ' ', $matches[1]); |
| } |
| else |
| $message = 'Unknown error'; |
| |
| // TODO Make custom exception |
| throw new Exception($message); |
| } |
| |
| } |
| |
| /** |
| * Instances of this class represent a bug instance from the client. |
| * Instances |
| * should not be directly created, but should instead be created by sending |
| * a message to an instance of the BugzillaClient class. |
| * |
| * @internal |
| * |
| * @see BugzillaClient::getBug |
| * @see BugzillaClient::startBug |
| */ |
| class Bug { |
| var $client; |
| var $id; |
| |
| function __construct($client, $id) { |
| $this->client = $client; |
| $this->id = $id; |
| } |
| |
| function getId() { |
| return $this->id; |
| } |
| |
| function startAttachment() { |
| return new AttachmentBuilder($this); |
| } |
| |
| function getUrl() { |
| return $this->client->host . '/show_bug.cgi?id=' . $this->id; |
| } |
| |
| } |
| |
| /** |
| * Instances of this class represent an attachment instance from the client. |
| * Instances should not be directly created, but should instead be created by |
| * sending a message to an instance of the Bug class. |
| * |
| * @internal |
| * |
| * @see Bug::startAttachment |
| */ |
| class Attachment { |
| var $bug; |
| var $id; |
| |
| function __construct($bug, $id) { |
| $this->bug = $bug; |
| $this->id = $id; |
| } |
| |
| } |
| |
| /** |
| * Instances of this class are used to build and execute a POST query |
| * against Bugzilla. |
| * |
| * @internal |
| * |
| */ |
| class BugzillaPostQuery { |
| var $client; |
| var $resource; |
| var $parameters = array(); |
| |
| function __construct($client, $resource) { |
| $this->client = $client; |
| $this->resource = $resource; |
| } |
| |
| function setData($key, $value) { |
| $this->parameters[$key] = $value; |
| return $this; |
| } |
| |
| function getUrl() { |
| return $this->client->host . '/' . $this->resource; |
| } |
| |
| function getDefaultOptions() { |
| return array( |
| CURLOPT_USERAGENT => "Eclipse", |
| CURLOPT_COOKIEFILE => $this->client->cookieFile, |
| CURLOPT_COOKIEJAR => $this->client->cookieFile, |
| // CURLOPT_VERBOSE => 1, |
| // CURLOPT_FOLLOWLOCATION => true, |
| CURLOPT_RETURNTRANSFER => 1, |
| CURLOPT_URL => $this->getUrl() |
| ); |
| } |
| |
| function getOptions() { |
| $options = $this->getDefaultOptions(); |
| $options[CURLOPT_POST] = TRUE; |
| $options[CURLOPT_POSTFIELDS] = $this->parameters; |
| return $options; |
| } |
| |
| function execute() { |
| $curl = curl_init($this->client->host); |
| curl_setopt_array($curl, $this->getOptions()); |
| $contents = curl_exec($curl); |
| curl_close($curl); |
| return new QueryResult($contents); |
| } |
| |
| } |
| |
| class QueryResult { |
| var $contents; |
| |
| function __construct($contents) { |
| $this->contents = $contents; |
| } |
| |
| function getToken() { |
| if (preg_match('/<input type="hidden" name="token" value="(\w+)">/', $this->contents, $matches)) |
| return $matches[1]; |
| return null; |
| } |
| |
| public function getCsv() { |
| // FIXME switch to str_getcsv when we upgrade to PHP 5.3+ |
| // TODO Make sure that the data is indeed CSV data. |
| |
| // We need to support pre-5.3 versions of PHP which do not include |
| // the str_getcsv function. Use some string-to-stream cleverness in |
| // its place. |
| // |
| // If fopen fails because, for example, PHP is configured for it |
| // to fail (via php.ini), then we brute force it with a regular |
| // expression. |
| if ($stream = fopen('data://text/plain,' . $this->contents, 'r')) { |
| $header = fgetcsv($stream); |
| $rows = array(); |
| while ($values = fgetcsv($stream)) { |
| $row = array(); |
| for ($index = 0; $index < count($header); $index++) |
| $row[$header[$index]] = $values[$index]; |
| $rows[] = $row; |
| } |
| fclose($stream); |
| return $rows; |
| } |
| else { |
| $lines = explode("\n", $this->contents); |
| $header = $this->sgetcsv(current($lines)); |
| $rows = array(); |
| while ($line = next($lines)) { |
| $values = $this->sgetcsv($line); |
| $row = array(); |
| for ($index = 0; $index < count($header); $index++) |
| $row[$header[$index]] = $values[$index]; |
| $rows[] = $row; |
| } |
| return $rows; |
| } |
| } |
| |
| /** |
| * This is a quick and dirty implementation of a string CSV parser. |
| * I'm sure that we can come up with a more clever regex, but this |
| * one will do. |
| * |
| * @internal |
| * |
| * @param unknown $input |
| * @return string[] |
| */ |
| private function sgetcsv($input) { |
| $values = array(); |
| if (preg_match_all('/(?:^|,)((?!")[^,|$]*|"(?:[^"\\\\]|\\\\.)*")/', $input, $matches)) { |
| foreach ($matches[1] as $match) { |
| if (preg_match('/^"(.*)"$/', $match, $contents)) { |
| $match = stripslashes($contents[1]); |
| } |
| $values[] = $match; |
| } |
| } |
| return $values; |
| } |
| |
| } |
| |
| class BugzillaGetQuery extends BugzillaPostQuery { |
| |
| function getUrl() { |
| return parent::getUrl() . '?' . $this->getParameters(); |
| } |
| |
| function getOptions() { |
| $options = $this->getDefaultOptions(); |
| $options[CURLOPT_HTTPGET] = TRUE; |
| return $options; |
| } |
| |
| function getParameters() { |
| $parameters = array(); |
| foreach ($this->parameters as $key => $value) |
| $parameters[] = "$key=" . urlencode($value); |
| return join('&', $parameters); |
| } |
| |
| } |