Passed
Push — staging ( 81ba0c...d71a8f )
by Woeler
14:37 queued 10s
created

bundles/ApiBundle/EventListener/ApiSubscriber.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\ApiBundle\EventListener;
13
14
use Mautic\ApiBundle\ApiEvents;
15
use Mautic\ApiBundle\Event as Events;
16
use Mautic\CoreBundle\EventListener\CommonSubscriber;
17
use Mautic\CoreBundle\Helper\CoreParametersHelper;
18
use Mautic\CoreBundle\Helper\IpLookupHelper;
19
use Mautic\CoreBundle\Model\AuditLogModel;
20
use Symfony\Component\HttpFoundation\JsonResponse;
21
use Symfony\Component\HttpFoundation\Request;
22
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
23
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
24
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
25
use Symfony\Component\HttpKernel\KernelEvents;
26
27
/**
28
 * Class ApiSubscriber.
29
 */
30
class ApiSubscriber extends CommonSubscriber
31
{
32
    /**
33
     * @var IpLookupHelper
34
     */
35
    protected $ipLookupHelper;
36
37
    /**
38
     * @var CoreParametersHelper
39
     */
40
    protected $coreParametersHelper;
41
42
    /**
43
     * @var AuditLogModel
44
     */
45
    protected $auditLogModel;
46
47
    /**
48
     * ApiSubscriber constructor.
49
     *
50
     * @param IpLookupHelper       $ipLookupHelper
51
     * @param CoreParametersHelper $coreParametersHelper
52
     * @param AuditLogModel        $auditLogModel
53
     */
54
    public function __construct(IpLookupHelper $ipLookupHelper, CoreParametersHelper $coreParametersHelper, AuditLogModel $auditLogModel)
55
    {
56
        $this->ipLookupHelper       = $ipLookupHelper;
57
        $this->coreParametersHelper = $coreParametersHelper;
58
        $this->auditLogModel        = $auditLogModel;
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public static function getSubscribedEvents()
65
    {
66
        return [
67
            KernelEvents::REQUEST         => ['onKernelRequest', 255],
68
            KernelEvents::RESPONSE        => ['onKernelResponse', 0],
69
            ApiEvents::CLIENT_POST_SAVE   => ['onClientPostSave', 0],
70
            ApiEvents::CLIENT_POST_DELETE => ['onClientDelete', 0],
71
        ];
72
    }
73
74
    /**
75
     * Check for API requests and throw denied access if API is disabled.
76
     *
77
     * @param GetResponseEvent $event
78
     */
79
    public function onKernelRequest(GetResponseEvent $event)
80
    {
81
        if (!$event->isMasterRequest()) {
82
            return;
83
        }
84
85
        $apiEnabled   = $this->coreParametersHelper->getParameter('api_enabled');
86
        $request      = $event->getRequest();
0 ignored issues
show
The assignment to $request is dead and can be removed.
Loading history...
87
        $isApiRequest = $this->isApiRequest($event);
88
89
        if ($isApiRequest && !$apiEnabled) {
90
            throw new AccessDeniedHttpException($this->translator->trans('mautic.api.error.api.disabled'));
91
        }
92
    }
93
94
    /**
95
     * @param FilterResponseEvent $event
96
     */
97
    public function onKernelResponse(FilterResponseEvent $event)
98
    {
99
        $response   = $event->getResponse();
100
        $content    = $response->getContent();
101
        $statusCode = $response->getStatusCode();
102
103
        if ($this->isApiRequest($event) && strpos($content, 'error') !== false) {
104
            // Override api messages with something useful
105
            if ($data = json_decode($content, true)) {
106
                $type = null;
107
                if (isset($data['error'])) {
108
                    $error   = $data['error'];
109
                    $message = false;
110
                    if (is_array($error)) {
111
                        if (!isset($error['message'])) {
112
                            return;
113
                        }
114
115
                        // Catch useless oauth1a errors
116
                        $error = $error['message'];
117
                    }
118
119
                    switch ($error) {
120
                        case 'access_denied':
121
                            if ($this->isBasicAuth($event->getRequest())) {
122
                                if ($this->coreParametersHelper->getParameter('api_enable_basic_auth')) {
123
                                    $message = $this->translator->trans('mautic.api.error.basic.auth.invalid.credentials');
124
                                } else {
125
                                    $message = $this->translator->trans('mautic.api.error.basic.auth.disabled');
126
                                }
127
                            } else {
128
                                $message = $this->translator->trans('mautic.api.auth.error.accessdenied');
129
                            }
130
131
                            $type = $error;
132
                            break;
133
                        default:
134
                            if (isset($data['error_description'])) {
135
                                $message = $data['error_description'];
136
                                $type    = $error;
137
                            } elseif ($this->translator->hasId('mautic.api.auth.error.'.$error)) {
138
                                $message = $this->translator->trans('mautic.api.auth.error.'.$error);
139
                                $type    = $error;
140
                            }
141
                    }
142
143
                    if ($message) {
144
                        $response = new JsonResponse(
145
                            [
146
                                'errors' => [
147
                                    [
148
                                        'message' => $message,
149
                                        'code'    => $response->getStatusCode(),
150
                                        'type'    => $type,
151
                                    ],
152
                                ],
153
                                // @deprecated 2.6.0 to be removed in 3.0
154
                                'error'             => $data['error'],
155
                                'error_description' => $message.' (`error` and `error_description` are deprecated as of 2.6.0 and will be removed in 3.0. Use the `errors` array instead.)',
156
                            ],
157
                            $statusCode
158
                        );
159
160
                        $event->setResponse($response);
161
                    }
162
                }
163
            }
164
        }
165
    }
166
167
    public function isBasicAuth(Request $request)
168
    {
169
        try {
170
            return strpos(strtolower($request->headers->get('Authorization')), 'basic') === 0;
171
        } catch (\Exception $e) {
172
            return false;
173
        }
174
    }
175
176
    /**
177
     * Add a client change entry to the audit log.
178
     *
179
     * @param Events\ClientEvent $event
180
     */
181
    public function onClientPostSave(Events\ClientEvent $event)
182
    {
183
        $client = $event->getClient();
184
        if ($details = $event->getChanges()) {
185
            $log = [
186
                'bundle'    => 'api',
187
                'object'    => 'client',
188
                'objectId'  => $client->getId(),
189
                'action'    => ($event->isNew()) ? 'create' : 'update',
190
                'details'   => $details,
191
                'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
192
            ];
193
            $this->auditLogModel->writeToLog($log);
194
        }
195
    }
196
197
    /**
198
     * Add a role delete entry to the audit log.
199
     *
200
     * @param Events\ClientEvent $event
201
     */
202
    public function onClientDelete(Events\ClientEvent $event)
203
    {
204
        $client = $event->getClient();
205
        $log    = [
206
            'bundle'    => 'api',
207
            'object'    => 'client',
208
            'objectId'  => $client->deletedId,
209
            'action'    => 'delete',
210
            'details'   => ['name' => $client->getName()],
211
            'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
212
        ];
213
        $this->auditLogModel->writeToLog($log);
214
    }
215
216
    /**
217
     * @param $event
218
     *
219
     * @return bool
220
     */
221
    private function isApiRequest($event)
222
    {
223
        $request    = $event->getRequest();
224
        $requestUrl = $request->getRequestUri();
225
226
        // Check if /oauth or /api
227
        $isApiRequest = (strpos($requestUrl, '/oauth') !== false || strpos($requestUrl, '/api') !== false);
228
229
        defined('MAUTIC_API_REQUEST') or define('MAUTIC_API_REQUEST', $isApiRequest);
230
231
        return $isApiRequest;
232
    }
233
}
234