WIP
diff --git a/classes/BugzillaClient.class.inc b/classes/BugzillaClient.class.inc
index ddc5d95..d3934da 100644
--- a/classes/BugzillaClient.class.inc
+++ b/classes/BugzillaClient.class.inc
@@ -1,5 +1,6 @@
<?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
@@ -7,304 +8,658 @@
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
- * Wayne Beaton (Eclipse Foundation)- initial API and implementation
- *******************************************************************************/
+ * 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 = '/Bug (\d+)+ Submitted/';
- var $attachmentCreatedPattern = '/Attachment (\d+) added to Bug (\d+)/';
-
- function __construct($host) {
- $this->host = $host;
- $this->cookieFile = tempnam(null, 'bugzilla');
- }
-
- 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', '1')
- ->execute();
+ var $host;
+ var $cookieFile;
+ var $loggedIn = false;
- /*
- * 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;
- }
-
- function startBug($product, $component, $summary) {
- return new BugBuilder($this, $product, $component, $summary);
- }
+ /**
+ * 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 {}
+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 $cc = array();
-
- function __construct($client, $product, $component, $summary) {
- $this->client = $client;
- $this->product = $product;
- $this->component = $component;
- $this->summary = $summary;
- }
-
- function addParagraph($paragraph) {
- $this->body[] = $paragraph;
- return $this;
- }
-
- function addCC($email) {
- $this->cc[] = $email;
- 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->execute();
-
- $query = new BugzillaPostQuery($this->client, 'post_bug.cgi');
- $result = $query
- ->setData('form_name', 'enter_bug')
- ->setData('token', $queryFormResult->getToken())
- ->setData('product', $this->product)
- ->setData('component', $this->component)
- ->setData('cc', join(',', $this->cc))
- ->setData('short_desc', $this->summary)
- ->setData('comment', join('\n\n', $this->body))
- ->setData('op_sys', 'All')
- ->setData('rep_platform', 'All')
- ->setData('bug_severity', 'normal')
- ->setData('priority', 'Normal')
- ->setData('version', 'unspecified')
- ->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);
- }
+ 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';
-
- function __construct($bug) {
- $this->bug = $bug;
- }
-
- function setFile($file) {
- $this->file = $file;
- return $this;
- }
-
- function setDescription($description) {
- $this->description = $description;
- return $this;
+ 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]);
}
- 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('contenttypemethod', 'autodetect')
- ->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);
- }
+ 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);
- }
+ 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;
- }
+ 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();
+ var $client;
+ var $resource;
+ var $parameters = array();
- function __construct($client, $resource) {
- $this->client = $client;
- $this->resource = $resource;
- }
+ function __construct($client, $resource) {
+ $this->client = $client;
+ $this->resource = $resource;
+ }
- function setData($key, $value) {
- $this->parameters[$key] = $value;
- return $this;
- }
+ 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_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);
- }
-
+ 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;
- }
+ var $contents;
- function getToken() {
- if (preg_match('/<input type="hidden" name="token" value="(\w+)">/', $this->contents, $matches))
- return $matches[1];
- return null;
+ 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 getUrl() {
+ return parent::getUrl() . '?' . $this->getParameters();
+ }
- function getParameters() {
- $parameters = array();
- foreach($this->parameters as $key => $value) $parameters[] = "$key=$value";
- return join('&', $parameters);
- }
-}
+ function getOptions() {
+ $options = $this->getDefaultOptions();
+ $options[CURLOPT_HTTPGET] = TRUE;
+ return $options;
+ }
-?>
\ No newline at end of file
+ function getParameters() {
+ $parameters = array();
+ foreach ($this->parameters as $key => $value)
+ $parameters[] = "$key=" . urlencode($value);
+ return join('&', $parameters);
+ }
+
+}
\ No newline at end of file
diff --git a/classes/Release.class.inc b/classes/Release.class.inc
index 5aef9ff..ed69e8b 100755
--- a/classes/Release.class.inc
+++ b/classes/Release.class.inc
@@ -1,7 +1,7 @@
<?php
/**
* *****************************************************************************
- * Copyright (c) 2017 Eclipse Foundation and others.
+ * Copyright (c) 2018 Eclipse Foundation and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -10,7 +10,11 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
+/*
+ * FIXME Experimental.
+ */
require_once dirname ( __FILE__ ) . '/../classes/Project.class.php';
+require_once dirname ( __FILE__ ) . '/../classes/BugzillaClient.class.inc';
require_once dirname ( __FILE__ ) . '/../classes/debug.php';
require_once dirname ( __FILE__ ) . '/../classes/database.inc';
@@ -20,30 +24,33 @@
var $date;
/**
- * This method answers an array containing all of
- * @param unknown $root
- * @param unknown $date
- * @return Release
+ * This method answers an array containing all of releases that
+ * either have occurred or are planned to occur after a particular
+ * date. The method has two optional parameters. If specified, only
+ * those projects that descend from a particular root are included.
+ * If no date (UNIX time) is specified, then the current date is used.
+ *
+ * @param string $root the root project
+ * @param int $date the data in UNIX time
+ * @return Release[]
*/
- public static function releasesSince($root = null, $date = null) {
- $releases = array ();
+ public static function releasesAfter($root = null, $date = null) {
$sql = "
select
project as id, name, date
from ProjectReleases
- where id regexp :pattern
- and date > date(:date)
+ where project regexp ':pattern'
+ and date > date(':date')
order by date";
$args = array(
':pattern' => $root ? "^${root}(?:\..*)?$" : '',
':date' => date('Y-m-d', $date ? $date : time())
);
- return new DatabaseQuery('dashboard', $sql, $args, function ($row) {
+ return new DatabaseQuery('dashboard', $sql, $args, function ($row) use (&$releases) {
$project = Project::getProject ( $row ['id'] );
$name = $row ['name'];
$date = strtotime ( $row ['date'] );
- $reviewDate = self::getProbableReviewDate ( $date );
return new Release ( $project, $name, $date );
} );
@@ -55,22 +62,61 @@
$this->date = $date;
}
- function getId() {
+ public function getId() {
return $this->project->getId ();
}
- function getUrl() {
+ public function getUrl() {
return $this->project->getUrl ();
}
- function getName() {
+ public function getName() {
return $this->project->getName () . ' ' . $this->name;
}
- function getDate() {
+ public function getDate() {
return $this->date;
}
+ public function getVersion() {
+ return $this->makeThreePartVersionNumber($this->name);
+ }
+
+ public function getTrackingBugUrl() {
+ $bugzilla = new BugzillaClient('https://bugs.eclipse.org/bugs');
+ $bugs = $bugzilla->findBugs()
+ ->setProduct('Community')
+ ->setComponent('Proposals and Reviews')
+ ->setSummary($this->getTrackingBugSummary())
+ ->searchBugs();
+
+ foreach ($bugs as $bug) {
+ return $bug->getUrl();
+ }
+
+ return null;
+ }
+
+ private function getTrackingBugSummary() {
+ return "[release] {$this->project->getId()} {$this->getVersion()}";
+ }
+
+ public static function makeThreePartVersionNumber($value) {
+ if (preg_match('/(\d+)(\.\d+)?(\.\d+)?/', $value, $matches)) {
+ $name = $matches[1];
+ $name .= isset($matches[2]) && $matches[2] ? $matches[2] : '.0';
+ $name .= isset($matches[3]) && $matches[3] ? $matches[3] : '.0';
+ return $name;
+ }
+ else
+ if (preg_match('/(\d+(\.\d+){1,2})/', $value)) {
+ return $match[1];
+ }
+ else {
+ return null;
+ }
+ }
+
private static function getProbableReviewDate($date) {
$diff = date ( 'w', $date ) - 3; // '3' means Wednesday
if ($diff < 0)
@@ -105,40 +151,4 @@
return true; // third week
return false;
}
-}
-
-
-
-function dumpFutureReleases() {
- foreach(getFutureReleases() as $date => $releases) {
- echo "<h3>" . ($date == 0 ? "It's already too late" : ("For review on " . date('Y-m-d', $date))) . "</h3>";
- echo "<ul>";
- foreach($releases as $release) {
- echo "<li><a href=\"{$release->getUrl()}\">{$release->getName()}</a> ";
- echo date('Y-m-d', $release->getDate());
- dumpOpenCQs($release);
- echo "</li>";
- }
- echo "</ul>";
- }
-}
-
-function dumpOpenCQs(Release $release) {
- echo "<ul>";
- $sql = '
- select
- b.bug_id as id, b.short_desc as title
- from bugs as b
- join components as c on b.component_id=c.id
- where c.name=\'$id\'
- and bug_status in (\'NEW\', \'REOPENED\')';
-
- query ('ipzilla', $sql, array('$id' => $release->getId()), function ($row) {
- $id = $row ['id'];
- $title = $row ['title'];
- echo "<li><a target=_blank href=\"https://dev.eclipse.org/ipzilla/show_bug.cgi?id=$id\">$id</a> $title</li>";
- });
- echo "</ul>";
-}
-
-
+}
\ No newline at end of file
diff --git a/tools/pmc.php b/tools/pmc.php
index d1ae379..098f4b7 100644
--- a/tools/pmc.php
+++ b/tools/pmc.php
@@ -8,12 +8,13 @@
*******************************************************************************/
/**
-
+ * FIXME Experimental. Refactoring required.
*/
require_once($_SERVER['DOCUMENT_ROOT'] . "/eclipse.org-common/system/app.class.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/eclipse.org-common/system/nav.class.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/eclipse.org-common/system/menu.class.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/Project.class.php");
+require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/Release.class.inc");
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/common.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/database.inc");
@@ -22,13 +23,49 @@
$Menu = new Menu();
include($App->getProjectCommon());
+callIfNotCommitter('exit');
+
$pageTitle = "Eclipse Foundation PMC Tasks";
$pageKeywords = "";
$pageAuthor = "Wayne Beaton";
-$root = Project::getProject($_GET['id']);
-if ($root->isTopLevel()) {
+/**
+ * This function renders a project selection drop-down.
+ * When the
+ * user selects a value, the page is reloaded with the id of the
+ * selected project in the <code>id</code> parameter. All other
+ * parameters that had been passed to the page are preserved via
+ * hidden fields.
+ *
+ * @param string $id
+ * The id of the selected project
+ */
+function dumpProjectSelectionForm($id) {
+ echo "<form method=\"get\">";
+ foreach ( $_GET as $key => $value ) {
+ if ($key == 'id')
+ continue;
+ echo "<input type=\"hidden\" name=\"$key\" value=\"$value\"";
+ }
+ echo "<p>Select a project: ";
+ echo "<select name=\"id\" onchange=\"this.form.submit()\">";
+ foreach ( Project::getTopLevelProjects() as $project ) {
+ $selected = $id == $project->getId () ? ' selected="true"' : '';
+ echo "<option value=\"{$project->getId()}\"$selected>{$project->getFormalName()}</option>";
+ }
+
+ echo "</select>";
+ echo "<input type=\"submit\" value=\"Go!\"/>";
+ echo "</p>";
+ echo "</form>";
+}
+
+$id = $_GET['id'];
+$root = Project::getProject($id);
+if ($root == null || !$root->isTopLevel()) {
+ $id = null;
+ $root = null;
}
function withCqsAwaitingPMC($id, $callable) {
@@ -45,15 +82,47 @@
from bugs as b
join components as c on b.component_id = c.id
join products as p on b.product_id = p.id
- where p.name='{$id}'
+ where p.name=':id'
and bug_status='NEW'
and bug_severity='awaiting_pmc'
order by c.name
";
- query('ipzilla', $sql, array(), $callable);
+ query('ipzilla', $sql, array(':id' => $id), $callable);
}
+function dumpFutureReleases($id) {
+ echo "<ul>";
+ Release::releasesAfter($id)->withEach(function(Release $release) {
+ echo "<li><a href=\"{$release->getUrl()}\">{$release->getName()}</a> ";
+ echo date('Y-m-d', $release->getDate());
+ if ($url = $release->getTrackingBugUrl()) {
+ echo "<a href=\"{$url}\">[track]</a>";
+ }
+ // dumpOpenCQs($release);
+ echo "</li>";
+ });
+ echo "</ul>";
+}
+
+function dumpOpenCQs(Release $release) {
+ echo "<ul>";
+ $sql = '
+ select
+ b.bug_id as id, b.short_desc as title
+ from bugs as b
+ join components as c on b.component_id=c.id
+ where c.name=\'$id\'
+ and bug_status in (\'NEW\', \'REOPENED\')';
+
+ query ('ipzilla', $sql, array('$id' => $release->getId()), function ($row) {
+ $id = $row ['id'];
+ $title = $row ['title'];
+ echo "<li><a target=_blank href=\"https://dev.eclipse.org/ipzilla/show_bug.cgi?id=$id\">$id</a> $title</li>";
+ });
+ echo "</ul>";
+}
+
ob_start();
?>
<div id="maincontent">
@@ -62,6 +131,10 @@
<p><strong>Experimental</strong> Provide a list of links to tasks that require the PMC's attention.</p>
+<?php dumpProjectSelectionForm($id) ?>
+
+<?php if ($id) { ?>
+<h2><?php print $root->getFormalName(); ?></h2>
<h3>Intellectual Property</h3>
<?php
@@ -83,8 +156,20 @@
}
print "</ul>";
}
+} else {
+ print "<p>There are no CQs that require PMC attention at this time.</p>";
}
+
?>
+<h3>Upcoming Releases</h3>
+
+<?php
+
+dumpFutureReleases($id);
+
+?>
+
+<?php } ?>
</div>
</div>