UserController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <?php
  2. /*
  3. * This file is part of Packagist.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. * Nils Adermann <naderman@naderman.de>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Packagist\WebBundle\Controller;
  12. use Doctrine\ORM\NoResultException;
  13. use FOS\UserBundle\Model\UserInterface;
  14. use Packagist\WebBundle\Entity\Job;
  15. use Packagist\WebBundle\Entity\Package;
  16. use Packagist\WebBundle\Entity\Version;
  17. use Packagist\WebBundle\Entity\User;
  18. use Packagist\WebBundle\Entity\VersionRepository;
  19. use Packagist\WebBundle\Form\Model\EnableTwoFactorRequest;
  20. use Packagist\WebBundle\Form\Type\EnableTwoFactorAuthType;
  21. use Packagist\WebBundle\Model\RedisAdapter;
  22. use Packagist\WebBundle\Security\TwoFactorAuthManager;
  23. use Pagerfanta\Adapter\DoctrineORMAdapter;
  24. use Pagerfanta\Pagerfanta;
  25. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  26. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  27. use Symfony\Component\Form\FormError;
  28. use Symfony\Component\HttpFoundation\Request;
  29. use Symfony\Component\HttpFoundation\Response;
  30. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  31. use Symfony\Component\Routing\Annotation\Route;
  32. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  33. /**
  34. * @author Jordi Boggiano <j.boggiano@seld.be>
  35. */
  36. class UserController extends Controller
  37. {
  38. /**
  39. * @Template()
  40. * @Route("/users/{name}/packages/", name="user_packages")
  41. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  42. */
  43. public function packagesAction(Request $req, User $user)
  44. {
  45. $packages = $this->getUserPackages($req, $user);
  46. return array(
  47. 'packages' => $packages,
  48. 'meta' => $this->getPackagesMetadata($packages),
  49. 'user' => $user,
  50. );
  51. }
  52. /**
  53. * @Route("/trigger-github-sync/", name="user_github_sync")
  54. */
  55. public function triggerGitHubSyncAction()
  56. {
  57. $user = $this->getUser();
  58. if (!$user) {
  59. throw new AccessDeniedException();
  60. }
  61. if (!$user->getGithubToken()) {
  62. $this->get('session')->getFlashBag()->set('error', 'You must connect your user account to github to sync packages.');
  63. return $this->redirectToRoute('fos_user_profile_show');
  64. }
  65. if (!$user->getGithubScope()) {
  66. $this->get('session')->getFlashBag()->set('error', 'Please log out and log in with GitHub again to make sure the correct GitHub permissions are granted.');
  67. return $this->redirectToRoute('fos_user_profile_show');
  68. }
  69. $this->get('scheduler')->scheduleUserScopeMigration($user->getId(), '', $user->getGithubScope());
  70. sleep(5);
  71. $this->get('session')->getFlashBag()->set('success', 'User sync scheduled. It might take a few seconds to run through, make sure you refresh then to check if any packages still need sync.');
  72. return $this->redirectToRoute('fos_user_profile_show');
  73. }
  74. /**
  75. * @Route("/spammers/{name}/", name="mark_spammer", methods={"POST"})
  76. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  77. */
  78. public function markSpammerAction(Request $req, User $user)
  79. {
  80. if (!$this->isGranted('ROLE_ANTISPAM')) {
  81. throw new AccessDeniedException('This user can not mark others as spammers');
  82. }
  83. $form = $this->createFormBuilder(array())->getForm();
  84. $form->submit($req->request->get('form'));
  85. if ($form->isValid()) {
  86. $user->addRole('ROLE_SPAMMER');
  87. $user->setEnabled(false);
  88. $this->get('fos_user.user_manager')->updateUser($user);
  89. $doctrine = $this->getDoctrine();
  90. $doctrine->getConnection()->executeUpdate(
  91. 'UPDATE package p JOIN maintainers_packages mp ON mp.package_id = p.id
  92. SET abandoned = 1, replacementPackage = "spam/spam", suspect = "spam", indexedAt = NULL, dumpedAt = "2100-01-01 00:00:00"
  93. WHERE mp.user_id = :userId',
  94. ['userId' => $user->getId()]
  95. );
  96. /** @var VersionRepository $versionRepo */
  97. $versionRepo = $doctrine->getRepository(Version::class);
  98. $packages = $doctrine
  99. ->getRepository(Package::class)
  100. ->getFilteredQueryBuilder(array('maintainer' => $user->getId()), true)
  101. ->getQuery()->getResult();
  102. $providerManager = $this->get('packagist.provider_manager');
  103. foreach ($packages as $package) {
  104. foreach ($package->getVersions() as $version) {
  105. $versionRepo->remove($version);
  106. }
  107. $providerManager->deletePackage($package);
  108. }
  109. $this->getDoctrine()->getManager()->flush();
  110. $this->get('session')->getFlashBag()->set('success', $user->getUsername().' has been marked as a spammer');
  111. }
  112. return $this->redirect(
  113. $this->generateUrl("user_profile", array("name" => $user->getUsername()))
  114. );
  115. }
  116. /**
  117. * @param Request $req
  118. * @return Response
  119. */
  120. public function viewProfileAction(Request $req)
  121. {
  122. $user = $this->container->get('security.token_storage')->getToken()->getUser();
  123. if (!is_object($user) || !$user instanceof UserInterface) {
  124. throw new AccessDeniedException('This user does not have access to this section.');
  125. }
  126. $packages = $this->getUserPackages($req, $user);
  127. $lastGithubSync = $this->getDoctrine()->getRepository(Job::class)->getLastGitHubSyncJob($user->getId());
  128. return $this->container->get('templating')->renderResponse(
  129. 'FOSUserBundle:Profile:show.html.twig',
  130. array(
  131. 'packages' => $packages,
  132. 'meta' => $this->getPackagesMetadata($packages),
  133. 'user' => $user,
  134. 'githubSync' => $lastGithubSync,
  135. )
  136. );
  137. }
  138. /**
  139. * @Template()
  140. * @Route("/users/{name}/", name="user_profile")
  141. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  142. */
  143. public function profileAction(Request $req, User $user)
  144. {
  145. $packages = $this->getUserPackages($req, $user);
  146. $data = array(
  147. 'packages' => $packages,
  148. 'meta' => $this->getPackagesMetadata($packages),
  149. 'user' => $user,
  150. );
  151. if ($this->isGranted('ROLE_ANTISPAM')) {
  152. $data['spammerForm'] = $this->createFormBuilder(array())->getForm()->createView();
  153. }
  154. return $data;
  155. }
  156. /**
  157. * @Route("/oauth/github/disconnect", name="user_github_disconnect")
  158. */
  159. public function disconnectGitHubAction(Request $req)
  160. {
  161. $user = $this->getUser();
  162. $token = $this->get('security.csrf.token_manager')->getToken('unlink_github')->getValue();
  163. if (!hash_equals($token, $req->query->get('token', '')) || !$user) {
  164. throw new AccessDeniedException('Invalid CSRF token');
  165. }
  166. if ($user->getGithubId()) {
  167. $user->setGithubId(null);
  168. $user->setGithubToken(null);
  169. $user->setGithubScope(null);
  170. $this->getDoctrine()->getEntityManager()->flush();
  171. }
  172. return $this->redirectToRoute('fos_user_profile_edit');
  173. }
  174. /**
  175. * @Template()
  176. * @Route("/users/{name}/favorites/", name="user_favorites", methods={"GET"})
  177. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  178. */
  179. public function favoritesAction(Request $req, User $user)
  180. {
  181. try {
  182. if (!$this->get('snc_redis.default')->isConnected()) {
  183. $this->get('snc_redis.default')->connect();
  184. }
  185. } catch (\Exception $e) {
  186. $this->get('session')->getFlashBag()->set('error', 'Could not connect to the Redis database.');
  187. $this->get('logger')->notice($e->getMessage(), array('exception' => $e));
  188. return array('user' => $user, 'packages' => array());
  189. }
  190. $paginator = new Pagerfanta(
  191. new RedisAdapter($this->get('packagist.favorite_manager'), $user, 'getFavorites', 'getFavoriteCount')
  192. );
  193. $paginator->setMaxPerPage(15);
  194. $paginator->setCurrentPage(max(1, (int) $req->query->get('page', 1)), false, true);
  195. return array('packages' => $paginator, 'user' => $user);
  196. }
  197. /**
  198. * @Route("/users/{name}/favorites/", name="user_add_fav", defaults={"_format" = "json"}, methods={"POST"})
  199. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  200. */
  201. public function postFavoriteAction(Request $req, User $user)
  202. {
  203. if ($user->getId() !== $this->getUser()->getId()) {
  204. throw new AccessDeniedException('You can only change your own favorites');
  205. }
  206. $package = $req->request->get('package');
  207. try {
  208. $package = $this->getDoctrine()
  209. ->getRepository(Package::class)
  210. ->findOneByName($package);
  211. } catch (NoResultException $e) {
  212. throw new NotFoundHttpException('The given package "'.$package.'" was not found.');
  213. }
  214. $this->get('packagist.favorite_manager')->markFavorite($user, $package);
  215. return new Response('{"status": "success"}', 201);
  216. }
  217. /**
  218. * @Route("/users/{name}/favorites/{package}", name="user_remove_fav", defaults={"_format" = "json"}, requirements={"package"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}, methods={"DELETE"})
  219. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  220. * @ParamConverter("package", options={"mapping": {"package": "name"}})
  221. */
  222. public function deleteFavoriteAction(User $user, Package $package)
  223. {
  224. if ($user->getId() !== $this->getUser()->getId()) {
  225. throw new AccessDeniedException('You can only change your own favorites');
  226. }
  227. $this->get('packagist.favorite_manager')->removeFavorite($user, $package);
  228. return new Response('{"status": "success"}', 204);
  229. }
  230. /**
  231. * @Template()
  232. * @Route("/users/{name}/2fa/", name="user_2fa_configure", methods={"GET"})
  233. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  234. */
  235. public function configureTwoFactorAuthAction(User $user)
  236. {
  237. if ($user->getId() !== $this->getUser()->getId()) {
  238. throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
  239. }
  240. return array('user' => $user);
  241. }
  242. /**
  243. * @Template()
  244. * @Route("/users/{name}/2fa/enable", name="user_2fa_enable", methods={"GET", "POST"})
  245. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  246. */
  247. public function enableTwoFactorAuthAction(Request $req, User $user)
  248. {
  249. if ($user->getId() !== $this->getUser()->getId()) {
  250. throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
  251. }
  252. $authenticator = $this->get("scheb_two_factor.security.totp_authenticator");
  253. $enableRequest = new EnableTwoFactorRequest($authenticator->generateSecret());
  254. $form = $this->createForm(EnableTwoFactorAuthType::class, $enableRequest);
  255. $form->handleRequest($req);
  256. // Temporarily store this code on the user, as we'll need it there to generate the
  257. // QR code and to check the confirmation code. We won't actually save this change
  258. // until we've confirmed the code
  259. $user->setTotpSecret($enableRequest->getSecret());
  260. if ($form->isSubmitted()) {
  261. // Validate the code using the secret that was submitted in the form
  262. if (!$authenticator->checkCode($user, $enableRequest->getCode())) {
  263. $form->get('code')->addError(new FormError('Invalid authenticator code'));
  264. }
  265. if ($form->isValid()) {
  266. $this->get(TwoFactorAuthManager::class)->enableTwoFactorAuth($user, $enableRequest->getSecret());
  267. $this->addFlash('success', 'Two-factor authentication has been enabled.');
  268. return $this->redirectToRoute('user_2fa_configure', array('name' => $user->getUsername()));
  269. }
  270. }
  271. return array('user' => $user, 'provisioningUri' => $authenticator->getQRContent($user), 'form' => $form->createView());
  272. }
  273. /**
  274. * @Template()
  275. * @Route("/users/{name}/2fa/disable", name="user_2fa_disable", methods={"GET"})
  276. * @ParamConverter("user", options={"mapping": {"name": "username"}})
  277. */
  278. public function disableTwoFactorAuthAction(Request $req, User $user)
  279. {
  280. if ($user->getId() !== $this->getUser()->getId()) {
  281. throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
  282. }
  283. $token = $this->get('security.csrf.token_manager')->getToken('disable_2fa')->getValue();
  284. if (hash_equals($token, $req->query->get('token', ''))) {
  285. $this->get(TwoFactorAuthManager::class)->disableTwoFactorAuth($user);
  286. $this->addFlash('success', 'Two-factor authentication has been disabled.');
  287. return $this->redirectToRoute('user_2fa_configure', array('name' => $user->getUsername()));
  288. }
  289. return array('user' => $user);
  290. }
  291. /**
  292. * @param Request $req
  293. * @param User $user
  294. * @return Pagerfanta
  295. */
  296. protected function getUserPackages($req, $user)
  297. {
  298. $packages = $this->getDoctrine()
  299. ->getRepository(Package::class)
  300. ->getFilteredQueryBuilder(array('maintainer' => $user->getId()), true);
  301. $paginator = new Pagerfanta(new DoctrineORMAdapter($packages, true));
  302. $paginator->setMaxPerPage(15);
  303. $paginator->setCurrentPage(max(1, (int) $req->query->get('page', 1)), false, true);
  304. return $paginator;
  305. }
  306. }