CRUDController.php 43 KB

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