CRUDController.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377
  1. <?php
  2. /*
  3. * This file is part of the Sonata Project package.
  4. *
  5. * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Sonata\AdminBundle\Controller;
  11. use Psr\Log\NullLogger;
  12. use Sonata\AdminBundle\Admin\AdminInterface;
  13. use Sonata\AdminBundle\Admin\BaseFieldDescription;
  14. use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
  15. use Sonata\AdminBundle\Exception\LockException;
  16. use Sonata\AdminBundle\Exception\ModelManagerException;
  17. use Sonata\AdminBundle\Util\AdminObjectAclData;
  18. use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
  19. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  20. use Symfony\Component\DependencyInjection\ContainerInterface;
  21. use Symfony\Component\HttpFoundation\JsonResponse;
  22. use Symfony\Component\HttpFoundation\RedirectResponse;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\HttpFoundation\Response;
  25. use Symfony\Component\HttpKernel\Exception\HttpException;
  26. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  27. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  28. /**
  29. * Class CRUDController.
  30. *
  31. * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  32. */
  33. class CRUDController extends Controller
  34. {
  35. /**
  36. * The related Admin class.
  37. *
  38. * @var AdminInterface
  39. */
  40. protected $admin;
  41. /**
  42. * Render JSON.
  43. *
  44. * @param mixed $data
  45. * @param int $status
  46. * @param array $headers
  47. * @param Request $request
  48. *
  49. * @return Response with json encoded data
  50. */
  51. protected function renderJson($data, $status = 200, $headers = array(), Request $request = null)
  52. {
  53. return new JsonResponse($data, $status, $headers);
  54. }
  55. /**
  56. * Returns true if the request is a XMLHttpRequest.
  57. *
  58. * @param Request $request
  59. *
  60. * @return bool True if the request is an XMLHttpRequest, false otherwise
  61. */
  62. protected function isXmlHttpRequest(Request $request = null)
  63. {
  64. $request = $this->resolveRequest($request);
  65. return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
  66. }
  67. /**
  68. * Returns the correct RESTful verb, given either by the request itself or
  69. * via the "_method" parameter.
  70. *
  71. * @param Request $request
  72. *
  73. * @return string HTTP method, either
  74. */
  75. protected function getRestMethod(Request $request = null)
  76. {
  77. $request = $this->resolveRequest($request);
  78. if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
  79. return $request->getMethod();
  80. }
  81. return $request->request->get('_method');
  82. }
  83. /**
  84. * Sets the Container associated with this Controller.
  85. *
  86. * @param ContainerInterface $container A ContainerInterface instance
  87. */
  88. public function setContainer(ContainerInterface $container = null)
  89. {
  90. $this->container = $container;
  91. $this->configure();
  92. }
  93. /**
  94. * Contextualize the admin class depends on the current request.
  95. *
  96. * @throws \RuntimeException
  97. */
  98. protected function configure()
  99. {
  100. $adminCode = $this->container->get('request')->get('_sonata_admin');
  101. if (!$adminCode) {
  102. throw new \RuntimeException(sprintf(
  103. 'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
  104. get_class($this),
  105. $this->container->get('request')->get('_route')
  106. ));
  107. }
  108. $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
  109. if (!$this->admin) {
  110. throw new \RuntimeException(sprintf(
  111. 'Unable to find the admin class related to the current controller (%s)',
  112. get_class($this)
  113. ));
  114. }
  115. $rootAdmin = $this->admin;
  116. if ($this->admin->isChild()) {
  117. $this->admin->setCurrentChild(true);
  118. $rootAdmin = $rootAdmin->getParent();
  119. }
  120. $request = $this->container->get('request');
  121. $rootAdmin->setRequest($request);
  122. if ($request->get('uniqid')) {
  123. $this->admin->setUniqid($request->get('uniqid'));
  124. }
  125. }
  126. /**
  127. * Proxy for the logger service of the container.
  128. * If no such service is found, a NullLogger is returned.
  129. *
  130. * @return \Psr\Log\LoggerInterface
  131. */
  132. protected function getLogger()
  133. {
  134. if ($this->container->has('logger')) {
  135. return $this->container->get('logger');
  136. } else {
  137. return new NullLogger();
  138. }
  139. }
  140. /**
  141. * Returns the base template name.
  142. *
  143. * @param Request $request
  144. *
  145. * @return string The template name
  146. */
  147. protected function getBaseTemplate(Request $request = null)
  148. {
  149. $request = $this->resolveRequest($request);
  150. if ($this->isXmlHttpRequest($request)) {
  151. return $this->admin->getTemplate('ajax');
  152. }
  153. return $this->admin->getTemplate('layout');
  154. }
  155. /**
  156. * {@inheritdoc}
  157. *
  158. * @param Request $request
  159. */
  160. public function render($view, array $parameters = array(), Response $response = null, Request $request = null)
  161. {
  162. $request = $this->resolveRequest($request);
  163. $parameters['admin'] = isset($parameters['admin']) ?
  164. $parameters['admin'] :
  165. $this->admin;
  166. $parameters['base_template'] = isset($parameters['base_template']) ?
  167. $parameters['base_template'] :
  168. $this->getBaseTemplate($request);
  169. $parameters['admin_pool'] = $this->get('sonata.admin.pool');
  170. return parent::render($view, $parameters, $response);
  171. }
  172. /**
  173. * @param \Exception $e
  174. *
  175. * @throws \Exception
  176. */
  177. protected function handleModelManagerException(\Exception $e)
  178. {
  179. if ($this->get('kernel')->isDebug()) {
  180. throw $e;
  181. }
  182. $context = array('exception' => $e);
  183. if ($e->getPrevious()) {
  184. $context['previous_exception_message'] = $e->getPrevious()->getMessage();
  185. }
  186. $this->getLogger()->error($e->getMessage(), $context);
  187. }
  188. /**
  189. * List action.
  190. *
  191. * @param Request $request
  192. *
  193. * @return Response
  194. *
  195. * @throws AccessDeniedException If access is not granted
  196. */
  197. public function listAction(Request $request = null)
  198. {
  199. $request = $this->resolveRequest($request);
  200. if (false === $this->admin->isGranted('LIST')) {
  201. throw new AccessDeniedException();
  202. }
  203. $preResponse = $this->preList($request);
  204. if ($preResponse !== null) {
  205. return $preResponse;
  206. }
  207. if ($listMode = $request->get('_list_mode')) {
  208. $this->admin->setListMode($listMode);
  209. }
  210. $datagrid = $this->admin->getDatagrid();
  211. $formView = $datagrid->getForm()->createView();
  212. // set the theme for the current Admin Form
  213. $this->get('twig')->getExtension('form')->renderer->setTheme($formView, $this->admin->getFilterTheme());
  214. return $this->render($this->admin->getTemplate('list'), array(
  215. 'action' => 'list',
  216. 'form' => $formView,
  217. 'datagrid' => $datagrid,
  218. 'csrf_token' => $this->getCsrfToken('sonata.batch'),
  219. ), null, $request);
  220. }
  221. /**
  222. * Execute a batch delete.
  223. *
  224. * @param ProxyQueryInterface $query
  225. *
  226. * @return RedirectResponse
  227. *
  228. * @throws AccessDeniedException If access is not granted
  229. */
  230. public function batchActionDelete(ProxyQueryInterface $query)
  231. {
  232. if (false === $this->admin->isGranted('DELETE')) {
  233. throw new AccessDeniedException();
  234. }
  235. $modelManager = $this->admin->getModelManager();
  236. try {
  237. $modelManager->batchDelete($this->admin->getClass(), $query);
  238. $this->addFlash('sonata_flash_success', 'flash_batch_delete_success');
  239. } catch (ModelManagerException $e) {
  240. $this->handleModelManagerException($e);
  241. $this->addFlash('sonata_flash_error', 'flash_batch_delete_error');
  242. }
  243. return new RedirectResponse($this->admin->generateUrl(
  244. 'list',
  245. array('filter' => $this->admin->getFilterParameters())
  246. ));
  247. }
  248. /**
  249. * Delete action.
  250. *
  251. * @param int|string|null $id
  252. * @param Request $request
  253. *
  254. * @return Response|RedirectResponse
  255. *
  256. * @throws NotFoundHttpException If the object does not exist
  257. * @throws AccessDeniedException If access is not granted
  258. */
  259. public function deleteAction($id, Request $request = null)
  260. {
  261. $request = $this->resolveRequest($request);
  262. $id = $request->get($this->admin->getIdParameter());
  263. $object = $this->admin->getObject($id);
  264. if (!$object) {
  265. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  266. }
  267. if (false === $this->admin->isGranted('DELETE', $object)) {
  268. throw new AccessDeniedException();
  269. }
  270. $preResponse = $this->preDelete($request, $object);
  271. if ($preResponse !== null) {
  272. return $preResponse;
  273. }
  274. if ($this->getRestMethod($request) === 'DELETE') {
  275. // check the csrf token
  276. $this->validateCsrfToken('sonata.delete', $request);
  277. $objectName = $this->admin->toString($object);
  278. try {
  279. $this->admin->delete($object);
  280. if ($this->isXmlHttpRequest($request)) {
  281. return $this->renderJson(array('result' => 'ok'), 200, array(), $request);
  282. }
  283. $this->addFlash(
  284. 'sonata_flash_success',
  285. $this->admin->trans(
  286. 'flash_delete_success',
  287. array('%name%' => $this->escapeHtml($objectName)),
  288. 'SonataAdminBundle'
  289. )
  290. );
  291. } catch (ModelManagerException $e) {
  292. $this->handleModelManagerException($e);
  293. if ($this->isXmlHttpRequest($request)) {
  294. return $this->renderJson(array('result' => 'error'), 200, array(), $request);
  295. }
  296. $this->addFlash(
  297. 'sonata_flash_error',
  298. $this->admin->trans(
  299. 'flash_delete_error',
  300. array('%name%' => $this->escapeHtml($objectName)),
  301. 'SonataAdminBundle'
  302. )
  303. );
  304. }
  305. return $this->redirectTo($object, $request);
  306. }
  307. return $this->render($this->admin->getTemplate('delete'), array(
  308. 'object' => $object,
  309. 'action' => 'delete',
  310. 'csrf_token' => $this->getCsrfToken('sonata.delete'),
  311. ), null, $request);
  312. }
  313. /**
  314. * Edit action.
  315. *
  316. * @param int|string|null $id
  317. * @param Request $request
  318. *
  319. * @return Response|RedirectResponse
  320. *
  321. * @throws NotFoundHttpException If the object does not exist
  322. * @throws AccessDeniedException If access is not granted
  323. */
  324. public function editAction($id = null, Request $request = null)
  325. {
  326. $request = $this->resolveRequest($request);
  327. // the key used to lookup the template
  328. $templateKey = 'edit';
  329. $id = $request->get($this->admin->getIdParameter());
  330. $object = $this->admin->getObject($id);
  331. if (!$object) {
  332. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  333. }
  334. if (false === $this->admin->isGranted('EDIT', $object)) {
  335. throw new AccessDeniedException();
  336. }
  337. $preResponse = $this->preEdit($request, $object);
  338. if ($preResponse !== null) {
  339. return $preResponse;
  340. }
  341. $this->admin->setSubject($object);
  342. /** @var $form \Symfony\Component\Form\Form */
  343. $form = $this->admin->getForm();
  344. $form->setData($object);
  345. $form->handleRequest($request);
  346. if ($form->isSubmitted()) {
  347. $isFormValid = $form->isValid();
  348. // persist if the form was valid and if in preview mode the preview was approved
  349. if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) {
  350. try {
  351. $object = $this->admin->update($object);
  352. if ($this->isXmlHttpRequest($request)) {
  353. return $this->renderJson(array(
  354. 'result' => 'ok',
  355. 'objectId' => $this->admin->getNormalizedIdentifier($object),
  356. 'objectName' => $this->escapeHtml($this->admin->toString($object)),
  357. ), 200, array(), $request);
  358. }
  359. $this->addFlash(
  360. 'sonata_flash_success',
  361. $this->admin->trans(
  362. 'flash_edit_success',
  363. array('%name%' => $this->escapeHtml($this->admin->toString($object))),
  364. 'SonataAdminBundle'
  365. )
  366. );
  367. // redirect to edit mode
  368. return $this->redirectTo($object, $request);
  369. } catch (ModelManagerException $e) {
  370. $this->handleModelManagerException($e);
  371. $isFormValid = false;
  372. } catch (LockException $e) {
  373. $this->addFlash('sonata_flash_error', $this->admin->trans('flash_lock_error', array(
  374. '%name%' => $this->escapeHtml($this->admin->toString($object)),
  375. '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $object).'">',
  376. '%link_end%' => '</a>',
  377. ), 'SonataAdminBundle'));
  378. }
  379. }
  380. // show an error message if the form failed validation
  381. if (!$isFormValid) {
  382. if (!$this->isXmlHttpRequest($request)) {
  383. $this->addFlash(
  384. 'sonata_flash_error',
  385. $this->admin->trans(
  386. 'flash_edit_error',
  387. array('%name%' => $this->escapeHtml($this->admin->toString($object))),
  388. 'SonataAdminBundle'
  389. )
  390. );
  391. }
  392. } elseif ($this->isPreviewRequested($request)) {
  393. // enable the preview template if the form was valid and preview was requested
  394. $templateKey = 'preview';
  395. $this->admin->getShow();
  396. }
  397. }
  398. $view = $form->createView();
  399. // set the theme for the current Admin Form
  400. $this->get('twig')->getExtension('form')->renderer->setTheme($view, $this->admin->getFormTheme());
  401. return $this->render($this->admin->getTemplate($templateKey), array(
  402. 'action' => 'edit',
  403. 'form' => $view,
  404. 'object' => $object,
  405. ), null, $request);
  406. }
  407. /**
  408. * Redirect the user depend on this choice.
  409. *
  410. * @param object $object
  411. * @param Request $request
  412. *
  413. * @return RedirectResponse
  414. */
  415. protected function redirectTo($object, Request $request = null)
  416. {
  417. $request = $this->resolveRequest($request);
  418. $url = false;
  419. if (null !== $request->get('btn_update_and_list')) {
  420. $url = $this->admin->generateUrl('list');
  421. }
  422. if (null !== $request->get('btn_create_and_list')) {
  423. $url = $this->admin->generateUrl('list');
  424. }
  425. if (null !== $request->get('btn_create_and_create')) {
  426. $params = array();
  427. if ($this->admin->hasActiveSubClass()) {
  428. $params['subclass'] = $request->get('subclass');
  429. }
  430. $url = $this->admin->generateUrl('create', $params);
  431. }
  432. if ($this->getRestMethod($request) === 'DELETE') {
  433. $url = $this->admin->generateUrl('list');
  434. }
  435. if (!$url) {
  436. $url = $this->admin->generateObjectUrl('edit', $object);
  437. }
  438. return new RedirectResponse($url);
  439. }
  440. /**
  441. * Batch action.
  442. *
  443. * @param Request $request
  444. *
  445. * @return Response|RedirectResponse
  446. *
  447. * @throws NotFoundHttpException If the HTTP method is not POST
  448. * @throws \RuntimeException If the batch action is not defined
  449. */
  450. public function batchAction(Request $request = null)
  451. {
  452. $request = $this->resolveRequest($request);
  453. $restMethod = $this->getRestMethod($request);
  454. if ('POST' !== $restMethod) {
  455. throw $this->createNotFoundException(sprintf('Invalid request type "%s", POST expected', $restMethod));
  456. }
  457. // check the csrf token
  458. $this->validateCsrfToken('sonata.batch', $request);
  459. $confirmation = $request->get('confirmation', false);
  460. if ($data = json_decode($request->get('data'), true)) {
  461. $action = $data['action'];
  462. $idx = $data['idx'];
  463. $allElements = $data['all_elements'];
  464. $request->request->replace($data);
  465. } else {
  466. $request->request->set('idx', $request->get('idx', array()));
  467. $request->request->set('all_elements', $request->get('all_elements', false));
  468. $action = $request->get('action');
  469. $idx = $request->get('idx');
  470. $allElements = $request->get('all_elements');
  471. $data = $request->request->all();
  472. unset($data['_sonata_csrf_token']);
  473. }
  474. $batchActions = $this->admin->getBatchActions();
  475. if (!array_key_exists($action, $batchActions)) {
  476. throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
  477. }
  478. $camelizedAction = BaseFieldDescription::camelize($action);
  479. $isRelevantAction = sprintf('batchAction%sIsRelevant', ucfirst($camelizedAction));
  480. if (method_exists($this, $isRelevantAction)) {
  481. $nonRelevantMessage = call_user_func(array($this, $isRelevantAction), $idx, $allElements);
  482. } else {
  483. $nonRelevantMessage = count($idx) != 0 || $allElements; // at least one item is selected
  484. }
  485. if (!$nonRelevantMessage) { // default non relevant message (if false of null)
  486. $nonRelevantMessage = 'flash_batch_empty';
  487. }
  488. $datagrid = $this->admin->getDatagrid();
  489. $datagrid->buildPager();
  490. if (true !== $nonRelevantMessage) {
  491. $this->addFlash('sonata_flash_info', $nonRelevantMessage);
  492. return new RedirectResponse(
  493. $this->admin->generateUrl(
  494. 'list',
  495. array('filter' => $this->admin->getFilterParameters())
  496. )
  497. );
  498. }
  499. $askConfirmation = isset($batchActions[$action]['ask_confirmation']) ?
  500. $batchActions[$action]['ask_confirmation'] :
  501. true;
  502. if ($askConfirmation && $confirmation != 'ok') {
  503. $actionLabel = $batchActions[$action]['label'];
  504. $formView = $datagrid->getForm()->createView();
  505. return $this->render($this->admin->getTemplate('batch_confirmation'), array(
  506. 'action' => 'list',
  507. 'action_label' => $actionLabel,
  508. 'datagrid' => $datagrid,
  509. 'form' => $formView,
  510. 'data' => $data,
  511. 'csrf_token' => $this->getCsrfToken('sonata.batch'),
  512. ), null, $request);
  513. }
  514. // execute the action, batchActionXxxxx
  515. $finalAction = sprintf('batchAction%s', ucfirst($camelizedAction));
  516. if (!is_callable(array($this, $finalAction))) {
  517. throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', get_class($this), $finalAction));
  518. }
  519. $query = $datagrid->getQuery();
  520. $query->setFirstResult(null);
  521. $query->setMaxResults(null);
  522. $this->admin->preBatchAction($action, $query, $idx, $allElements);
  523. if (count($idx) > 0) {
  524. $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
  525. } elseif (!$allElements) {
  526. $query = null;
  527. }
  528. return call_user_func(array($this, $finalAction), $query);
  529. }
  530. /**
  531. * Create action.
  532. *
  533. * @param Request $request
  534. *
  535. * @return Response
  536. *
  537. * @throws AccessDeniedException If access is not granted
  538. */
  539. public function createAction(Request $request = null)
  540. {
  541. $request = $this->resolveRequest($request);
  542. // the key used to lookup the template
  543. $templateKey = 'edit';
  544. if (false === $this->admin->isGranted('CREATE')) {
  545. throw new AccessDeniedException();
  546. }
  547. $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
  548. if ($class->isAbstract()) {
  549. return $this->render(
  550. 'SonataAdminBundle:CRUD:select_subclass.html.twig',
  551. array(
  552. 'base_template' => $this->getBaseTemplate(),
  553. 'admin' => $this->admin,
  554. 'action' => 'create',
  555. ),
  556. null,
  557. $request
  558. );
  559. }
  560. $object = $this->admin->getNewInstance();
  561. $preResponse = $this->preCreate($request, $object);
  562. if ($preResponse !== null) {
  563. return $preResponse;
  564. }
  565. $this->admin->setSubject($object);
  566. /** @var $form \Symfony\Component\Form\Form */
  567. $form = $this->admin->getForm();
  568. $form->setData($object);
  569. $form->handleRequest($request);
  570. if ($form->isSubmitted()) {
  571. $isFormValid = $form->isValid();
  572. // persist if the form was valid and if in preview mode the preview was approved
  573. if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) {
  574. if (false === $this->admin->isGranted('CREATE', $object)) {
  575. throw new AccessDeniedException();
  576. }
  577. try {
  578. $object = $this->admin->create($object);
  579. if ($this->isXmlHttpRequest($request)) {
  580. return $this->renderJson(array(
  581. 'result' => 'ok',
  582. 'objectId' => $this->admin->getNormalizedIdentifier($object),
  583. ), 200, array(), $request);
  584. }
  585. $this->addFlash(
  586. 'sonata_flash_success',
  587. $this->admin->trans(
  588. 'flash_create_success',
  589. array('%name%' => $this->escapeHtml($this->admin->toString($object))),
  590. 'SonataAdminBundle'
  591. )
  592. );
  593. // redirect to edit mode
  594. return $this->redirectTo($object, $request);
  595. } catch (ModelManagerException $e) {
  596. $this->handleModelManagerException($e);
  597. $isFormValid = false;
  598. }
  599. }
  600. // show an error message if the form failed validation
  601. if (!$isFormValid) {
  602. if (!$this->isXmlHttpRequest($request)) {
  603. $this->addFlash(
  604. 'sonata_flash_error',
  605. $this->admin->trans(
  606. 'flash_create_error',
  607. array('%name%' => $this->escapeHtml($this->admin->toString($object))),
  608. 'SonataAdminBundle'
  609. )
  610. );
  611. }
  612. } elseif ($this->isPreviewRequested($request)) {
  613. // pick the preview template if the form was valid and preview was requested
  614. $templateKey = 'preview';
  615. $this->admin->getShow();
  616. }
  617. }
  618. $view = $form->createView();
  619. // set the theme for the current Admin Form
  620. $this->get('twig')->getExtension('form')->renderer->setTheme($view, $this->admin->getFormTheme());
  621. return $this->render($this->admin->getTemplate($templateKey), array(
  622. 'action' => 'create',
  623. 'form' => $view,
  624. 'object' => $object,
  625. ), null, $request);
  626. }
  627. /**
  628. * Returns true if the preview is requested to be shown.
  629. *
  630. * @param Request $request
  631. *
  632. * @return bool
  633. */
  634. protected function isPreviewRequested(Request $request = null)
  635. {
  636. $request = $this->resolveRequest($request);
  637. return ($request->get('btn_preview') !== null);
  638. }
  639. /**
  640. * Returns true if the preview has been approved.
  641. *
  642. * @param Request $request
  643. *
  644. * @return bool
  645. */
  646. protected function isPreviewApproved(Request $request = null)
  647. {
  648. $request = $this->resolveRequest($request);
  649. return ($request->get('btn_preview_approve') !== null);
  650. }
  651. /**
  652. * Returns true if the request is in the preview workflow.
  653. *
  654. * That means either a preview is requested or the preview has already been shown
  655. * and it got approved/declined.
  656. *
  657. * @param Request $request
  658. *
  659. * @return bool
  660. */
  661. protected function isInPreviewMode(Request $request = null)
  662. {
  663. $request = $this->resolveRequest($request);
  664. return $this->admin->supportsPreviewMode()
  665. && ($this->isPreviewRequested($request)
  666. || $this->isPreviewApproved($request)
  667. || $this->isPreviewDeclined($request));
  668. }
  669. /**
  670. * Returns true if the preview has been declined.
  671. *
  672. * @param Request $request
  673. *
  674. * @return bool
  675. */
  676. protected function isPreviewDeclined(Request $request = null)
  677. {
  678. $request = $this->resolveRequest($request);
  679. return ($request->get('btn_preview_decline') !== null);
  680. }
  681. /**
  682. * Show action.
  683. *
  684. * @param int|string|null $id
  685. * @param Request $request
  686. *
  687. * @return Response
  688. *
  689. * @throws NotFoundHttpException If the object does not exist
  690. * @throws AccessDeniedException If access is not granted
  691. */
  692. public function showAction($id = null, Request $request = null)
  693. {
  694. $request = $this->resolveRequest($request);
  695. $id = $request->get($this->admin->getIdParameter());
  696. $object = $this->admin->getObject($id);
  697. if (!$object) {
  698. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  699. }
  700. if (false === $this->admin->isGranted('VIEW', $object)) {
  701. throw new AccessDeniedException();
  702. }
  703. $preResponse = $this->preShow($request, $object);
  704. if ($preResponse !== null) {
  705. return $preResponse;
  706. }
  707. $this->admin->setSubject($object);
  708. return $this->render($this->admin->getTemplate('show'), array(
  709. 'action' => 'show',
  710. 'object' => $object,
  711. 'elements' => $this->admin->getShow(),
  712. ), null, $request);
  713. }
  714. /**
  715. * Show history revisions for object.
  716. *
  717. * @param int|string|null $id
  718. * @param Request $request
  719. *
  720. * @return Response
  721. *
  722. * @throws AccessDeniedException If access is not granted
  723. * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
  724. */
  725. public function historyAction($id = null, Request $request = null)
  726. {
  727. $request = $this->resolveRequest($request);
  728. $id = $request->get($this->admin->getIdParameter());
  729. $object = $this->admin->getObject($id);
  730. if (!$object) {
  731. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  732. }
  733. if (false === $this->admin->isGranted('EDIT', $object)) {
  734. throw new AccessDeniedException();
  735. }
  736. $manager = $this->get('sonata.admin.audit.manager');
  737. if (!$manager->hasReader($this->admin->getClass())) {
  738. throw new NotFoundHttpException(
  739. sprintf(
  740. 'unable to find the audit reader for class : %s',
  741. $this->admin->getClass()
  742. )
  743. );
  744. }
  745. $reader = $manager->getReader($this->admin->getClass());
  746. $revisions = $reader->findRevisions($this->admin->getClass(), $id);
  747. return $this->render($this->admin->getTemplate('history'), array(
  748. 'action' => 'history',
  749. 'object' => $object,
  750. 'revisions' => $revisions,
  751. 'currentRevision' => $revisions ? current($revisions) : false,
  752. ), null, $request);
  753. }
  754. /**
  755. * View history revision of object.
  756. *
  757. * @param int|string|null $id
  758. * @param string|null $revision
  759. * @param Request $request
  760. *
  761. * @return Response
  762. *
  763. * @throws AccessDeniedException If access is not granted
  764. * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  765. */
  766. public function historyViewRevisionAction($id = null, $revision = null, Request $request = null)
  767. {
  768. $request = $this->resolveRequest($request);
  769. $id = $request->get($this->admin->getIdParameter());
  770. $object = $this->admin->getObject($id);
  771. if (!$object) {
  772. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  773. }
  774. if (false === $this->admin->isGranted('EDIT', $object)) {
  775. throw new AccessDeniedException();
  776. }
  777. $manager = $this->get('sonata.admin.audit.manager');
  778. if (!$manager->hasReader($this->admin->getClass())) {
  779. throw new NotFoundHttpException(
  780. sprintf(
  781. 'unable to find the audit reader for class : %s',
  782. $this->admin->getClass()
  783. )
  784. );
  785. }
  786. $reader = $manager->getReader($this->admin->getClass());
  787. // retrieve the revisioned object
  788. $object = $reader->find($this->admin->getClass(), $id, $revision);
  789. if (!$object) {
  790. throw new NotFoundHttpException(
  791. sprintf(
  792. 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  793. $id,
  794. $revision,
  795. $this->admin->getClass()
  796. )
  797. );
  798. }
  799. $this->admin->setSubject($object);
  800. return $this->render($this->admin->getTemplate('show'), array(
  801. 'action' => 'show',
  802. 'object' => $object,
  803. 'elements' => $this->admin->getShow(),
  804. ), null, $request);
  805. }
  806. /**
  807. * Compare history revisions of object.
  808. *
  809. * @param int|string|null $id
  810. * @param int|string|null $base_revision
  811. * @param int|string|null $compare_revision
  812. * @param Request $request
  813. *
  814. * @return Response
  815. *
  816. * @throws AccessDeniedException If access is not granted
  817. * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  818. */
  819. public function historyCompareRevisionsAction($id = null, $base_revision = null, $compare_revision = null, Request $request = null)
  820. {
  821. $request = $this->resolveRequest($request);
  822. if (false === $this->admin->isGranted('EDIT')) {
  823. throw new AccessDeniedException();
  824. }
  825. $id = $request->get($this->admin->getIdParameter());
  826. $object = $this->admin->getObject($id);
  827. if (!$object) {
  828. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  829. }
  830. $manager = $this->get('sonata.admin.audit.manager');
  831. if (!$manager->hasReader($this->admin->getClass())) {
  832. throw new NotFoundHttpException(
  833. sprintf(
  834. 'unable to find the audit reader for class : %s',
  835. $this->admin->getClass()
  836. )
  837. );
  838. }
  839. $reader = $manager->getReader($this->admin->getClass());
  840. // retrieve the base revision
  841. $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
  842. if (!$base_object) {
  843. throw new NotFoundHttpException(
  844. sprintf(
  845. 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  846. $id,
  847. $base_revision,
  848. $this->admin->getClass()
  849. )
  850. );
  851. }
  852. // retrieve the compare revision
  853. $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
  854. if (!$compare_object) {
  855. throw new NotFoundHttpException(
  856. sprintf(
  857. 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  858. $id,
  859. $compare_revision,
  860. $this->admin->getClass()
  861. )
  862. );
  863. }
  864. $this->admin->setSubject($base_object);
  865. return $this->render($this->admin->getTemplate('show_compare'), array(
  866. 'action' => 'show',
  867. 'object' => $base_object,
  868. 'object_compare' => $compare_object,
  869. 'elements' => $this->admin->getShow(),
  870. ), null, $request);
  871. }
  872. /**
  873. * Export data to specified format.
  874. *
  875. * @param Request $request
  876. *
  877. * @return Response
  878. *
  879. * @throws AccessDeniedException If access is not granted
  880. * @throws \RuntimeException If the export format is invalid
  881. */
  882. public function exportAction(Request $request = null)
  883. {
  884. $request = $this->resolveRequest($request);
  885. if (false === $this->admin->isGranted('EXPORT')) {
  886. throw new AccessDeniedException();
  887. }
  888. $format = $request->get('format');
  889. $allowedExportFormats = (array) $this->admin->getExportFormats();
  890. if (!in_array($format, $allowedExportFormats)) {
  891. throw new \RuntimeException(
  892. sprintf(
  893. 'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
  894. $format,
  895. $this->admin->getClass(),
  896. implode(', ', $allowedExportFormats)
  897. )
  898. );
  899. }
  900. $filename = sprintf(
  901. 'export_%s_%s.%s',
  902. strtolower(substr($this->admin->getClass(), strripos($this->admin->getClass(), '\\') + 1)),
  903. date('Y_m_d_H_i_s', strtotime('now')),
  904. $format
  905. );
  906. return $this->get('sonata.admin.exporter')->getResponse(
  907. $format,
  908. $filename,
  909. $this->admin->getDataSourceIterator()
  910. );
  911. }
  912. /**
  913. * Gets ACL users.
  914. *
  915. * @return \Traversable
  916. */
  917. protected function getAclUsers()
  918. {
  919. $aclUsers = array();
  920. $userManagerServiceName = $this->container->getParameter('sonata.admin.security.acl_user_manager');
  921. if ($userManagerServiceName !== null && $this->has($userManagerServiceName)) {
  922. $userManager = $this->get($userManagerServiceName);
  923. if (method_exists($userManager, 'findUsers')) {
  924. $aclUsers = $userManager->findUsers();
  925. }
  926. }
  927. return is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
  928. }
  929. /**
  930. * Gets ACL roles.
  931. *
  932. * @return \Traversable
  933. */
  934. protected function getAclRoles()
  935. {
  936. $aclRoles = array();
  937. $roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
  938. $pool = $this->container->get('sonata.admin.pool');
  939. foreach ($pool->getAdminServiceIds() as $id) {
  940. try {
  941. $admin = $pool->getInstance($id);
  942. } catch (\Exception $e) {
  943. continue;
  944. }
  945. $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
  946. foreach ($admin->getSecurityInformation() as $role => $permissions) {
  947. $role = sprintf($baseRole, $role);
  948. $aclRoles[] = $role;
  949. }
  950. }
  951. foreach ($roleHierarchy as $name => $roles) {
  952. $aclRoles[] = $name;
  953. $aclRoles = array_merge($aclRoles, $roles);
  954. }
  955. $aclRoles = array_unique($aclRoles);
  956. return is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
  957. }
  958. /**
  959. * Returns the Response object associated to the acl action.
  960. *
  961. * @param int|string|null $id
  962. * @param Request $request
  963. *
  964. * @return Response|RedirectResponse
  965. *
  966. * @throws AccessDeniedException If access is not granted.
  967. * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
  968. */
  969. public function aclAction($id = null, Request $request = null)
  970. {
  971. $request = $this->resolveRequest($request);
  972. if (!$this->admin->isAclEnabled()) {
  973. throw new NotFoundHttpException('ACL are not enabled for this admin');
  974. }
  975. $id = $request->get($this->admin->getIdParameter());
  976. $object = $this->admin->getObject($id);
  977. if (!$object) {
  978. throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id));
  979. }
  980. if (false === $this->admin->isGranted('MASTER', $object)) {
  981. throw new AccessDeniedException();
  982. }
  983. $this->admin->setSubject($object);
  984. $aclUsers = $this->getAclUsers();
  985. $aclRoles = $this->getAclRoles();
  986. $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
  987. $adminObjectAclData = new AdminObjectAclData(
  988. $this->admin,
  989. $object,
  990. $aclUsers,
  991. $adminObjectAclManipulator->getMaskBuilderClass(),
  992. $aclRoles
  993. );
  994. $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
  995. $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
  996. if ($request->getMethod() === 'POST') {
  997. if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
  998. $form = $aclUsersForm;
  999. $updateMethod = 'updateAclUsers';
  1000. } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
  1001. $form = $aclRolesForm;
  1002. $updateMethod = 'updateAclRoles';
  1003. }
  1004. if (isset($form)) {
  1005. $form->handleRequest($request);
  1006. if ($form->isValid()) {
  1007. $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
  1008. $this->addFlash('sonata_flash_success', 'flash_acl_edit_success');
  1009. return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
  1010. }
  1011. }
  1012. }
  1013. return $this->render($this->admin->getTemplate('acl'), array(
  1014. 'action' => 'acl',
  1015. 'permissions' => $adminObjectAclData->getUserPermissions(),
  1016. 'object' => $object,
  1017. 'users' => $aclUsers,
  1018. 'roles' => $aclRoles,
  1019. 'aclUsersForm' => $aclUsersForm->createView(),
  1020. 'aclRolesForm' => $aclRolesForm->createView(),
  1021. ), null, $request);
  1022. }
  1023. /**
  1024. * Adds a flash message for type.
  1025. *
  1026. * @param string $type
  1027. * @param string $message
  1028. *
  1029. * @TODO Remove this method when bumping requirements to Symfony >= 2.6
  1030. */
  1031. protected function addFlash($type, $message)
  1032. {
  1033. if (method_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller', 'addFlash')) {
  1034. parent::addFlash($type, $message);
  1035. } else {
  1036. $this->get('session')
  1037. ->getFlashBag()
  1038. ->add($type, $message);
  1039. }
  1040. }
  1041. /**
  1042. * Validate CSRF token for action without form.
  1043. *
  1044. * @param string $intention
  1045. * @param Request $request
  1046. *
  1047. * @throws HttpException
  1048. */
  1049. protected function validateCsrfToken($intention, Request $request = null)
  1050. {
  1051. if (!$this->container->has('form.csrf_provider')) {
  1052. return;
  1053. }
  1054. $request = $this->resolveRequest($request);
  1055. if (!$this->container->get('form.csrf_provider')->isCsrfTokenValid(
  1056. $intention,
  1057. $request->request->get('_sonata_csrf_token', false)
  1058. )) {
  1059. throw new HttpException(400, 'The csrf token is not valid, CSRF attack?');
  1060. }
  1061. }
  1062. /**
  1063. * Escape string for html output.
  1064. *
  1065. * @param string $s
  1066. *
  1067. * @return string
  1068. */
  1069. protected function escapeHtml($s)
  1070. {
  1071. return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  1072. }
  1073. /**
  1074. * Get CSRF token.
  1075. *
  1076. * @param string $intention
  1077. *
  1078. * @return string|false
  1079. */
  1080. protected function getCsrfToken($intention)
  1081. {
  1082. if ($this->container->has('security.csrf.token_manager')) {
  1083. return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
  1084. }
  1085. // TODO: Remove it when bumping requirements to SF 2.4+
  1086. if ($this->container->has('form.csrf_provider')) {
  1087. return $this->container->get('form.csrf_provider')->generateCsrfToken($intention);
  1088. }
  1089. return false;
  1090. }
  1091. /**
  1092. * This method can be overloaded in your custom CRUD controller.
  1093. * It's called from createAction.
  1094. *
  1095. * @param Request $request
  1096. * @param mixed $object
  1097. *
  1098. * @return Response|null
  1099. */
  1100. protected function preCreate(Request $request, $object)
  1101. {
  1102. }
  1103. /**
  1104. * This method can be overloaded in your custom CRUD controller.
  1105. * It's called from editAction.
  1106. *
  1107. * @param Request $request
  1108. * @param mixed $object
  1109. *
  1110. * @return Response|null
  1111. */
  1112. protected function preEdit(Request $request, $object)
  1113. {
  1114. }
  1115. /**
  1116. * This method can be overloaded in your custom CRUD controller.
  1117. * It's called from deleteAction.
  1118. *
  1119. * @param Request $request
  1120. * @param mixed $object
  1121. *
  1122. * @return Response|null
  1123. */
  1124. protected function preDelete(Request $request, $object)
  1125. {
  1126. }
  1127. /**
  1128. * This method can be overloaded in your custom CRUD controller.
  1129. * It's called from showAction.
  1130. *
  1131. * @param Request $request
  1132. * @param mixed $object
  1133. *
  1134. * @return Response|null
  1135. */
  1136. protected function preShow(Request $request, $object)
  1137. {
  1138. }
  1139. /**
  1140. * This method can be overloaded in your custom CRUD controller.
  1141. * It's called from listAction.
  1142. *
  1143. * @param Request $request
  1144. *
  1145. * @return Response|null
  1146. */
  1147. protected function preList(Request $request)
  1148. {
  1149. }
  1150. /**
  1151. * To keep backwards compatibility with older Sonata Admin code.
  1152. *
  1153. * @internal
  1154. */
  1155. private function resolveRequest(Request $request = null)
  1156. {
  1157. if (null === $request) {
  1158. return $this->getRequest();
  1159. }
  1160. return $request;
  1161. }
  1162. }