blob: 12b5251b46cf4f30b0397267a02c718670486612 [file]
<?php
/*******************************************************************************
* Copyright (c) 2010, 2017 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
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
require_once (dirname(__FILE__) . "/Forge.class.inc");
require_once (dirname(__FILE__) . "/License.class.inc");
require_once (dirname(__FILE__) . "/database.inc");
require_once (dirname(__FILE__) . "/debug.php");
require_once (dirname(__FILE__) . "/common.php");
define('PROJECT_LIVELINESS_NEVER_ACTIVE', 0);
define('PROJECT_LIVELINESS_ACTIVE', 1);
define('PROJECT_LIVELINESS_STALE', 2);
define('PROJECT_LIVELINESS_INACTIVE', 2);
define('PROJECT_LIVELINESS_DEAD', 4);
/**
* Get a project from the PMI instead of the foundation database.
* Going directly to the PMI is no longer necessary now that the
* implementation has been cleaned up. Now, we can just load the
* project; if information is required from the PMI, it will be
* loaded from there.
*
* @deprecated use Project::getProject()
* @param string $id
* a project id of the form 'technology.egit'
* @return PMIProject
*/
function get_project_from_pmi($id) {
return Project::getProject($id);
}
/**
* Answers an array containing all top-level projects.
* Only
* active projects are included.
*
* @deprecated Use Project::getTopLevelProjects()
* @return Project[]
*/
function getTopLevelProjects() {
PMIProject::preload();
return Project::getTopLevelProjects();
}
/**
* Instances of the subclasses of Project represent a single Eclipse Foundation
* Project.
* Projects may be in any of the forges supported by the Eclipse
* Foundation.
*
* This class has two subclasses. The ProductProxy class is basically the
* entry-point; when you ask for a project, you first get one of these. These
* instances are pretty cheap to make and can answer a lot of questions. When
* requests are made that require more information, that information is
* gathered from the PMI and results in the creation of a PMIProduct instance
* (loading from the PMI is pretty expensive, so we try to avoid it when we
* can).
*
* The entry points are all static methods on this class. Instances should
* never be created using the constructors.
*
* Note that instances are cached as they are loaded; subsequent queries
* will always return the cached instance.
*/
abstract class Project {
static $config;
static $projects = array();
/**
* Initialize the class by loading the configuration from
* projects.ini. The projects.ini file includes information about
* how projects have moved (e.g. 'technology.ecf' became 'rt.ecf'),
* information about Top Level Project specific brands, etc.
*/
static function init() {
self::$config = parse_ini_file(dirname(__FILE__) . '/projects.ini');
}
/**
* Get all projects regardless of whether or not they are active
* or archived. The Foundation database is used to get this information.
* This implementation takes project moves that occurred in the day
* when we duplicated information (e.g. there are records in the
* database for 'technology.ecf' and 'rt.ecf') into consideration.
* A project that has been moved will only be represented once in
* this query.
*
* This move information is represented in the projects.ini file.
*
* Other pseudo projects are also skipped by this query.
*
* @return ProjectProxy[]|mixed[]
*/
public static function getAll() {
// Skip all pseudo projects.
$skip = array("'root'", "'foundation-internal'", "'galileo'");
// Skip all projects that were duplicated in a move operation.
// TODO Consider encapsulating this access of the static field.
foreach(self::$config['move'] as $from => $to) {
$skip[] = "'{$from}'";
}
$skip = implode(',', $skip);
return self::getProjects(array(
"IsComponent = 0",
"(p.ProjectID not in ($skip))",
"(p.ParentProjectID not in ('foundation-internal'))"
));
}
public static function getProject($id) {
if (!isValidProjectId($id))
return null;
$projects = self::getProjects(array(
"(p.ProjectId = '$id')"
));
if ($projects)
return $projects[$id];
return null;
}
public static function getAllProjects($ids) {
$all = array();
foreach($ids as $id) {
if (isValidProjectId($id)) {
$all[] = "'{$id}'";
}
}
$all = implode(',', $all);
return self::getProjects(array(
"(p.ProjectId in ($all))"
));
}
public static function getAllWithShortname($name) {
if (!isValidProjectId($name)) return null;
return self::getProjects(array(
"(p.ProjectId like '%.{$name}')",
"p.IsActive"
));
}
private static function getAlternateForgeIds() {
$forges = array();
foreach (Forge::getForges() as $forge) {
if (!$forge->isEclipseForge())
$forges[] = "'{$forge->getId()}'";
}
return implode(',', $forges);
}
public static function getTopLevelProjects() {
$forges = self::getAlternateForgeIds();
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))");
if ($forges)
$where[] = "((p.ParentProjectID = 'root' and p.ProjectId not in ($forges)) or p.ParentProjectID in ($forges))";
else
$where[] = "p.ParentProjectID = 'root'";
return self::getProjects($where);
}
public static function getActiveProjects() {
$forges = self::getAlternateForgeIds();
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))");
if ($forges)
$where[] = "(p.ProjectId not in ($forges))";
return self::getProjects($where);
}
public static function getProjectsForCommitter($id) {
$forges = self::getAlternateForgeIds();
$where = array(
"(p.ProjectID in (SELECT ProjectID FROM PeopleProjects where PersonId='$id' and Relation='CM' and InactiveDate is null))",
"p.IsActive"
);
if ($forges)
$where[] = "(p.ProjectId not in ($forges))";
return self::getProjects($where);
}
/**
* This function answers an array containing all those projects that
* use the license with the provided id.
*
* @param string $id the license id as expressed in the Foundation DB (e.g. 'EPL-2.0')
* @return Project[]
*/
public static function getProjectsWithLicense($id) {
return self::getProjects(array(
"p.ProjectId in (select ProjectId from ProjectLicenses where LicenseId='{$id}')",
"p.IsActive"
));
}
public static function getSubprojects($id) {
return self::getProjects(array(
"(p.ParentProjectId = '$id')",
"p.IsActive"
));
}
public static function visit($callable, $pre = null, $post = null) {
if ($pre) call_user_func($pre, null);
foreach (self::getTopLevelProjects() as $project) {
$project->visitHierarchy($callable, $pre, $post, 0);
}
if ($post) call_user_func($post, null);
}
private static function getProjects($conditions) {
$where = join(' and ', $conditions);
$sql = "
SELECT distinct
p.ProjectId as id, p.Name as name, p.ParentProjectID as parent, p.ProjectPhase as phase,
p.UrlDownload,
group_concat(distinct l.LicenseId) as licenses,
min(pp.ActiveDate) as provisioned_date,
group_concat(distinct cm.PersonId) as committer_ids,
group_concat(distinct pl.PersonId) as lead_ids
FROM Projects as p
left join ProjectLicenses as l on p.ProjectId=l.ProjectId
left join PeopleProjects as pp on p.ProjectId=pp.ProjectId
left join PeopleProjects as cm on p.ProjectId=cm.ProjectId and cm.Relation='CM' and cm.InactiveDate is null
left join PeopleProjects as pl on p.ProjectId=pl.ProjectId and pl.Relation='PL' and pl.InactiveDate is null
where
$where
group by p.ProjectId";
$projects = array();
query('foundation', $sql, null, function ($row) use (&$projects) {
$id = $row['id'];
if (isset(Project::$projects[$id])) {
$projects[$id] = Project::$projects[$id];
}
else {
$project = new ProjectProxy($id, $row);
Project::$projects[$id] = $project;
$projects[$id] = $project;
}
});
// The configuration defines projects that should be removed
// from query results. Remove those projects.
// TODO Consider encapsulating this access of the static field.
foreach(self::$config['remove'] as $remove) {
unset($projects[$remove]);
}
return $projects;
}
private function __construct() {
}
/**
* The short id is the last segment in the fully-qualified
* id. e.g. the short id of 'technology.egit' is 'egit'.
*
* @return string
*/
public function getShortId() {
preg_match("/([^\.]+)$/", $this->getId(), $matches);
return $matches[1];
}
public function getLocalId() {
return $this->getForge()->getLocalProjectId($this->getId());
}
public function getShortName() {
return $this->getName();
}
public function getChildren() {
return self::getSubprojects($this->getId());
}
public function getParent() {
return self::getProject($this->getParentId());
}
public function isInIncubationPhase() {
return in_array($this->getProjectPhase(), array(
'Incubation',
'Incubation.nc'
));
}
/**
* Components are an old concept.
* Nothing is a component anymore.
*
* @return boolean Always false
*/
public function isComponent() {
return false;
}
public function isArchived() {
return $this->getProjectPhase() == 'Archived';
}
public function isInIncubationConformingPhase() {
return $this->getProjectPhase() == 'Incubation';
}
public function isInIncubationNonConformingPhase() {
return false;
}
public function getUrl() {
$id = $this->getLocalId();
$base = $this->getForge()->getUrl();
return "{$base}/projects/{$id}";
}
/**
* This function answers the URL to use to get more data
* about the project in JSON format, including releases, etc.
*/
public function getDataUrl() {
if (!$forge = Forge::getForgeForProjectId($this->getId())) {
return null;
}
$base = $forge->getUrl();
$local = $forge->getLocalProjectId($this->getId());
return "{$base}/json/project/$local";
}
public function getTopLevelProject() {
if ($this->isTopLevel())
return $this;
if (!$parent = $this->getParent())
return $this;
return $parent->getTopLevelProject();
}
public function isTopLevel() {
if (!$forge = $this->getForge())
return true;
if ($forge->isEclipseForge())
return $this->getParentId() == 'root';
return $this->getParentId() == $forge->getId();
}
public function getForge() {
return Forge::getForgeForProjectId($this->getId());
}
public function visitHierarchy($function, $pre = null, $post = null, $level) {
call_user_func($function, $this, $level);
if ($pre) call_user_func($pre, $this);
foreach ($this->getChildren() as $child) {
$child->visitHierarchy($function, $pre, $post, $level + 1);
}
if ($post) call_user_func($post, $this);
}
/**
* This method answers the formal name of the project. If the
* project's name does not include either 'Eclipse', or the
* brand name associated with its Top Level Project, then the
* brand name is prepended to the project name.
*
* Remove anything that's between parentheses. We actually
* look for a space followed by an open parenthesis, because
* some projects (e.g. "Eclipse e(fx)clipse") include them
* in their proper name (and "Eclipse eclipse" just looks weird).
*
* e.g.
* <li>"EGit" becomes "Eclipse EGit"</li>
* <li>"e(fx)clipse" becomes "Eclipse e(fx)clipse"</li>
* <li>"Eclipse Communication Framework (ECF)" becomes "Eclipse Communication Framework"</li>
* <li>"PolarSys Capella" becomes "PolarSys Capella"</li>
* <li>"Capella" becomes "PolarSys Capella"</li>
*
* @see Project::getNickName()
*
* @return string
*/
public function getFormalName() {
$brand = $this->getBrandName();
$name = trim(preg_replace('/ \([^\)]*\)/', ' ', $this->getName()));
if (!preg_match("/(?:$brand|Eclipse)/", $name)) {
return "$brand $name";
}
return $name;
}
/**
* Sort out the project's brand. If the project name starts with one of our brands,
* as defined in the projects.ini file, then that is the project's brand.
* The project's brand might also be determined by the top-level project
* (a top-level project may have a brand associated with it; in this case,
* all projects that fall under that top-level project use that brand.
*
* @return string
*/
public function getBrandName() {
foreach(@self::$config['brands'] as $brand) {
if (preg_match("/^{$brand}/", $this->getName())) return $brand;
}
if ($brand = @self::$config['tlp'][$this->getTopLevelProject()->getId()])
return $brand;
return 'Eclipse';
}
/**
* This method answers the nickname for the project.
* The name of the project as listed in the foundation database is the best
* source of this information. This is difficult to guess as sometimes the
* forge name is part of the nick name and sometimes it's not. Values are
* stored in the foundation database as the nickname with optional acronym in
* parentheses. This method strips anything in parentheses from the returned
* value.
*
* We actually look for a space followed by an open parenthesis, because
* some projects (e.g. "Eclipse e(fx)clipse") include them
* in their proper name (and "Eclipse eclipse" just looks weird).
*
* @see Project::getFormalName()
*
* @return string
*/
public function getNickName() {
// We get the proxy and ask it for the name.
return trim(preg_replace('/ \([^\)]*\)/', ' ', $this->getProxy()->getName()));
}
public function getLiveliness() {
if (!$this->getCommitActivity())
return PROJECT_LIVELINESS_NEVER_ACTIVE;
if ($this->hasRecentCommitActivity(3))
return PROJECT_LIVELINESS_ACTIVE;
if ($this->hasRecentCommitActivity(6))
return PROJECT_LIVELINESS_STALE;
if ($this->hasRecentCommitActivity(12))
return PROJECT_LIVELINESS_INACTIVE;
return PROJECT_LIVELINESS_DEAD;
}
/**
* Answers whether or not the receiver has recent commit activity.
*
* @param int $months
* (Optional) how many months in the past to check.
* @return boolean
*/
public function hasRecentCommitActivity($months = 6) {
$activity = $this->getCommitActivity();
$date = strtotime('now');
while ($months) {
if (@$activity[date('Ym', $date)])
return true;
$date = strtotime("-1 month", $date);
$months--;
}
return false;
}
/**
* Returns the commit activity for the receiver in the form of an
* array mapping period to commit count.
*
* e.g.,
*
* array('201001' => 52, '201002' => 33, ...)
*
* Note that this information is cached. The first time this method
* is called (on any instance), the cache is created.
*
* @returns [] array mapping period to commit count.
*/
public function getCommitActivity() {
if (isset($this->_commitActivity))
return $this->_commitActivity;
$id = $this->getId();
$sql = "select period, count from ProjectCommitActivity where project='$id'";
$this->_commitActivity = array();
query('dashboard', $sql, array(), function ($row) {
$this->_commitActivity[$row['period']] = $row['count'];
});
return $this->_commitActivity;
}
/**
* Answer an array containing information about the source
* repositories registered for the project.
*/
public function getSourceRepositories() {
$project = $this;
$repositories = array();
$sql = "select path from GitRepo where project=':id:'";
query('dashboard', $sql, array(':id:' => $this->getId()), function($row) use (&$project, &$repositories) {
$repositories[] = $row['path'];
});
return $repositories;
}
/**
* Answers an array containing the trademarks that are known
* to be associated with the project.
*
* @return mixed
*/
function getTrademarks() {
$sql = "select name, demarcation, type from Trademark where category='project' and type is not null and id=':id:'";
$trademarks = array();
query('dashboard', $sql, array(':id:' => $this->getId()), function($row) use (&$trademarks) {
$trademarks[] = $row;
});
return $trademarks;
}
}
/**
* This class implements a lightweight stand-in to represent a
* project.
* The basic idea is that it is far less expensive to
* query the Foundation Database for information about projects
* than it is to query the PMI. However, the PMI has a lot more
* information. Instances of this class provide as much information
* as they can, but then defer to the PMI when more information is
* required.
*
* @see Project
*/
class ProjectProxy extends Project {
private $id;
private $data;
private $project;
public function __construct($id, $data) {
$this->id = $id;
$this->data = $data;
}
public function __toString() {
return "Project ({$this->getId()})";
}
public function getId() {
return $this->id;
}
public function getParentId() {
return $this->data['parent'];
}
public function getProxy() {
return $this;
}
public function getName() {
$name = $this->data ['name'];
$name = preg_replace('/\s*\[archived\]\s*/','', $name);
$name = preg_replace ( '/ Root$/', '', $name);
return $name;
}
public function getProjectPhase() {
return $this->data['phase'];
}
/**
* We don't directly track the date of project provisioning.
* Rather, we leverage a feature of the provisioning process by
* which the provisioning process is triggered when the first
* committer is ready to be assigned to the project (this is
* figured into the query that we pull from the Foundation DB).
*
* This function returns our best guess at the provisioning date
* in UNIX date format, or null if no such date can be determined.
*
* @see Project::getProjects
* @return NULL|number
*/
public function getProvisionedDate() {
if (!isset($this->data['provisioned_date'])) return null;
return strtotime($this->data['provisioned_date']);
}
public function hasProjectLeads() {
return !empty($this->data['lead_ids']);
}
public function hasCommitters() {
return !empty($this->data['committer_ids']);
}
public function getDescription() {
$apiData = $this->getAPIData();
return @$apiData[0]['description'];
}
public function getSpecificationWorkingGroupName() {
$apiData = $this->getAPIData();
return @$apiData[0]['spec_project_working_group']['name'];
}
public function isSpecificationProject() {
$apiData = $this->getAPIData();
return !empty($apiData[0]['spec_project_working_group']);
}
/**
* Get the data related to this project from the Eclipse API.
*
* @return array|mixed
*/
private function getAPIData() {
if (@$this->apiData === NULL) {
$url = 'https://projects.eclipse.org/api/projects/' . preg_replace('/\./','_',$this->getId());
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$this->apiData = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$this->apiData) $this->apiData = array();
}
return $this->apiData;
}
/**
*
* @deprecated
* @return License[]
*/
public function getLicenses() {
return License::getLicensesForProject($this->getId());
}
/**
* Answer a statement (suitable for use in a file header)
* that describes the licenses of the project.
*
* @deprecated Use License::getLicensesStatement
* @return string
*/
public function getLicensesStatement() {
return License::getLicensesStatement($this->getLicenses());
}
/**
* @deprecated Use License::getSPDXLicenseExpression
* @return string
*/
public function getSPDXLicenseExpression() {
return License::getSPDXExpression($this->getLicenses());
}
/**
* @deprecated Use License::getDefaultFileHeader
* @return string
*/
public function getDefaultFileHeader() {
return License::getDefaultFileHeader($this->getLicenses());
}
/**
* @deprecated Use License::getAlternativeFileHeader
* @return string
*/
public function getAlternativeFileHeader() {
return License::getAlternativeFileHeader($this->getLicenses());
}
public function getDownloadUri() {
if (!$url = $this->data['UrlDownload'])
return null;
// TODO We should probably ask the forge for the downloadUrl
$forge = $this->getForge()->getId();
if (!preg_match("/^https?:\/\/download.{$forge}.org\/(.*)$/", $url, $matches))
return null;
return $matches[1];
}
public function __call($name, $arguments) {
if (!$project = $this->getPMIProject())
return null;
return call_user_func_array(array(
$project,
$name
), $arguments);
}
private function getPMIProject() {
if ($this->project === null) {
$project = PMIProject::getProject($this->getId());
$this->project = $project ? $project : FALSE;
}
return $this->project;
}
}
/**
* Representation of an Eclipse Foundation project with information
* obtained from a PMI instance.
* Instances of this class should rarely
* be made directly; use the static methods on Project to obtain
* instances.
*
* @see Project
*/
class PMIProject extends Project {
private $data;
private $forge;
private static $cache = array();
/**
* Obtain an representation of a project from a PMI instance.
*
* This method should not generally be used directly. Instead,
* use the static methods on Project to obtain a reference to a project
* and that reference will make this call if it is required.
*
* @param string $id
* @return NULL|PMIProject
*/
public static function getProject($id) {
if (isset(self::$cache[$id]))
return self::$cache[$id];
if (!$forge = Forge::getForgeForProjectId($id))
return null;
$base = $forge->getUrl();
$local = $forge->getLocalProjectId($id);
$url = "{$base}/json/project/$local";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$json = curl_exec($ch);
curl_close($ch);
if (!$json)
return null;
if (!$all = json_decode($json, true))
return null;
if (!$data = $all['projects'][$local])
return null;
return new PMIProject($data, $forge);
}
/**
* @deprecated
*/
public static function preload() {
}
private function __construct($data, $forge) {
$this->data = $data;
$this->forge = $forge;
}
/**
* Answer the project's id as it appears in the Foundation Database.
* The PMI data contains the local id. If the project comes from an
* alternate forge, then the forge id is prepended to the local value.
*
* @return string
*/
public function getId() {
return $this->getForge()->getFoundationDBId($this->getLocalId());
}
public function getLocalId() {
return $this->data['id'][0]['value'];
}
public function getForge() {
return $this->forge;
}
public function getProxy() {
// TODO Consider caching this value
return Project::getProject($this->getId());
}
public function getName() {
// TODO Get the name (when it's available)
return $this->data['title'];
}
public function getDevListUrl() {
return $this->data['dev_list']['url'];
}
public function getPlanUrl() {
return @$this->data['plan_url'][0]['url'];
}
/**
* Answer the project's parent's id as it appears in the Foundation Database.
* The PMI data contains the local id. If the project comes from an
* alternate forge, then the forge id is prepended to the local value.
*
* @return string
*/
public function getParentId() {
return $this->getForge()->getFoundationDBId(@$this->data['parent_project'][0]['id']);
}
/**
*
* @deprecated
*
* @return string
*/
function getProjectUrl() {
return $this->getUrl();
}
/**
* Answer an array of information regarding project releases.
*
* @return PMIRelease[]
*/
function getReleases() {
$releases = array();
if (isset($this->data['releases'])) {
foreach ($this->data['releases'] as $data) {
$releases[] = new PMIRelease($this, $data);
}
}
return $releases;
}
public function getReviews() {
// FIXME Consider implementing this.
return array();
}
/**
* Answers the release that falls after a particular time.
* If a date is not provided in the parameter, the current
* date and time is assumed.
*
* @param int $time
* UNIX date.
*/
public function getNextRelease($since = null) {
if (!$since)
$since = time();
$releases = $this->getReleases();
usort($releases, function ($a, $b) {
return $a->getDate() == $b->getDate() ? 0 : ($a->getDate() < $b->getDate() ? -1 : 1);
});
foreach ($releases as $release) {
if ($release->getDate() > $since)
return $release;
}
return null;
}
}
class PMIRelease {
private $project;
private $data;
function __construct($project, $data) {
$this->project = $project;
$this->data = $data;
}
public function getTitle() {
return $this->data['title'];
}
public function getName() {
return $this->getTitle();
}
/**
* Answers the UNIX date of the release.
*
* @return int
*/
public function getDate() {
foreach ($this->data['date'] as $data) {
return strtotime($data['value']);
}
return null;
}
/**
* Answers the URL for the release record.
* If the information
* is provided with the PMI data, then return that value. Otherwise,
* make a very good guess at the value using the project URL and
* the standard pattern for naming releases.
*
* Most of the time, the guess will be correct. But since the PMI
* uses pathauto and there some cases where the project team e.g.
* creates two releases with the same name, there is some risk that
* the URL will be invalid. This risk is relatively low, however,
* as we we usually (always?) have a URL from the PMI.
*
* @return string
*/
public function getUrl() {
if ($url = $this->data['url'])
return $url;
$url = $this->project->getUrl();
$title = $this->getTitle();
return "$url/releases/$title";
}
public function getNoteworthyUrl() {
return @$this->data['noteworthy'][0]['url'];
}
}
/**
* Sort the array of projects by the given key.
* Valid keys
* are 'name', or 'top' (top level project).
*
* @param Project[] $projects
* @param string $key
*/
function sortProjects(&$projects, $key = 'name') {
if (!in_array($key, array(
'name',
'top'
)))
$key = 'name';
return usort($projects, "_sortProjects_$key");
}
/**
* Sort by Project name
*
* @internal
*
* @param Project $a
* @param Project $b
* @return number
*/
function _sortProjects_name($a, $b) {
return strcasecmp($a->getName(), $b->getName());
}
/**
* Sort project by Top Level Project.
*
* @internal
*
* @param Project $a
* @param Project $b
* @return number
*/
function _sortProjects_top($a, $b) {
return strcasecmp($a->getTopLevelProject()->getName(), $b->getTopLevelProject()->getName());
}
// Initialize the class
Project::init();
?>