CRUDController.php 43 KB

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