1: <?php
2: namespace TIC\CoreBundle\Base;
3:
4: use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
5:
6: use Symfony\Component\Form\FormInterface;
7: use Symfony\Component\HttpFoundation\Request;
8: use Symfony\Component\HttpFoundation\RedirectResponse;
9: use Symfony\Component\HttpFoundation\StreamedResponse;
10:
11: /**
12: * Controlleur de base disposant de nombreuses méthodes utiles.
13: * @see https://symfony.com/doc/current/controller.html
14: */
15: abstract class TICController extends AbstractController
16: {
17: // Initialisation automatique des propriétés à partir du nom de la classe courante
18: use \TIC\CoreBundle\Traits\ContextProperties; // ctxForm, ctxTrans...
19: use \TIC\CoreBundle\Traits\ControllerRouting; // redirectTo, redirectItem, redirectList...
20:
21: // ----------------------------------------------------------------- FORMS
22:
23: /**
24: * Raccourci pour créer le formulaire correspondant au controlleur.
25: *
26: * @param mixed $item
27: * @param Request $request
28: * @param array $options Options du formulaire + facultatif "form_name" pour spécifier son nommage
29: * @return FormInterface
30: */
31: protected function getForm(mixed $data = null, ?Request $request = null, ?array $options = []): FormInterface
32: {
33: # $form = $this->createForm($this->ctxForm, $data, $options);
34:
35: // Génération du FormType (avec ma méthode createNamed sur présence de l'option "form_name")
36: if (isset($options['form_name'])) {
37: $name = $options['form_name']; unset($options['form_name']);
38: $form = $this->container->get('form.factory')->createNamed($name, $this->ctxForm, $data, $options);
39: } else {
40: $form = $this->container->get('form.factory')->create($this->ctxForm, $data, $options);
41: }
42:
43: // Application des données de la requête (si soumission et $request transmis)
44: if ($request !== null) $form->handleRequest($request);
45:
46: return $form;
47: }
48:
49: // ----------------------------------------------------------------- TRANSLATOR
50:
51:
52: /**
53: * Raccourci vers la méthode trans du translator.
54: *
55: * @param atring|array $message Clé de message à traduire (possibilité de traiter une liste)
56: * @param array $parameters Liste de paramètres à fournir à la méthode du translator
57: * @return string|array Chaine traduite (ou liste des chaines traduites)
58: */
59: protected function trans(mixed $message, array $parameters = []): mixed
60: {
61: if (\is_array($message)) {
62: foreach ($message as $key => $val) $message[$key] = $this->get('translator')->trans($val, $parameters);
63: return $message;
64: }
65: return $this->get('translator')->trans($message, $parameters);
66: }
67:
68: /**
69: *
70: */
71: protected function mesg(string $mesg): string
72: {
73: return $this->ctxTrans . $mesg;
74: }
75:
76: /**
77: * Retourne le nom (traduit, au singulier ou au pluriel) de l'objet du controlleur.
78: */
79: protected function getItemLabel(bool $pluriel = false): string
80: {
81: return $this->trans(\sprintf('%s.names.%s', $this->ctxTrans, ($pluriel) ? 'plur' : 'sing'));
82: }
83:
84: // ----------------------------------------------------------------- REDIRECT
85:
86: /**
87: * Ajout d'un message dans le FlashBag (avec redirection optionnelle).
88: *
89: * @param string $type Contexte bootstrap : success | info | warning | danger
90: * @param string|array $message Texte simple ou structure avec : title [text] [extra] [params]
91: * @param mixed $redirect Boolean (liste/referer), Entité (ou id) ou autre route
92: * @return RedirectResponse Réponse HTTP avec redirection selon le paramètre $redirect
93: */
94: protected function alert(string $type, mixed $message, mixed $redirect = null): ?RedirectResponse
95: {
96: $this->addFlash($type, $message);
97: return $this->redirectTo($redirect);
98: }
99:
100: // ----------------------------------------------------------------- DOWNLOAD
101:
102: /**
103: * Retourne les entêtes HTTP pour le téléchargement d'un fichier du serveur.
104: *
105: * @param string $filepath Chemin complet sur le système de fichier
106: * @param string $filename Nom à présenter pour le téléchargement (depuis $filepath par défaut)
107: * @param bool $inline Faux pour forcer le téléchargement
108: * @param bool|string $mimetype Type MIME (deviné par défaut, application/octet-stream si True)
109: * @return array Liste d'entêtes HTTP pour une Response
110: */
111: protected function fileHeaders(string $filepath, ?string $filename, $mimetype, ?bool $inline): array
112: {
113: if ($filepath === null) throw new \Exception('alert.file.download.notexist');
114: if (! \file_exists($filepath)) throw new \Exception('alert.file.download.notfound');
115: if ($filename === null) $filename = \basename($filepath);
116:
117: if ($mimetype === null) $mimetype = ($inline) ? true : "application/octet-stream";
118: if ($mimetype === true) {
119: $fileinst = new \Symfony\Component\HttpFoundation\File\File($filepath);
120: $mimetype = $fileinst->getMimeType();
121: }
122: $disposition = ($inline) ? "inline" : "attachment";
123:
124: return array(
125: 'Content-Type' => $mimetype,
126: 'Content-Disposition' => $disposition . '; filename="'. $filename .'"',
127: 'Content-Length' => \filesize($filepath),
128: );
129: }
130:
131: /**
132: * Méthode utilitaire pour retourner un fichier en réponse d'une action.
133: *
134: * @param string $filepath Chemin complet sur le système de fichier
135: * @param string $filename Nom à présenter pour le téléchargement (depuis $filepath par défaut)
136: * @param bool $inline Faux pour forcer le téléchargement
137: * @param bool|string $mimetype Type MIME (deviné par défaut, application/octet-stream si True)
138: * @return Response Réponse HTTP contenant les données du fichier
139: */
140: protected function fileDownload(string $filepath, ?string $filename=null, ?bool $inline=false, $mimetype=null): Response
141: {
142: $headers = $this->fileHeaders($filepath, $filename, $mimetype, $inline);
143: return new Response(\file_get_contents($filepath), 200, $headers);
144: }
145:
146: /**
147: * Méthode utilitaire pour retourner un fichier en réponse d'une action (version stream avec option delete).
148: *
149: * @param string $filepath Chemin complet sur le système de fichier
150: * @param string $filename Nom à présenter pour le téléchargement (depuis $filepath par défaut)
151: * @param bool $inline Faux pour forcer le téléchargement
152: * @param bool|string $mimetype Type MIME (deviné par défaut, application/octet-stream si True)
153: * @param bool $delete Suppression du fichier à la fin du téléchargement
154: * @return StreamedResponse Réponse HTTP avec callback pour transmission des données du fichier
155: */
156: protected function fileStream(string $filepath, ?string $filename=null, ?bool $inline=false, $mimetype=null, ?bool $delete=false): StreamedResponse
157: {
158: $headers = $this->fileHeaders($filepath, $filename, $mimetype, $inline);
159: return new StreamedResponse(function() use ($filepath, $delete) {
160: \readfile($filepath);
161: if ($delete === true) @\unlink($filepath);
162: }, 200, $headers);
163: }
164:
165: // ----------------------------------------------------------------- SECURITY
166:
167: /**
168: * Vérification de la validité d'un token CSRF dans la requête.
169: *
170: * @param string $name Clé du token à vérifier dans la requête
171: * @param Request $request Requête courante pour récupération de la valeur du token
172: * @return bool
173: */
174: protected function checkCSRF(string $name, ?Request $request = null): ?bool
175: {
176: if (empty($request)) return null;
177: $token = $request->request->get('_token');
178: if ($token === null) $token = $request->query->get('_token');
179: return $this->isCsrfTokenValid($name, $token);
180: }
181:
182: /**
183: * Vérification que l'utilisateur courant dispose de l'un des droits mentionnés.
184: *
185: * @param string|array $roles Liste des rôles requis (test 'OR'), sans le préfixe 'ROLE_'
186: * @param string|bool $exception Message pour lever une exception (True pour le message par défaut)
187: * @return bool True si l'un des rôles est trouvé, sinon False (si $exception à False)
188: */
189: protected function checkRole(mixed $roles, mixed $exception = false): bool
190: {
191: if (! \is_array($roles)) $roles = \explode(',', $roles);
192: foreach ($roles as $role) {
193: if ($this->isGranted('ROLE_' . $role)) return true;
194: }
195: if ($exception === false) return false;
196: if ($exception === true) $exception = 'error.role.access_denied';
197: throw $this->createAccessDeniedException($this->trans($exception));
198: }
199:
200: }
201: