1<?php
2
3/*
4 * This file is part of the Fxp Composer Asset Plugin package.
5 *
6 * (c) François Pluchino <francois.pluchino@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Fxp\Composer\AssetPlugin\Repository\Vcs;
13
14use Composer\Cache;
15use Composer\Downloader\TransportException;
16use Composer\Json\JsonFile;
17use Composer\Repository\Vcs\GitHubDriver as BaseGitHubDriver;
18use Fxp\Composer\AssetPlugin\Repository\Util as RepoUtil;
19
20/**
21 * Abstract class for GitHub vcs driver.
22 *
23 * @author François Pluchino <francois.pluchino@gmail.com>
24 */
25abstract class AbstractGitHubDriver extends BaseGitHubDriver
26{
27    /**
28     * @var Cache
29     */
30    protected $cache;
31
32    /**
33     * @var null|false|string
34     */
35    protected $redirectApi;
36
37    public function initialize()
38    {
39        if (!isset($this->repoConfig['no-api'])) {
40            $this->repoConfig['no-api'] = $this->getNoApiOption();
41        }
42
43        parent::initialize();
44    }
45
46    /**
47     * {@inheritdoc}
48     */
49    public function getBranches()
50    {
51        if ($this->gitDriver) {
52            return $this->gitDriver->getBranches();
53        }
54
55        if (null === $this->branches) {
56            $this->branches = array();
57            $resource = $this->getApiUrl().'/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100';
58            $branchBlacklist = 'gh-pages' === $this->getRootIdentifier() ? array() : array('gh-pages');
59
60            $this->doAddBranches($resource, $branchBlacklist);
61        }
62
63        return $this->branches;
64    }
65
66    /**
67     * Get the no-api repository option.
68     *
69     * @return bool
70     */
71    protected function getNoApiOption()
72    {
73        $packageName = $this->repoConfig['package-name'];
74        $opts = RepoUtil::getArrayValue($this->repoConfig, 'vcs-driver-options', array());
75        $noApiOpt = RepoUtil::getArrayValue($opts, 'github-no-api', array());
76        $defaultValue = false;
77
78        if (\is_bool($noApiOpt)) {
79            $defaultValue = $noApiOpt;
80            $noApiOpt = array();
81        }
82
83        $noApiOpt['default'] = (bool) RepoUtil::getArrayValue($noApiOpt, 'default', $defaultValue);
84        $noApiOpt['packages'] = (array) RepoUtil::getArrayValue($noApiOpt, 'packages', array());
85
86        return (bool) RepoUtil::getArrayValue($noApiOpt['packages'], $packageName, $defaultValue);
87    }
88
89    /**
90     * Get the remote content.
91     *
92     * @param string $url              The URL of content
93     * @param bool   $fetchingRepoData Fetching the repo data or not
94     *
95     * @return mixed The result
96     */
97    protected function getContents($url, $fetchingRepoData = false)
98    {
99        $url = $this->getValidContentUrl($url);
100
101        if (null !== $this->redirectApi) {
102            return parent::getContents($url, $fetchingRepoData);
103        }
104
105        try {
106            $contents = $this->getRemoteContents($url);
107            $this->redirectApi = false;
108
109            return $contents;
110        } catch (TransportException $e) {
111            if ($this->hasRedirectUrl($url)) {
112                $url = $this->getValidContentUrl($url);
113            }
114
115            return parent::getContents($url, $fetchingRepoData);
116        }
117    }
118
119    /**
120     * @param string $url The url
121     *
122     * @return string The url redirected
123     */
124    protected function getValidContentUrl($url)
125    {
126        if (null === $this->redirectApi && false !== $redirectApi = $this->cache->read('redirect-api')) {
127            $this->redirectApi = $redirectApi;
128        }
129
130        if (\is_string($this->redirectApi) && 0 === strpos($url, $this->getRepositoryApiUrl())) {
131            $url = $this->redirectApi.substr($url, \strlen($this->getRepositoryApiUrl()));
132        }
133
134        return $url;
135    }
136
137    /**
138     * Check if the driver must find the new url.
139     *
140     * @param string $url The url
141     *
142     * @return bool
143     */
144    protected function hasRedirectUrl($url)
145    {
146        if (null === $this->redirectApi && 0 === strpos($url, $this->getRepositoryApiUrl())) {
147            $this->redirectApi = $this->getNewRepositoryUrl();
148
149            if (\is_string($this->redirectApi)) {
150                $this->cache->write('redirect-api', $this->redirectApi);
151            }
152        }
153
154        return \is_string($this->redirectApi);
155    }
156
157    /**
158     * Get the new url of repository.
159     *
160     * @return false|string The new url or false if there is not a new url
161     */
162    protected function getNewRepositoryUrl()
163    {
164        try {
165            $this->getRemoteContents($this->getRepositoryUrl());
166            $headers = $this->remoteFilesystem->getLastHeaders();
167
168            if (!empty($headers[0]) && preg_match('{^HTTP/\S+ (30[1278])}i', $headers[0], $match)) {
169                array_shift($headers);
170
171                return $this->findNewLocationInHeader($headers);
172            }
173
174            return false;
175        } catch (\Exception $ex) {
176            return false;
177        }
178    }
179
180    /**
181     * Find the new url api in the header.
182     *
183     * @param array $headers The http header
184     *
185     * @return false|string
186     */
187    protected function findNewLocationInHeader(array $headers)
188    {
189        $url = false;
190
191        foreach ($headers as $header) {
192            if (0 === strpos($header, 'Location:')) {
193                $newUrl = trim(substr($header, 9));
194                preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git|/)?$#', $newUrl, $match);
195                $owner = $match[3];
196                $repository = $match[4];
197                $paramPos = strpos($repository, '?');
198                $repository = \is_int($paramPos) ? substr($match[4], 0, $paramPos) : $repository;
199                $url = $this->getRepositoryApiUrl($owner, $repository);
200
201                break;
202            }
203        }
204
205        return $url;
206    }
207
208    /**
209     * Get the url API of the repository.
210     *
211     * @param string $owner
212     * @param string $repository
213     *
214     * @return string
215     */
216    protected function getRepositoryApiUrl($owner = null, $repository = null)
217    {
218        $owner = null !== $owner ? $owner : $this->owner;
219        $repository = null !== $repository ? $repository : $this->repository;
220
221        return $this->getApiUrl().'/repos/'.$owner.'/'.$repository;
222    }
223
224    /**
225     * Get the remote content.
226     *
227     * @param string $url
228     *
229     * @return bool|string
230     */
231    protected function getRemoteContents($url)
232    {
233        return $this->remoteFilesystem->getContents($this->originUrl, $url, false);
234    }
235
236    /**
237     * Push the list of all branch.
238     *
239     * @param string $resource
240     * @param array  $branchBlacklist
241     */
242    protected function doAddBranches($resource, array $branchBlacklist)
243    {
244        do {
245            $branchData = JsonFile::parseJson((string) $this->getContents($resource), $resource);
246
247            foreach ($branchData as $branch) {
248                $name = substr($branch['ref'], 11);
249
250                if (!\in_array($name, $branchBlacklist, true)) {
251                    $this->branches[$name] = $branch['object']['sha'];
252                }
253            }
254
255            $resource = $this->getNextPage();
256        } while ($resource);
257    }
258}
259