/** * Scan the directory set in $repoConfig['url'] * and create any found packages. */ protected function scanDir() { $dir = $this->repoConfig['url']; // make sure tilde is not escaped so it can be expanded // this allows '~/' followed by a path or just '~' if (($tilde = substr($dir, 0, 2)) === '~/' || $tilde === '~') { $dir = $tilde . ProcessExecutor::escape(substr($dir, strlen($tilde))); } else { $dir = ProcessExecutor::escape($dir); } // patterns specific to both plugins and themes // 'inflating' is a line printed by unzip which indicates which internal file we are looking at $patterns = ['inflating|Version|Description|Author|Author URI|License']; // files within the archives to look at $files = []; // look for plugins? if (isset($this->repoConfig['package-types']['wordpress-plugin']) || isset($this->repoConfig['package-types']['wordpress-muplugin'])) { $patterns[] = 'Plugin Name|Plugin URI'; $files[] = "'*.php'"; } // look for themes? if (isset($this->repoConfig['package-types']['wordpress-theme'])) { $patterns[] = 'Theme Name|Theme URI'; $files[] = "'style.css'"; } // determine if we have a depth limit $maxdepth = ($depth = (int) $this->repoConfig['max-depth']) > 0 ? "-maxdepth {$depth}" : ''; // assemble the command // 1. `find` to get all zip files in the given directory // 2. echo the filename so we can capture where the zip is // 3. use `unzip` piped into `grep` to scan the zip for WP // theme or plugin headers in style.css or *.php files, // respectively, but only in the top two directories within the zip $cmd = "find -L {$dir} {$maxdepth} -iname '*.zip' -exec echo '{}' ';' -exec sh -c " . "\"unzip -c {} '*.php' -x '*/*/*' | grep -iE '^[ ^I*]*(" . implode('|', $patterns) . ")'\" ';'"; // if this is using ssh, wrap the command in an ssh call instead if ($this->ssh) { $cmd = 'ssh ' . ProcessExecutor::escape($this->repoConfig['ssh']) . ' ' . ProcessExecutor::escape($cmd); } $process = new ProcessExecutor($this->io); // execute the command and see if the response code indicates success // @todo: do we need to catch any exceptions here? if (($code = $process->execute($cmd, $output)) === 0) { // store details about each of the files, which may be used to create a package $files = []; $zipFile = null; $fileName = null; // parse the response line-by-line to pluck out the header information foreach ($process->splitLines($output) as $line) { // is this a new zip file? if (strtolower(substr($line, -4)) === '.zip') { $zipFile = $line; // is this a new internal file? } else { if (preg_match('/^\\s*inflating:\\s*(.+?)\\s*$/i', $line, $matches)) { $fileName = $matches[1]; } else { // parse the line for information if (preg_match('/^[\\s*]*([^:]+):\\s*(.+?)\\s*$/i', $line, $matches)) { // for clarity list(, $property, $value) = $matches; $files[$zipFile][$fileName][$property] = $value; } } } } // take the header information and create packages! foreach ($files as $url => $packages) { // we will only consider zips that have one package inside if (count($packages) === 1) { // make sure all the keys are consistent $headers = array_change_key_case(reset($packages), CASE_LOWER); // file within the zip where the headers were found $fileName = key($packages); // the info used to create the package $package = []; // we have a theme! if (!empty($headers['theme name'])) { $package['type'] = 'wordpress-theme'; $name = Util::slugify($headers['theme name']); $name = Util::callFilter($this->repoConfig['name-filter'], $name, $url, $fileName, $headers); if (!empty($headers['theme uri'])) { $package['homepage'] = $headers['theme uri']; } // we have a plugin! } else { if (!empty($headers['plugin name'])) { $package['type'] = 'wordpress-plugin'; // use the basename of the file where the plugin headers were as the name // this is a wordpress convention, but may not always be accurate // @todo: what do we do about that? $name = Util::slugify($headers['plugin name']); $name = Util::callFilter($this->repoConfig['name-filter'], $name, $url, $fileName, $headers); if (!empty($headers['plugin uri'])) { $package['homepage'] = $headers['plugin uri']; } // does not appear to be a theme or plugin // sometimes other files get picked up } else { if ($this->io->isVerbose()) { $this->io->writeError("{$url} does not appear to contain a valid package"); } continue; } } // if the name is empty we don't use it if (!strlen($name)) { continue; } // add version if (!empty($headers['version'])) { $package['version'] = Util::fixVersion($headers['version'], 'dev-default'); } else { $package['version'] = 'dev-default'; } $package['version'] = Util::callFilter($this->repoConfig['version-filter'], $package['version'], $name, $url, $fileName, $headers); // empty version means we don't use it if (!strlen($package['version'])) { continue; } // add author information if (!empty($headers['author'])) { $package['authors'][0]['name'] = $headers['author']; if (!empty($headers['author uri'])) { $package['authors'][0]['homepage'] = $headers['author uri']; } } // add description if (!empty($headers['description'])) { $package['description'] = strip_tags($headers['description']); } // add license if (!empty($headers['license'])) { $package['license'] = $headers['license']; } // add dist information $package['dist'] = ['url' => $this->ssh ? "ssh://{$this->repoConfig['ssh']}:{$url}" : $url, 'type' => 'zip']; // add a new package for each vendor alias of the given type // @todo: maybe use links instead? or in addition to? foreach ($this->repoConfig['vendors'] as $vendor => $type) { // match wordpress-plugin for wordpress-muplugin vendors if ($type === $package['type'] || $type === 'wordpress-muplugin' && $package['type'] === 'wordpress-plugin') { // this makes sure muplugins are the correct type $package['type'] = $type; $package['name'] = "{$vendor}/{$name}"; $packageObj = $this->loader->load($package); Util::callFilter($this->repoConfig['package-filter'], $packageObj, $url, $fileName, $headers); $this->addPackage($packageObj); } } } else { // if the zip contains multiple packages, we can't use it @todo - maybe make it possible? if ($this->io->isVerbose()) { $this->io->writeError("Cannot use file {$url} as is appears to contain multiple packages."); } } } } else { // some sort of error - boo! throw new \RuntimeException('Could not complete directory scan of ' . $this->repoConfig['url'] . '. ' . $process->getErrorOutput()); } }
public function whatProvides(Pool $pool, $name, $bypassFilters = false) { // split on vendor and name if (count($parts = explode('/', $name)) !== 2) { return []; } list($vendor, $shortName) = $parts; // does the vendor match one of our virtual vendors? if (!isset($this->vendors[$vendor])) { return []; } // do we already have its packages? if (isset($this->providers[$name])) { return $this->providers[$name]; } // make sure the providers have been loaded $this->loadProviders(); // does the shortname even exist in this repo? if (!isset($this->providerHash[$shortName])) { return []; } // base url for the requested set of packages (i.e. the provider) // there should be no trailing slash $providerUrl = $this->providerHash[$shortName]; $packages = []; // get a listing of available packages // these are paths under the provider url where we should find actual packages foreach ((array) $this->repoConfig->get('package-paths') as $path) { // the relative path without surrounding slashes $relPath = trim($path, '/'); // if the path ends with a slash, we grab its subdirectories if (substr($path, -1) === '/') { // try to fetch the packages! try { if ($this->io->isVerbose()) { $this->io->writeError("Fetching available versions for {$name}"); } $pkgRaw = $this->svnUtil->execute('ls', "{$providerUrl}/{$relPath}"); } catch (\RuntimeException $e) { // @todo maybe don't throw an exception and just pass this one up? throw new \RuntimeException("SVN Error: Could not retrieve package listing for {$name}. " . $e->getMessage()); } // check the versions and add any good ones to the set foreach (SvnUtil::parseSvnList($pkgRaw) as $version) { // format the version identifier to be composer-compatible $version = Util::fixVersion($version, 'dev-default'); $version = Util::callFilter($this->repoConfig->get('version-filter'), $version, $name, $path, $providerUrl); // if the version string is empty, we don't take it if (!empty($version)) { $packages[$version] = trim("{$relPath}/{$version}", '/'); } } } else { // otherwise we add as-is (no checking is performed to see if this reference really exists) // @todo: perhaps add an optional check? $version = Util::fixVersion(basename($path), 'dev-default'); $version = Util::callFilter($this->repoConfig->get('version-filter'), $version, $name, $path, $providerUrl); // if the version string is empty, we don't take it if (!empty($version)) { $packages[$version] = $relPath; } } } // store the providers based on its full name (i.e. with vendor) // this allows the same package to be loaded as different types, // which allows the package type to be changed in composer.json, // i.e. the type that is being removed AND the type that is being installed // both have to exist during the solving $this->providers[$name] = []; // create a package for each tag foreach ($packages as $version => $reference) { if (!$pool->isPackageAcceptable($shortName, VersionParser::parseStability($version))) { continue; } // first, setup the repo-determined package properties $data = ['name' => $name, 'version' => $version, 'type' => $this->vendors[$vendor], 'source' => ['type' => 'svn', 'url' => "{$providerUrl}/", 'reference' => $reference ?: '/']]; // next, fill in any defaults that were missing if (($defaults = $this->repoConfig->get('package-defaults')) && is_array($defaults)) { $data = array_merge($defaults, $data); } // finally, apply any overrides if (($overrides = $this->repoConfig->get('package-overrides')) && is_array($overrides)) { $data = array_replace($data, $overrides); } // create the package object $package = $this->createPackage($data, 'Composer\\Package\\CompletePackage'); $package->setRepository($this); // add "replaces" array for any other vendors that this repository supports if (count($this->vendors) > 1) { $replaces = []; $constraint = new Constraint('=', $package->getVersion()); foreach ($this->vendors as $vendorName => $type) { // it doesn't replace itself if ($vendorName === $vendor) { continue; } $replaces[] = new Link($package->getName(), "{$vendorName}/{$shortName}", $constraint, "'{$type}' alias for", $package->getPrettyVersion()); } $package->setReplaces($replaces); } // apply a filter to the package object Util::callFilter($this->repoConfig->get('package-filter'), $package); // add the package object to the set $this->providers[$name][$version] = $package; // handle root aliases // @todo: not sure if this is correct (this was copped from the parent class) if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { $rootAliasData = $this->rootAliases[$package->getName()][$package->getVersion()]; $alias = $this->createAliasPackage($package, $rootAliasData['alias_normalized'], $rootAliasData['alias']); $alias->setRepository($this); $this->providers[$name][$version . '-root'] = $alias; } } return $this->providers[$name]; }