1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of tenside/core-bundle. |
5
|
|
|
* |
6
|
|
|
* (c) Christian Schiffler <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
* |
11
|
|
|
* This project is provided in good faith and hope to be usable by anyone. |
12
|
|
|
* |
13
|
|
|
* @package tenside/core-bundle |
14
|
|
|
* @author Christian Schiffler <[email protected]> |
15
|
|
|
* @author Andreas Schempp <[email protected]> |
16
|
|
|
* @copyright 2015 Christian Schiffler <[email protected]> |
17
|
|
|
* @license https://github.com/tenside/core-bundle/blob/master/LICENSE MIT |
18
|
|
|
* @link https://github.com/tenside/core-bundle |
19
|
|
|
* @filesource |
20
|
|
|
*/ |
21
|
|
|
|
22
|
|
|
namespace Tenside\CoreBundle\Controller; |
23
|
|
|
|
24
|
|
|
use Composer\Composer; |
25
|
|
|
use Composer\Package\PackageInterface; |
26
|
|
|
use Composer\Repository\CompositeRepository; |
27
|
|
|
use Composer\Repository\RepositoryInterface; |
28
|
|
|
use Nelmio\ApiDocBundle\Annotation\ApiDoc; |
29
|
|
|
use Symfony\Component\HttpFoundation\JsonResponse; |
30
|
|
|
use Symfony\Component\HttpFoundation\Request; |
31
|
|
|
use Tenside\Core\Composer\Package\VersionedPackage; |
32
|
|
|
use Tenside\Core\Composer\PackageConverter; |
33
|
|
|
use Tenside\Core\Composer\Search\CompositeSearch; |
34
|
|
|
use Tenside\Core\Composer\Search\RepositorySearch; |
35
|
|
|
use Tenside\Core\Util\JsonArray; |
36
|
|
|
use Tenside\CoreBundle\Annotation\ApiDescription; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* List and manipulate the installed packages. |
40
|
|
|
*/ |
41
|
|
|
class SearchPackageController extends AbstractController |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* Search for packages. |
45
|
|
|
* |
46
|
|
|
* @param Request $request The search request. |
47
|
|
|
* |
48
|
|
|
* @return JsonResponse |
49
|
|
|
* |
50
|
|
|
* @ApiDoc( |
51
|
|
|
* section="search", |
52
|
|
|
* statusCodes = { |
53
|
|
|
* 200 = "When everything worked out ok" |
54
|
|
|
* }, |
55
|
|
|
* authentication = true, |
56
|
|
|
* authenticationRoles = { |
57
|
|
|
* "ROLE_MANIPULATE_REQUIREMENTS" |
58
|
|
|
* } |
59
|
|
|
* ) |
60
|
|
|
* @ApiDescription( |
61
|
|
|
* request={ |
62
|
|
|
* "keywords" = { |
63
|
|
|
* "dataType" = "string", |
64
|
|
|
* "description" = "The name of the project to search or any other keyword.", |
65
|
|
|
* "required" = true |
66
|
|
|
* }, |
67
|
|
|
* "type" = { |
68
|
|
|
* "dataType" = "choice", |
69
|
|
|
* "description" = "The type of package to search (optional, default: all).", |
70
|
|
|
* "format" = "['installed', 'contao', 'all']", |
71
|
|
|
* "required" = false |
72
|
|
|
* }, |
73
|
|
|
* "threshold" = { |
74
|
|
|
* "dataType" = "int", |
75
|
|
|
* "description" = "The amount of results after which the search shall be stopped (optional, default: 20).", |
76
|
|
|
* "required" = false |
77
|
|
|
* } |
78
|
|
|
* }, |
79
|
|
|
* response={ |
80
|
|
|
* "package name 1...n" = { |
81
|
|
|
* "actualType" = "object", |
82
|
|
|
* "subType" = "object", |
83
|
|
|
* "description" = "The content of the packages", |
84
|
|
|
* "children" = { |
85
|
|
|
* "name" = { |
86
|
|
|
* "dataType" = "string", |
87
|
|
|
* "description" = "The name of the package" |
88
|
|
|
* }, |
89
|
|
|
* "version" = { |
90
|
|
|
* "dataType" = "string", |
91
|
|
|
* "description" = "The version of the package" |
92
|
|
|
* }, |
93
|
|
|
* "constraint" = { |
94
|
|
|
* "dataType" = "string", |
95
|
|
|
* "description" = "The constraint of the package (when package is installed)" |
96
|
|
|
* }, |
97
|
|
|
* "type" = { |
98
|
|
|
* "dataType" = "string", |
99
|
|
|
* "description" = "The noted package type" |
100
|
|
|
* }, |
101
|
|
|
* "locked" = { |
102
|
|
|
* "dataType" = "string", |
103
|
|
|
* "description" = "Flag if the package has been locked for updates" |
104
|
|
|
* }, |
105
|
|
|
* "time" = { |
106
|
|
|
* "dataType" = "datetime", |
107
|
|
|
* "description" = "The release date" |
108
|
|
|
* }, |
109
|
|
|
* "upgrade_version" = { |
110
|
|
|
* "dataType" = "string", |
111
|
|
|
* "description" = "The version available for upgrade (optional, if any)" |
112
|
|
|
* }, |
113
|
|
|
* "description" = { |
114
|
|
|
* "dataType" = "string", |
115
|
|
|
* "description" = "The package description" |
116
|
|
|
* }, |
117
|
|
|
* "license" = { |
118
|
|
|
* "actualType" = "collection", |
119
|
|
|
* "subType" = "string", |
120
|
|
|
* "description" = "The licenses" |
121
|
|
|
* }, |
122
|
|
|
* "keywords" = { |
123
|
|
|
* "actualType" = "collection", |
124
|
|
|
* "subType" = "string", |
125
|
|
|
* "description" = "The keywords" |
126
|
|
|
* }, |
127
|
|
|
* "homepage" = { |
128
|
|
|
* "dataType" = "string", |
129
|
|
|
* "description" = "The support website (optional, if any)" |
130
|
|
|
* }, |
131
|
|
|
* "authors" = { |
132
|
|
|
* "actualType" = "collection", |
133
|
|
|
* "subType" = "object", |
134
|
|
|
* "description" = "The authors", |
135
|
|
|
* "children" = { |
136
|
|
|
* "name" = { |
137
|
|
|
* "dataType" = "string", |
138
|
|
|
* "description" = "Full name of the author (optional, if any)" |
139
|
|
|
* }, |
140
|
|
|
* "homepage" = { |
141
|
|
|
* "dataType" = "string", |
142
|
|
|
* "description" = "Email address of the author (optional, if any)" |
143
|
|
|
* }, |
144
|
|
|
* "email" = { |
145
|
|
|
* "dataType" = "string", |
146
|
|
|
* "description" = "Homepage URL for the author (optional, if any)" |
147
|
|
|
* }, |
148
|
|
|
* "role" = { |
149
|
|
|
* "dataType" = "string", |
150
|
|
|
* "description" = "Author's role in the project (optional, if any)" |
151
|
|
|
* } |
152
|
|
|
* } |
153
|
|
|
* }, |
154
|
|
|
* "support" = { |
155
|
|
|
* "actualType" = "collection", |
156
|
|
|
* "subType" = "object", |
157
|
|
|
* "description" = "The support options", |
158
|
|
|
* "children" = { |
159
|
|
|
* "email" = { |
160
|
|
|
* "dataType" = "string", |
161
|
|
|
* "description" = "Email address for support (optional, if any)" |
162
|
|
|
* }, |
163
|
|
|
* "issues" = { |
164
|
|
|
* "dataType" = "string", |
165
|
|
|
* "description" = "URL to the issue tracker (optional, if any)" |
166
|
|
|
* }, |
167
|
|
|
* "forum" = { |
168
|
|
|
* "dataType" = "string", |
169
|
|
|
* "description" = "URL to the forum (optional, if any)" |
170
|
|
|
* }, |
171
|
|
|
* "wiki" = { |
172
|
|
|
* "dataType" = "string", |
173
|
|
|
* "description" = "URL to the wiki (optional, if any)" |
174
|
|
|
* }, |
175
|
|
|
* "irc" = { |
176
|
|
|
* "dataType" = "string", |
177
|
|
|
* "description" = "IRC channel for support, as irc://server/channel (optional, if any)" |
178
|
|
|
* }, |
179
|
|
|
* "source" = { |
180
|
|
|
* "dataType" = "string", |
181
|
|
|
* "description" = "URL to browse or download the sources (optional, if any)" |
182
|
|
|
* }, |
183
|
|
|
* "docs" = { |
184
|
|
|
* "dataType" = "string", |
185
|
|
|
* "description" = "URL to the documentation (optional, if any)" |
186
|
|
|
* }, |
187
|
|
|
* } |
188
|
|
|
* }, |
189
|
|
|
* "abandoned" = { |
190
|
|
|
* "dataType" = "boolean", |
191
|
|
|
* "description" = "Flag if this package is abandoned" |
192
|
|
|
* }, |
193
|
|
|
* "replacement" = { |
194
|
|
|
* "dataType" = "string", |
195
|
|
|
* "description" = "Replacement for this package (optional, if any)" |
196
|
|
|
* }, |
197
|
|
|
* "installed" = { |
198
|
|
|
* "dataType" = "int", |
199
|
|
|
* "description" = "Amount of installations" |
200
|
|
|
* }, |
201
|
|
|
* "downloads" = { |
202
|
|
|
* "dataType" = "int", |
203
|
|
|
* "description" = "Amount of downloads" |
204
|
|
|
* }, |
205
|
|
|
* "favers" = { |
206
|
|
|
* "dataType" = "int", |
207
|
|
|
* "description" = "Amount of favers" |
208
|
|
|
* }, |
209
|
|
|
* } |
210
|
|
|
* } |
211
|
|
|
* } |
212
|
|
|
* ) |
213
|
|
|
*/ |
214
|
|
|
public function searchAction(Request $request) |
215
|
|
|
{ |
216
|
|
|
$composer = $this->getComposer(); |
217
|
|
|
$data = new JsonArray($request->getContent()); |
|
|
|
|
218
|
|
|
$keywords = $data->get('keywords'); |
219
|
|
|
$type = $data->get('type'); |
220
|
|
|
$threshold = $data->has('threshold') ? $data->get('threshold') : 20; |
221
|
|
|
$localRepository = $composer->getRepositoryManager()->getLocalRepository(); |
222
|
|
|
$searcher = $this->getRepositorySearch($keywords, $type, $composer, $threshold); |
223
|
|
|
$results = $searcher->searchAndDecorate($keywords, $this->getFilters($type)); |
224
|
|
|
$responseData = []; |
225
|
|
|
$rootPackage = $composer->getPackage(); |
226
|
|
|
$converter = new PackageConverter($rootPackage); |
227
|
|
|
|
228
|
|
|
foreach ($results as $versionedResult) { |
229
|
|
|
/** @var VersionedPackage $versionedResult */ |
230
|
|
|
|
231
|
|
|
// Might have no version matching the current stability setting. |
232
|
|
|
if (null === ($latestVersion = $versionedResult->getLatestVersion())) { |
233
|
|
|
continue; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$package = $converter->convertPackageToArray($latestVersion); |
237
|
|
|
$package |
238
|
|
|
->set('installed', $this->getInstalledVersion($localRepository, $versionedResult->getName())) |
239
|
|
|
->set('downloads', $versionedResult->getMetaData('downloads')) |
240
|
|
|
->set('favers', $versionedResult->getMetaData('favers')); |
241
|
|
|
|
242
|
|
|
$responseData[$package->get('name')] = $package->getData(); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return JsonResponse::create($responseData) |
246
|
|
|
->setEncodingOptions((JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT)); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Get the array of filter closures. |
251
|
|
|
* |
252
|
|
|
* @param string $type The desired search type (contao, installed or empty). |
253
|
|
|
* |
254
|
|
|
* @return \Closure[] |
255
|
|
|
*/ |
256
|
|
|
private function getFilters($type) |
257
|
|
|
{ |
258
|
|
|
$filters = []; |
259
|
|
|
if ('contao' === $type) { |
260
|
|
|
$filters[] = |
261
|
|
|
function ($package) { |
262
|
|
|
/** @var PackageInterface $package */ |
263
|
|
|
return in_array($package->getType(), ['contao-module', 'contao-bundle', 'legacy-contao-module']); |
264
|
|
|
}; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
return $filters; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Retrieve the installed version of a package (if any). |
272
|
|
|
* |
273
|
|
|
* @param RepositoryInterface $localRepository The local repository. |
274
|
|
|
* |
275
|
|
|
* @param string $packageName The name of the package to search. |
276
|
|
|
* |
277
|
|
|
* @return null|string |
278
|
|
|
*/ |
279
|
|
|
private function getInstalledVersion($localRepository, $packageName) |
280
|
|
|
{ |
281
|
|
|
if (count($installed = $localRepository->findPackages($packageName))) { |
282
|
|
|
/** @var PackageInterface[] $installed */ |
283
|
|
|
return $installed[0]->getPrettyVersion(); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return null; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Create a repository search instance. |
291
|
|
|
* |
292
|
|
|
* @param string $keywords The search keywords. |
293
|
|
|
* |
294
|
|
|
* @param string $type The desired search type. |
295
|
|
|
* |
296
|
|
|
* @param Composer $composer The composer instance. |
297
|
|
|
* |
298
|
|
|
* @param int $threshold The threshold after which to stop searching. |
299
|
|
|
* |
300
|
|
|
* @return CompositeSearch |
301
|
|
|
*/ |
302
|
|
|
private function getRepositorySearch($keywords, $type, Composer $composer, $threshold) |
303
|
|
|
{ |
304
|
|
|
$repositoryManager = $composer->getRepositoryManager(); |
305
|
|
|
$localRepository = $repositoryManager->getLocalRepository(); |
306
|
|
|
|
307
|
|
|
$repositories = new CompositeRepository([$localRepository]); |
308
|
|
|
|
309
|
|
|
// If we do not search locally, add the other repositories now. |
310
|
|
|
if ('installed' !== $type) { |
311
|
|
|
$repositories->addRepository(new CompositeRepository($repositoryManager->getRepositories())); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$repositorySearch = new RepositorySearch($repositories); |
315
|
|
|
$repositorySearch->setSatisfactionThreshold($threshold); |
316
|
|
|
if (false !== strpos($keywords, '/')) { |
317
|
|
|
$repositorySearch->disableSearchType(RepositoryInterface::SEARCH_FULLTEXT); |
318
|
|
|
} else { |
319
|
|
|
$repositorySearch->disableSearchType(RepositoryInterface::SEARCH_NAME); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
$searcher = new CompositeSearch( |
323
|
|
|
[ |
324
|
|
|
$repositorySearch |
325
|
|
|
] |
326
|
|
|
); |
327
|
|
|
$searcher->setSatisfactionThreshold($threshold); |
328
|
|
|
|
329
|
|
|
return $searcher; |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.