PackageController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <?php
  2. namespace Packagist\WebBundle\Controller;
  3. use Packagist\WebBundle\Form\Type\AbandonedType;
  4. use Packagist\WebBundle\Entity\Package;
  5. use Packagist\WebBundle\Entity\Version;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\JsonResponse;
  8. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  9. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  10. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  11. use Composer\Package\Version\VersionParser;
  12. use DateTimeImmutable;
  13. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  14. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  15. use Pagerfanta\Adapter\FixedAdapter;
  16. use Pagerfanta\Pagerfanta;
  17. class PackageController extends Controller
  18. {
  19. /**
  20. * @Template()
  21. * @Route(
  22. * "/packages/{name}/edit",
  23. * name="edit_package",
  24. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}
  25. * )
  26. */
  27. public function editAction(Request $req, Package $package)
  28. {
  29. if (!$package->getMaintainers()->contains($this->getUser()) && !$this->isGranted('ROLE_EDIT_PACKAGES')) {
  30. throw new AccessDeniedException;
  31. }
  32. $form = $this->createFormBuilder($package, array("validation_groups" => array("Update")))
  33. ->add("repository", "text")
  34. ->getForm();
  35. if ($req->isMethod("POST")) {
  36. $form->bind($req);
  37. if ($form->isValid()) {
  38. // Force updating of packages once the package is viewed after the redirect.
  39. $package->setCrawledAt(null);
  40. $em = $this->getDoctrine()->getManager();
  41. $em->persist($package);
  42. $em->flush();
  43. $this->get("session")->getFlashBag()->set("success", "Changes saved.");
  44. return $this->redirect(
  45. $this->generateUrl("view_package", array("name" => $package->getName()))
  46. );
  47. }
  48. }
  49. return array(
  50. "package" => $package, "form" => $form->createView()
  51. );
  52. }
  53. /**
  54. * @Route(
  55. * "/packages/{name}/abandon",
  56. * name="abandon_package",
  57. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}
  58. * )
  59. * @Template()
  60. */
  61. public function abandonAction(Request $request, Package $package)
  62. {
  63. if (!$package->getMaintainers()->contains($this->getUser()) && !$this->isGranted('ROLE_EDIT_PACKAGES')) {
  64. throw new AccessDeniedException;
  65. }
  66. $form = $this->createForm(new AbandonedType());
  67. if ($request->getMethod() === 'POST') {
  68. $form->bind($request->request->get('package'));
  69. if ($form->isValid()) {
  70. $package->setAbandoned(true);
  71. $package->setReplacementPackage(str_replace('https://packagist.org/packages/', '', $form->get('replacement')->getData()));
  72. $package->setIndexedAt(null);
  73. $em = $this->getDoctrine()->getManager();
  74. $em->flush();
  75. return $this->redirect($this->generateUrl('view_package', array('name' => $package->getName())));
  76. }
  77. }
  78. return array(
  79. 'package' => $package,
  80. 'form' => $form->createView()
  81. );
  82. }
  83. /**
  84. * @Route(
  85. * "/packages/{name}/unabandon",
  86. * name="unabandon_package",
  87. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}
  88. * )
  89. */
  90. public function unabandonAction(Package $package)
  91. {
  92. if (!$package->getMaintainers()->contains($this->getUser()) && !$this->isGranted('ROLE_EDIT_PACKAGES')) {
  93. throw new AccessDeniedException;
  94. }
  95. $package->setAbandoned(false);
  96. $package->setReplacementPackage(null);
  97. $package->setIndexedAt(null);
  98. $em = $this->getDoctrine()->getManager();
  99. $em->flush();
  100. return $this->redirect($this->generateUrl('view_package', array('name' => $package->getName())));
  101. }
  102. /**
  103. * @Route(
  104. * "/packages/{name}/stats.{_format}",
  105. * name="view_package_stats",
  106. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?", "_format"="(json)"},
  107. * defaults={"_format"="html"}
  108. * )
  109. * @Template()
  110. */
  111. public function statsAction(Request $req, Package $package)
  112. {
  113. $versions = $package->getVersions()->toArray();
  114. usort($versions, Package::class.'::sortVersions');
  115. $date = $this->guessStatsStartDate($package);
  116. $data = [
  117. 'downloads' => $this->get('packagist.download_manager')->getDownloads($package),
  118. 'versions' => $versions,
  119. 'average' => $this->guessStatsAverage($date),
  120. 'date' => $date->format('Y-m-d'),
  121. ];
  122. if ($req->getRequestFormat() === 'json') {
  123. $data['versions'] = array_map(function ($version) {
  124. return $version->getVersion();
  125. }, $data['versions']);
  126. return new JsonResponse($data);
  127. }
  128. $data['package'] = $package;
  129. $expandedVersion = reset($versions);
  130. foreach ($versions as $v) {
  131. if (!$v->isDevelopment()) {
  132. $expandedVersion = $v;
  133. break;
  134. }
  135. }
  136. $data['expandedId'] = $expandedVersion ? $expandedVersion->getId() : false;
  137. return $data;
  138. }
  139. /**
  140. * @Route(
  141. * "/packages/{name}/dependents",
  142. * name="view_package_dependents",
  143. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}
  144. * )
  145. * @Template()
  146. */
  147. public function dependentsAction(Request $req, $name)
  148. {
  149. $page = $req->query->get('page', 1);
  150. $repo = $this->getDoctrine()->getRepository('PackagistWebBundle:Package');
  151. $depCount = $repo->getDependentCount($name);
  152. $packages = $repo->getDependents($name, ($page - 1) * 15, 15);
  153. $paginator = new Pagerfanta(new FixedAdapter($depCount, $packages));
  154. $paginator->setMaxPerPage(15);
  155. $paginator->setCurrentPage($page, false, true);
  156. $data['packages'] = $paginator;
  157. $data['meta'] = $this->getPackagesMetadata($data['packages']);
  158. $data['name'] = $name;
  159. return $data;
  160. }
  161. /**
  162. * @Route(
  163. * "/packages/{name}/stats/all.json",
  164. * name="package_stats",
  165. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}
  166. * )
  167. */
  168. public function overallStatsAction(Request $req, Package $package, Version $version = null)
  169. {
  170. if ($from = $req->query->get('from')) {
  171. $from = new DateTimeImmutable($from);
  172. } else {
  173. $from = $this->guessStatsStartDate($version ?: $package);
  174. }
  175. if ($to = $req->query->get('to')) {
  176. $to = new DateTimeImmutable($to);
  177. } else {
  178. $to = new DateTimeImmutable('-2days 00:00:00');
  179. }
  180. $average = $req->query->get('average', $this->guessStatsAverage($from, $to));
  181. $datePoints = $this->createDatePoints($from, $to, $average, $package, $version);
  182. $redis = $this->get('snc_redis.default');
  183. if ($average === 'Daily') {
  184. $datePoints = array_map(function ($vals) {
  185. return $vals[0];
  186. }, $datePoints);
  187. $datePoints = array(
  188. 'labels' => array_keys($datePoints),
  189. 'values' => $redis->mget(array_values($datePoints))
  190. );
  191. } else {
  192. $datePoints = array(
  193. 'labels' => array_keys($datePoints),
  194. 'values' => array_values(array_map(function ($vals) use ($redis) {
  195. return array_sum($redis->mget(array_values($vals)));
  196. }, $datePoints))
  197. );
  198. }
  199. $datePoints['average'] = $average;
  200. if ($average !== 'daily') {
  201. $dividers = [
  202. 'monthly' => 30.41,
  203. 'weekly' => 7,
  204. ];
  205. $divider = $dividers[$average];
  206. $datePoints['values'] = array_map(function ($val) use ($divider) {
  207. return ceil($val / $divider);
  208. }, $datePoints['values']);
  209. }
  210. if (empty($datePoints['labels']) && empty($datePoints['values'])) {
  211. $datePoints['labels'][] = date('Y-m-d');
  212. $datePoints['values'][] = 0;
  213. }
  214. $response = new JsonResponse($datePoints);
  215. $response->setSharedMaxAge(1800);
  216. return $response;
  217. }
  218. /**
  219. * @Route(
  220. * "/packages/{name}/stats/{version}.json",
  221. * name="version_stats",
  222. * requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?", "version"=".+?"}
  223. * )
  224. */
  225. public function versionStatsAction(Request $req, Package $package, $version)
  226. {
  227. $normalizer = new VersionParser;
  228. $normVersion = $normalizer->normalize($version);
  229. $version = $this->getDoctrine()->getRepository('PackagistWebBundle:Version')->findOneBy([
  230. 'package' => $package,
  231. 'normalizedVersion' => $normVersion
  232. ]);
  233. if (!$version) {
  234. throw new NotFoundHttpException();
  235. }
  236. return $this->overallStatsAction($req, $package, $version);
  237. }
  238. private function createDatePoints(DateTimeImmutable $from, DateTimeImmutable $to, $average, Package $package, Version $version = null)
  239. {
  240. $interval = $this->getStatsInterval($average);
  241. $dateKey = $average === 'monthly' ? 'Ym' : 'Ymd';
  242. $dateFormat = $average === 'monthly' ? 'Y-m' : 'Y-m-d';
  243. $dateJump = $average === 'monthly' ? '+1month' : '+1day';
  244. if ($average === 'monthly') {
  245. $to = new DateTimeImmutable('last day of '.$to->format('Y-m'));
  246. }
  247. $nextDataPointLabel = $from->format($dateFormat);
  248. $nextDataPoint = $from->modify($interval);
  249. $datePoints = [];
  250. while ($from <= $to) {
  251. $datePoints[$nextDataPointLabel][] = 'dl:'.$package->getId().($version ? '-' . $version->getId() : '').':'.$from->format($dateKey);
  252. $from = $from->modify($dateJump);
  253. if ($from >= $nextDataPoint) {
  254. $nextDataPointLabel = $from->format($dateFormat);
  255. $nextDataPoint = $from->modify($interval);
  256. }
  257. }
  258. return $datePoints;
  259. }
  260. private function guessStatsStartDate($packageOrVersion)
  261. {
  262. if ($packageOrVersion instanceof Package) {
  263. $date = DateTimeImmutable::createFromMutable($packageOrVersion->getCreatedAt());
  264. } elseif ($packageOrVersion instanceof Version) {
  265. $date = DateTimeImmutable::createFromMutable($packageOrVersion->getReleasedAt());
  266. } else {
  267. throw new \LogicException('Version or Package expected');
  268. }
  269. $statsRecordDate = new DateTimeImmutable('2012-04-13 00:00:00');
  270. if ($date < $statsRecordDate) {
  271. $date = $statsRecordDate;
  272. }
  273. return $date->setTime(0, 0, 0);
  274. }
  275. private function guessStatsAverage(DateTimeImmutable $from, DateTimeImmutable $to = null)
  276. {
  277. if ($to === null) {
  278. $to = new DateTimeImmutable('-2 days');
  279. }
  280. if ($from < $to->modify('-48months')) {
  281. $average = 'monthly';
  282. } elseif ($from < $to->modify('-7months')) {
  283. $average = 'weekly';
  284. } else {
  285. $average = 'daily';
  286. }
  287. return $average;
  288. }
  289. private function getStatsInterval($average)
  290. {
  291. $intervals = [
  292. 'monthly' => '+1month',
  293. 'weekly' => '+7days',
  294. 'daily' => '+1day',
  295. ];
  296. if (!isset($intervals[$average])) {
  297. throw new BadRequestHttpException();
  298. }
  299. return $intervals[$average];
  300. }
  301. }