Source for file Install.php
Documentation is available at Install.php
* PEAR_Command_Install (install, upgrade, upgrade-all, uninstall, bundle, run-scripts commands)
* @author Stig Bakken <ssb@php.net>
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2009 The Authors
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: Install.php 313023 2011-07-06 19:17:11Z dufuz $
* @link http://pear.php.net/package/PEAR
* @since File available since Release 0.1
require_once 'PEAR/Command/Common.php';
* PEAR commands for installation or deinstallation/upgrading of
* @author Stig Bakken <ssb@php.net>
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2009 The Authors
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version Release: 1.9.4
* @link http://pear.php.net/package/PEAR
* @since Class available since Release 0.1
'summary' => 'Install Package',
'function' => 'doInstall',
'doc' => 'will overwrite newer installed packages',
'doc' => 'do not check for recommended dependency version',
'doc' => 'ignore dependencies, install anyway',
'register-only' => array (
'doc' => 'do not install files, only register the package as installed',
'doc' => 'soft install, fail silently, or upgrade if already installed',
'doc' => 'don\'t build C extensions',
'doc' => 'request uncompressed files when downloading',
'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT), use packagingroot for RPM',
'packagingroot' => array (
'doc' => 'root directory used when packaging files, like RPM packaging',
'ignore-errors' => array (
'doc' => 'force install even if there were errors',
'doc' => 'install all required and optional dependencies',
'doc' => 'install all required dependencies',
'doc' => 'do not attempt to download any urls or contact channels',
'doc' => 'Only list the packages that would be downloaded',
'doc' => '[channel/]<package> ...
Installs one or more PEAR packages. You can specify a package to
"Package-1.0.tgz" : installs from a local file
"http://example.com/Package-1.0.tgz" : installs from
"package.xml" : installs the package described in
package.xml. Useful for testing, or for wrapping a PEAR package in
another package manager such as RPM.
"Package[-version/state][.tar]" : queries your default channel\'s server
({config master_server}) and downloads the newest package with
the preferred quality/state ({config preferred_state}).
To retrieve Package version 1.1, use "Package-1.1," to retrieve
Package state beta, use "Package-beta." To retrieve an uncompressed
file, append .tar (make sure there is no file by the same name first)
To download a package from another channel, prefix with the channel name like
More than one package may be specified at once. It is ok to mix these
four ways of specifying packages.
'summary' => 'Upgrade Package',
'function' => 'doInstall',
'doc' => 'upgrade packages from a specific channel',
'doc' => 'overwrite newer installed packages',
'doc' => 'do not check for recommended dependency version',
'doc' => 'ignore dependencies, upgrade anyway',
'register-only' => array (
'doc' => 'do not install files, only register the package as upgraded',
'doc' => 'don\'t build C extensions',
'doc' => 'request uncompressed files when downloading',
'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT)',
'ignore-errors' => array (
'doc' => 'force install even if there were errors',
'doc' => 'install all required and optional dependencies',
'doc' => 'install all required dependencies',
'doc' => 'do not attempt to download any urls or contact channels',
'doc' => 'Only list the packages that would be downloaded',
Upgrades one or more PEAR packages. See documentation for the
"install" command for ways to specify a package.
When upgrading, your package will be updated if the provided new
package has a higher version number (use the -f option if you need to
More than one package may be specified at once.
'summary' => 'Upgrade All Packages [Deprecated in favor of calling upgrade with no parameters]',
'function' => 'doUpgradeAll',
'doc' => 'upgrade packages from a specific channel',
'doc' => 'ignore dependencies, upgrade anyway',
'register-only' => array (
'doc' => 'do not install files, only register the package as upgraded',
'doc' => 'don\'t build C extensions',
'doc' => 'request uncompressed files when downloading',
'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT), use packagingroot for RPM',
'ignore-errors' => array (
'doc' => 'force install even if there were errors',
'doc' => 'do not check for recommended dependency version',
WARNING: This function is deprecated in favor of using the upgrade command with no params
Upgrades all packages that have a newer release available. Upgrades are
done only if there is a release available of the state specified in
"preferred_state" (currently {config preferred_state}), or a state considered
'summary' => 'Un-install Package',
'function' => 'doUninstall',
'doc' => 'ignore dependencies, uninstall anyway',
'register-only' => array (
'doc' => 'do not remove files, only register the packages as not installed',
'doc' => 'root directory used when installing files (ala PHP\'s INSTALL_ROOT)',
'ignore-errors' => array (
'doc' => 'force install even if there were errors',
'doc' => 'do not attempt to uninstall remotely',
'doc' => '[channel/]<package> ...
Uninstalls one or more PEAR packages. More than one package may be
specified at once. Prefix with channel name to uninstall from a
channel not in your default channel ({config default_channel})
'summary' => 'Unpacks a Pecl Package',
'function' => 'doBundle',
'doc' => 'Optional destination directory for unpacking (defaults to current path or "ext" if exists)',
'doc' => 'Force the unpacking even if there were errors in the package',
Unpacks a Pecl Package into the selected location. It will download the
'summary' => 'Run Post-Install Scripts bundled with a package',
'function' => 'doRunScripts',
Run post-installation scripts in package <package>, if any exist.
* PEAR_Command_Install constructor.
* For unit testing purposes
require_once 'PEAR/Downloader.php';
* For unit testing purposes
require_once 'PEAR/Installer.php';
if (!($phpini = $this->config->get ('php_ini', null , 'pear.php.net'))) {
return PEAR::raiseError('configuration option "php_ini" is not set to php.ini location');
$ini = $this->_parseIni ($phpini);
if ($type == 'extsrc' || $type == 'extbin') {
$search = 'zend_extensions';
$ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : '';
$enable = 'zend_extension' . $debug . $ts;
foreach ($ini[$search] as $line => $extension) {
$ini['extension_dir'] . DIRECTORY_SEPARATOR . $extension, $binaries, true )) {
// already enabled - assume if one is, all are
foreach ($binaries as $binary) {
if ($ini['extension_dir']) {
$newini[] = $enable . '="' . $binary . '"' . (OS_UNIX ? "\n" : "\r\n");
$fp = @fopen($phpini, 'wb');
return PEAR::raiseError('cannot open php.ini "' . $phpini . '" for writing');
foreach ($newini as $line) {
if (!($phpini = $this->config->get ('php_ini', null , 'pear.php.net'))) {
return PEAR::raiseError('configuration option "php_ini" is not set to php.ini location');
$ini = $this->_parseIni ($phpini);
if ($type == 'extsrc' || $type == 'extbin') {
$search = 'zend_extensions';
$ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : '';
$enable = 'zend_extension' . $debug . $ts;
foreach ($ini[$search] as $line => $extension) {
$ini['extension_dir'] . DIRECTORY_SEPARATOR . $extension, $binaries, true )) {
$fp = @fopen($phpini, 'wb');
return PEAR::raiseError('cannot open php.ini "' . $phpini . '" for writing');
// delete the enable line
foreach ($newini as $line) {
function _parseIni ($filename)
return PEAR::raiseError('php.ini "' . $filename . '" does not exist');
return PEAR::raiseError('php.ini "' . $filename . '" is too large, aborting');
$ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : '';
$zend_extension_line = 'zend_extension' . $debug . $ts;
return PEAR::raiseError('php.ini "' . $filename . '" could not be read');
$zend_extensions = $extensions = array ();
// assume this is right, but pull from the php.ini if it is found
$extension_dir = ini_get('extension_dir');
foreach ($all as $linenum => $line) {
'extensions' => $extensions,
'zend_extensions' => $zend_extensions,
'extension_dir' => $extension_dir,
function doInstall($command, $options, $params)
require_once 'PEAR/PackageFile.php';
if (isset ($options['installroot']) && isset ($options['packagingroot'])) {
return $this->raiseError('ERROR: cannot use both --installroot and --packagingroot');
$reg = &$this->config->getRegistry ();
$channel = isset ($options['channel']) ? $options['channel'] : $this->config->get ('default_channel');
if (!$reg->channelExists ($channel)) {
return $this->raiseError('Channel "' . $channel . '" does not exist');
if (empty ($this->installer)) {
if ($command == 'upgrade' || $command == 'upgrade-all') {
// If people run the upgrade command but pass nothing, emulate a upgrade-all
if ($command == 'upgrade' && empty ($params)) {
$options['upgrade'] = true;
$instreg = &$reg; // instreg used to check if package is installed
if (isset ($options['packagingroot']) && !isset ($options['upgrade'])) {
$packrootphp_dir = $this->installer->_prependPath (
$this->config->get ('php_dir', null , 'pear.php.net'),
$options['packagingroot']);
$instreg = new PEAR_Registry($packrootphp_dir); // other instreg!
if ($this->config->get ('verbose') > 2 ) {
$this->ui->outputData ('using package root: ' . $options['packagingroot']);
$abstractpackages = $otherpackages = array ();
foreach ($params as $param) {
if (strpos($param, 'http://') === 0 ) {
$otherpackages[] = $param;
if (isset ($options['force'])) {
$otherpackages[] = $param;
$otherpackages[] = $param;
$exists = $reg->packageExists ($pf->getPackage (), $pf->getChannel ());
$pversion = $reg->packageInfo ($pf->getPackage (), 'version', $pf->getChannel ());
if ($exists && $version_compare) {
if ($this->config->get ('verbose')) {
$this->ui->outputData ('Ignoring installed package ' .
$reg->parsedPackageNameToString (
array ('package' => $pf->getPackage (),
'channel' => $pf->getChannel ()), true ));
$otherpackages[] = $param;
$e = $reg->parsePackageName ($param, $channel);
$otherpackages[] = $param;
$abstractpackages[] = $e;
// if there are any local package .tgz or remote static url, we can't
// filter. The filter only works for abstract packages
if (count($abstractpackages) && !isset ($options['force'])) {
// when not being forced, only do necessary upgrades/installs
if (isset ($options['upgrade'])) {
$abstractpackages = $this->_filterUptodatePackages ($abstractpackages, $command);
$count = count($abstractpackages);
foreach ($abstractpackages as $i => $package) {
if (isset ($package['group'])) {
// do not filter out install groups
if ($instreg->packageExists ($package['package'], $package['channel'])) {
if ($this->config->get ('verbose')) {
$this->ui->outputData ('Ignoring installed package ' .
$reg->parsedPackageNameToString ($package, true ));
unset ($abstractpackages[$i]);
} elseif ($count === 1 ) {
// Lets try to upgrade it since it's already installed
$options['upgrade'] = true;
array_map(array ($reg, 'parsedPackageNameToString'), $abstractpackages);
} elseif (count($abstractpackages)) {
array_map(array ($reg, 'parsedPackageNameToString'), $abstractpackages);
$packages = array_merge($abstractpackages, $otherpackages);
if (isset ($options['channel'])){
$c .= ' in channel "' . $options['channel'] . '"';
$this->ui->outputData ('Nothing to ' . $command . $c);
$errors = $downloaded = $binaries = array ();
$downloaded = &$this->downloader->download ($packages);
$errors = $this->downloader->getErrorMsgs ();
foreach ($errors as $error) {
$err['data'][] = array ($error);
if (!empty ($err['data'])) {
$err['headline'] = 'Install Errors';
$this->ui->outputData ($err);
if (!count($downloaded)) {
'headline' => 'Packages that would be Installed'
if (isset ($options['pretend'])) {
foreach ($downloaded as $package) {
$data['data'][] = array ($reg->parsedPackageNameToString ($package->getParsedPackage ()));
$this->ui->outputData ($data, 'pretend');
$this->installer->setOptions ($options);
$this->installer->sortPackagesForInstall ($downloaded);
if (PEAR::isError($err = $this->installer->setDownloadedPackages ($downloaded))) {
$binaries = $extrainfo = array ();
foreach ($downloaded as $param) {
$info = $this->installer->install ($param, $options);
$pkg = &$param->getPackageFile ();
if (!($info = $pkg->installBinary ($this->installer))) {
$this->ui->outputData ('ERROR: ' . $oldinfo->getMessage ());
// we just installed a different package than requested,
// let's change the param and info so that the rest of this works
if ($param->getPackageType () == 'extsrc' ||
$param->getPackageType () == 'extbin' ||
$param->getPackageType () == 'zendextsrc' ||
$param->getPackageType () == 'zendextbin'
$pkg = &$param->getPackageFile ();
if ($instbin = $pkg->getInstalledBinary ()) {
$instpkg = &$instreg->getPackage ($instbin, $pkg->getChannel ());
$instpkg = &$instreg->getPackage ($pkg->getPackage (), $pkg->getChannel ());
foreach ($instpkg->getFilelist () as $name => $atts) {
$pinfo = pathinfo($atts['installed_as']);
if (!isset ($pinfo['extension']) ||
in_array($pinfo['extension'], array ('c', 'h'))
continue; // make sure we don't match php_blah.h
if ((strpos($pinfo['basename'], 'php_') === 0 &&
$pinfo['extension'] == 'dll') ||
$pinfo['extension'] == 'so' ||
$pinfo['extension'] == 'sl') {
$binaries[] = array ($atts['installed_as'], $pinfo);
foreach ($binaries as $pinfo) {
$extrainfo[] = $ret->getMessage ();
if ($param->getPackageType () == 'extsrc' ||
$param->getPackageType () == 'extbin') {
$ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : '';
$exttype = 'zend_extension' . $debug . $ts;
$extrainfo[] = 'You should add "' . $exttype . '=' .
$pinfo[1 ]['basename'] . '" to php.ini';
$extrainfo[] = 'Extension ' . $instpkg->getProvidesExtension () .
if ($this->config->get ('verbose') > 0 ) {
$chan = $param->getChannel ();
$label = $reg->parsedPackageNameToString (
'package' => $param->getPackage (),
'version' => $param->getVersion (),
$out = array ('data' => " $command ok: $label" );
if (isset ($info['release_warnings'])) {
$out['release_warnings'] = $info['release_warnings'];
$this->ui->outputData ($out, $command);
if (!isset ($options['register-only']) && !isset ($options['offline'])) {
if ($this->config->isDefinedLayer ('ftp')) {
$info = $this->installer->ftpInstall ($param);
$this->ui->outputData ($info->getMessage ());
$this->ui->outputData (" remote install failed: $label" );
$this->ui->outputData (" remote install ok: $label" );
$deps = $param->getDeps ();
if (isset ($deps['group'])) {
$groups = $deps['group'];
if (!isset ($groups[0 ])) {
$groups = array ($groups);
foreach ($groups as $group) {
if ($group['attribs']['name'] == 'default') {
// default group is always installed, unless the user
// explicitly chooses to install another group
$extrainfo[] = $param->getPackage () . ': Optional feature ' .
$group['attribs']['name'] . ' available (' .
$group['attribs']['hint'] . ')';
$extrainfo[] = $param->getPackage () .
': To install optional features use "pear install ' .
$reg->parsedPackageNameToString (
array ('package' => $param->getPackage (),
'channel' => $param->getChannel ()), true ) .
$pkg = &$instreg->getPackage ($param->getPackage (), $param->getChannel ());
// $pkg may be NULL if install is a 'fake' install via --packagingroot
$pkg->setConfig ($this->config);
if ($list = $pkg->listPostinstallScripts ()) {
$pn = $reg->parsedPackageNameToString (array ('channel' =>
$param->getChannel (), 'package' => $param->getPackage ()), true );
$extrainfo[] = $pn . ' has post-install scripts:';
foreach ($list as $file) {
$extrainfo[] = $param->getPackage () .
': Use "pear run-scripts ' . $pn . '" to finish setup.';
$extrainfo[] = 'DO NOT RUN SCRIPTS FROM UNTRUSTED SOURCES';
foreach ($extrainfo as $info) {
$this->ui->outputData ($info);
$reg = &$this->config->getRegistry ();
if (isset ($options['channel'])) {
$channels = array ($options['channel']);
$channels = $reg->listChannels ();
foreach ($channels as $channel) {
if ($channel == '__uri') {
// parse name with channel
foreach ($reg->listPackages ($channel) as $name) {
$upgrade[] = $reg->parsedPackageNameToString (array (
$err = $this->doInstall($command, $options, $upgrade);
$this->ui->outputData ($err->getMessage (), $command);
if (count($params) < 1 ) {
return $this->raiseError("Please supply the package(s) you want to uninstall");
if (empty ($this->installer)) {
if (isset ($options['remoteconfig'])) {
$e = $this->config->readFTPConfigFile ($options['remoteconfig']);
$this->installer->setConfig ($this->config);
$reg = &$this->config->getRegistry ();
foreach ($params as $pkg) {
$channel = $this->config->get ('default_channel');
$parsed = $reg->parsePackageName ($pkg, $channel);
$package = $parsed['package'];
$channel = $parsed['channel'];
$info = &$reg->getPackage ($package, $channel);
($channel == 'pear.php.net' || $channel == 'pecl.php.net')) {
// make sure this isn't a package that has flipped from pear to pecl but
// used a package.xml 1.0
$testc = ($channel == 'pear.php.net') ? 'pecl.php.net' : 'pear.php.net';
$info = &$reg->getPackage ($package, $testc);
// check for binary packages (this is an alias for those packages if so)
if ($installedbinary = $info->getInstalledBinary ()) {
$this->ui->log ('adding binary package ' .
$reg->parsedPackageNameToString (array ('channel' => $channel,
'package' => $installedbinary), true ));
$newparams[] = &$reg->getPackage ($installedbinary, $channel);
// add the contents of a dependency group to the list of installed packages
if (isset ($parsed['group'])) {
$group = $info->getDependencyGroup ($parsed['group']);
$installed = $reg->getInstalledGroup ($group);
foreach ($installed as $i => $p) {
$newparams[] = &$installed[$i];
$err = $this->installer->sortPackagesForUninstall ($newparams);
$this->ui->outputData ($err->getMessage (), $command);
// twist this to use it to check on whether dependent packages are also being uninstalled
// for circular dependencies like subpackages
$this->installer->setUninstallPackages ($newparams);
foreach ($params as $pkg) {
if ($err = $this->installer->uninstall ($pkg, $options)) {
$this->installer->popErrorHandling ();
$this->ui->outputData ($err->getMessage (), $command);
if ($pkg->getPackageType () == 'extsrc' ||
$pkg->getPackageType () == 'extbin' ||
$pkg->getPackageType () == 'zendextsrc' ||
$pkg->getPackageType () == 'zendextbin') {
if ($instbin = $pkg->getInstalledBinary ()) {
continue; // this will be uninstalled later
foreach ($pkg->getFilelist () as $name => $atts) {
$pinfo = pathinfo($atts['installed_as']);
if (!isset ($pinfo['extension']) ||
in_array($pinfo['extension'], array ('c', 'h'))) {
continue; // make sure we don't match php_blah.h
if ((strpos($pinfo['basename'], 'php_') === 0 &&
$pinfo['extension'] == 'dll') ||
$pinfo['extension'] == 'so' ||
$pinfo['extension'] == 'sl') {
$binaries[] = array ($atts['installed_as'], $pinfo);
foreach ($binaries as $pinfo) {
$extrainfo[] = $ret->getMessage ();
if ($pkg->getPackageType () == 'extsrc' ||
$pkg->getPackageType () == 'extbin') {
$ts = preg_match('/Thread Safety.+enabled/', $info) ? '_ts' : '';
$exttype = 'zend_extension' . $debug . $ts;
$this->ui->outputData ('Unable to remove "' . $exttype . '=' .
$pinfo[1 ]['basename'] . '" from php.ini', $command);
$this->ui->outputData ('Extension ' . $pkg->getProvidesExtension () .
' disabled in php.ini', $command);
if ($this->config->get ('verbose') > 0 ) {
$pkg = $reg->parsedPackageNameToString ($pkg);
$this->ui->outputData (" uninstall ok: $pkg" , $command);
if (!isset ($options['offline']) && is_object($savepkg) &&
defined('PEAR_REMOTEINSTALL_OK')) {
if ($this->config->isDefinedLayer ('ftp')) {
$info = $this->installer->ftpUninstall ($savepkg);
$this->installer->popErrorHandling ();
$this->ui->outputData ($info->getMessage ());
$this->ui->outputData (" remote uninstall failed: $pkg" );
$this->ui->outputData (" remote uninstall ok: $pkg" );
$this->installer->popErrorHandling ();
return $this->raiseError(" uninstall failed: $pkg" );
$pkg = $reg->parsedPackageNameToString ($pkg);
(cox) It just downloads and untars the package, does not do
any check that the PEAR_Installer::_installFile() does.
function doBundle($command, $options, $params)
$reg = &$this->config->getRegistry ();
if (count($params) < 1 ) {
return $this->raiseError("Please supply the package you want to bundle");
if (isset ($options['destination'])) {
if (!is_dir($options['destination'])) {
System::mkdir ('-p ' . $options['destination']);
$dest = realpath($options['destination']);
$dir = $pwd . DIRECTORY_SEPARATOR . 'ext';
$dest = is_dir($dir) ? $dir : $pwd;
$err = $downloader->setDownloadDir ($dest);
$result = &$downloader->download (array ($params[0 ]));
if (!isset ($result[0 ])) {
return $this->raiseError('unable to unpack ' . $params[0 ]);
$pkgfile = &$result[0 ]->getPackageFile ();
$pkgname = $pkgfile->getName ();
$pkgversion = $pkgfile->getVersion ();
// Unpacking -------------------------------------------------
$dest .= DIRECTORY_SEPARATOR . $pkgname;
$orig = $pkgname . '-' . $pkgversion;
$tar = &new Archive_Tar ($pkgfile->getArchiveFile ());
if (!$tar->extractModify ($dest, $orig)) {
return $this->raiseError('unable to unpack ' . $pkgfile->getArchiveFile ());
$this->ui->outputData (" Package ready at '$dest'" );
if (!isset ($params[0 ])) {
return $this->raiseError('run-scripts expects 1 parameter: a package name');
$reg = &$this->config->getRegistry ();
$parsed = $reg->parsePackageName ($params[0 ], $this->config->get ('default_channel'));
$package = &$reg->getPackage ($parsed['package'], $parsed['channel']);
return $this->raiseError('Could not retrieve package "' . $params[0 ] . '" from registry');
$package->setConfig ($this->config);
$package->runPostinstallScripts ();
$this->ui->outputData ('Install scripts complete', $command);
* Given a list of packages, filter out those ones that are already up to date
* @param $packages: packages, in parsed array format !
* @return list of packages that can be upgraded
function _filterUptodatePackages ($packages, $command)
$reg = &$this->config->getRegistry ();
$latestReleases = array ();
foreach ($packages as $package) {
if (isset ($package['group'])) {
$channel = $package['channel'];
$name = $package['package'];
if (!$reg->packageExists ($name, $channel)) {
if (!isset ($latestReleases[$channel])) {
// fill in cache for this channel
$chan = &$reg->getChannel ($channel);
$preferred_mirror = $this->config->get ('preferred_mirror', null , $channel);
if ($chan->supportsREST ($preferred_mirror) &&
//($base2 = $chan->getBaseURL('REST1.4', $preferred_mirror)) ||
($base = $chan->getBaseURL ('REST1.0', $preferred_mirror))
if (!isset ($package['state'])) {
$state = $this->config->get ('preferred_state', null , $channel);
$state = $package['state'];
$rest = &$this->config->getREST ('1.4', array ());
$rest = &$this->config->getREST ('1.0', array ());
$installed = array_flip($reg->listPackages ($channel));
$latest = $rest->listLatestUpgrades ($base, $state, $installed, $channel, $reg);
$this->ui->outputData ('Error getting channel info from ' . $channel .
': ' . $latest->getMessage ());
// check package for latest release
if (isset ($latestReleases[$channel][$name_lower])) {
// if not set, up to date
$inst_version = $reg->packageInfo ($name, 'version', $channel);
$channel_version = $latestReleases[$channel][$name_lower]['version'];
// installed version is up-to-date
if ($command == 'upgrade-all') {
$this->ui->outputData (array ('data' => 'Will upgrade ' .
$reg->parsedPackageNameToString ($package)), $command);
Documentation generated on Wed, 06 Jul 2011 23:30:52 +0000 by phpDocumentor 1.4.3. PEAR Logo Copyright © PHP Group 2004.
|