DefaultController::wikifyApiAction()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use App\Model\Edit;
8
use App\Repository\ProjectRepository;
9
use MediaWiki\OAuthClient\Client;
10
use MediaWiki\OAuthClient\ClientConfig;
11
use MediaWiki\OAuthClient\Consumer;
12
use MediaWiki\OAuthClient\Exception;
13
use MediaWiki\OAuthClient\Token;
14
use OpenApi\Annotations as OA;
15
use Symfony\Component\HttpFoundation\JsonResponse;
16
use Symfony\Component\HttpFoundation\RedirectResponse;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\RequestStack;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\Routing\Annotation\Route;
21
22
/**
23
 * The DefaultController handles the homepage, about pages, and user authentication.
24
 */
25
class DefaultController extends XtoolsController
26
{
27
    /** @var Client The Oauth HTTP client. */
28
    protected Client $oauthClient;
29
30
    /**
31
     * Required to be defined by XtoolsController, though here it is unused.
32
     * @inheritDoc
33
     * @codeCoverageIgnore
34
     */
35
    public function getIndexRoute(): string
36
    {
37
        return 'homepage';
38
    }
39
40
    /**
41
     * Display the homepage.
42
     * @Route("/", name="homepage")
43
     * @Route("/index.php", name="homepageIndexPhp")
44
     * @return Response
45
     */
46
    public function indexAction(): Response
47
    {
48
        return $this->render('default/index.html.twig', [
49
            'xtPage' => 'home',
50
        ]);
51
    }
52
53
    /**
54
     * Redirect to the default project (or Meta) for Oauth authentication.
55
     * @Route("/login", name="login")
56
     * @param Request $request
57
     * @param RequestStack $requestStack
58
     * @param ProjectRepository $projectRepo
59
     * @param string $centralAuthProject
60
     * @return RedirectResponse
61
     * @codeCoverageIgnore
62
     */
63
    public function loginAction(
64
        Request $request,
65
        RequestStack $requestStack,
66
        ProjectRepository $projectRepo,
67
        string $centralAuthProject
68
    ): RedirectResponse {
69
        try {
70
            [ $next, $token ] = $this->getOauthClient($request, $projectRepo, $centralAuthProject)->initiate();
71
        } catch (Exception $oauthException) {
72
            $this->addFlashMessage('notice', 'error-login');
73
            return $this->redirectToRoute('homepage');
74
        }
75
76
        // Save the request token to the session.
77
        $requestStack->getSession()->set('oauth_request_token', $token);
78
        return new RedirectResponse($next);
79
    }
80
81
    /**
82
     * Receive authentication credentials back from the Oauth wiki.
83
     * @Route("/oauth_callback", name="oauth_callback")
84
     * @Route("/oauthredirector.php", name="old_oauth_callback")
85
     * @param RequestStack $requestStack
86
     * @param ProjectRepository $projectRepo
87
     * @param string $centralAuthProject
88
     * @return RedirectResponse
89
     */
90
    public function oauthCallbackAction(
91
        RequestStack $requestStack,
92
        ProjectRepository $projectRepo,
93
        string $centralAuthProject
94
    ): RedirectResponse {
95
        $request = $requestStack->getCurrentRequest();
96
        $session = $requestStack->getSession();
97
        // Give up if the required GET params don't exist.
98
        if (!$request->get('oauth_verifier')) {
99
            throw $this->createNotFoundException('No OAuth verifier given.');
100
        }
101
102
        // Complete authentication.
103
        $client = $this->getOauthClient($request, $projectRepo, $centralAuthProject);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type null; however, parameter $request of App\Controller\DefaultController::getOauthClient() does only seem to accept Symfony\Component\HttpFoundation\Request, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
        $client = $this->getOauthClient(/** @scrutinizer ignore-type */ $request, $projectRepo, $centralAuthProject);
Loading history...
104
        $token = $requestStack->getSession()->get('oauth_request_token');
105
106
        if (!is_a($token, Token::class)) {
107
            $this->addFlashMessage('notice', 'error-login');
108
            return $this->redirectToRoute('homepage');
109
        }
110
111
        $verifier = $request->get('oauth_verifier');
112
        $accessToken = $client->complete($token, $verifier);
113
114
        // Store access token, and remove request token.
115
        $session->set('oauth_access_token', $accessToken);
116
        $session->remove('oauth_request_token');
117
118
        // Store user identity.
119
        $ident = $client->identify($accessToken);
120
        $session->set('logged_in_user', $ident);
121
122
        // Store reference to the client.
123
        $session->set('oauth_client', $this->oauthClient);
124
125
        // Redirect to callback, if given.
126
        if ($request->query->get('redirect')) {
127
            return $this->redirect($request->query->get('redirect'));
128
        }
129
130
        // Send back to homepage.
131
        return $this->redirectToRoute('homepage');
132
    }
133
134
    /**
135
     * Get an OAuth client, configured to the default project.
136
     * (This shouldn't really be in this class, but oh well.)
137
     * @param Request $request
138
     * @param ProjectRepository $projectRepo
139
     * @param string $centralAuthProject
140
     * @return Client
141
     * @codeCoverageIgnore
142
     */
143
    protected function getOauthClient(
144
        Request $request,
145
        ProjectRepository $projectRepo,
146
        string $centralAuthProject
147
    ): Client {
148
        if (isset($this->oauthClient)) {
149
            return $this->oauthClient;
150
        }
151
        $defaultProject = $projectRepo->getProject($centralAuthProject);
152
        $endpoint = $defaultProject->getUrl(false)
153
                    . $defaultProject->getScript()
154
                    . '?title=Special:OAuth';
155
        $conf = new ClientConfig($endpoint);
156
        $consumerKey = $this->getParameter('oauth_key');
157
        $consumerSecret =  $this->getParameter('oauth_secret');
158
        $conf->setConsumer(new Consumer($consumerKey, $consumerSecret));
0 ignored issues
show
Bug introduced by
It seems like $consumerSecret can also be of type array; however, parameter $secret of MediaWiki\OAuthClient\Consumer::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
        $conf->setConsumer(new Consumer($consumerKey, /** @scrutinizer ignore-type */ $consumerSecret));
Loading history...
Bug introduced by
It seems like $consumerKey can also be of type array; however, parameter $key of MediaWiki\OAuthClient\Consumer::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
        $conf->setConsumer(new Consumer(/** @scrutinizer ignore-type */ $consumerKey, $consumerSecret));
Loading history...
159
        $this->oauthClient = new Client($conf);
160
161
        // Set the callback URL if given. Used to redirect back to target page after logging in.
162
        if ($request->query->get('callback')) {
163
            $this->oauthClient->setCallback($request->query->get('callback'));
0 ignored issues
show
Security Header Injection introduced by
$request->query->get('callback') can contain request data and is used in request header context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. ParameterBag::get() returns request data
    in vendor/symfony/http-foundation/ParameterBag.php on line 90
  2. parent::get($key, $this) is assigned to $value
    in vendor/symfony/http-foundation/InputBag.php on line 36
  3. $this === $value ? $default : $value is returned
    in vendor/symfony/http-foundation/InputBag.php on line 42

Used in request-header context

  1. Client::setCallback() is called
    in src/Controller/DefaultController.php on line 164
  2. Enters via parameter $url
    in vendor/mediawiki/oauthclient/src/Client.php on line 121
  3. $url is assigned to property Client::$callbackUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 122
  4. Read from property Client::$callbackUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 135
  5. Data is passed through urlencode()
    in vendor/mediawiki/oauthclient/src/Client.php on line 135
  6. $this->config->endpointURL . '/initiate&format=json&oauth_callback=' . urlencode($this->callbackUrl) is assigned to $initUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 133
  7. Client::makeOAuthCall() is called
    in vendor/mediawiki/oauthclient/src/Client.php on line 136
  8. Enters via parameter $url
    in vendor/mediawiki/oauthclient/src/Client.php on line 222
  9. Client::makeCurlCall() is called
    in vendor/mediawiki/oauthclient/src/Client.php on line 249
  10. Enters via parameter $url
    in vendor/mediawiki/oauthclient/src/Client.php on line 265
  11. curl_setopt() is called
    in vendor/mediawiki/oauthclient/src/Client.php on line 268

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
Security Variable Injection introduced by
$request->query->get('callback') can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. ParameterBag::get() returns request data
    in vendor/symfony/http-foundation/ParameterBag.php on line 90
  2. parent::get($key, $this) is assigned to $value
    in vendor/symfony/http-foundation/InputBag.php on line 36
  3. $this === $value ? $default : $value is returned
    in vendor/symfony/http-foundation/InputBag.php on line 42

Used in variable context

  1. Client::setCallback() is called
    in src/Controller/DefaultController.php on line 164
  2. Enters via parameter $url
    in vendor/mediawiki/oauthclient/src/Client.php on line 121
  3. $url is assigned to property Client::$callbackUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 122
  4. Read from property Client::$callbackUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 135
  5. Data is passed through urlencode()
    in vendor/mediawiki/oauthclient/src/Client.php on line 135
  6. $this->config->endpointURL . '/initiate&format=json&oauth_callback=' . urlencode($this->callbackUrl) is assigned to $initUrl
    in vendor/mediawiki/oauthclient/src/Client.php on line 133
  7. Client::makeOAuthCall() is called
    in vendor/mediawiki/oauthclient/src/Client.php on line 136
  8. Enters via parameter $url
    in vendor/mediawiki/oauthclient/src/Client.php on line 222
  9. Data is passed through parse_url()
    in vendor/mediawiki/oauthclient/src/Client.php on line 227
  10. parse_url($url) is assigned to $parsed
    in vendor/mediawiki/oauthclient/src/Client.php on line 227
  11. parse_str() is called
    in vendor/mediawiki/oauthclient/src/Client.php on line 228

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
164
        }
165
166
        return $this->oauthClient;
167
    }
168
169
    /**
170
     * Log out the user and return to the homepage.
171
     * @Route("/logout", name="logout")
172
     * @param RequestStack $requestStack
173
     * @return RedirectResponse
174
     */
175
    public function logoutAction(RequestStack $requestStack): RedirectResponse
176
    {
177
        $requestStack->getSession()->invalidate();
178
        return $this->redirectToRoute('homepage');
179
    }
180
181
    /************************ API endpoints ************************/
182
183
    /**
184
     * Get domain name, URL, API path and database name for the given project.
185
     * @Route("/api/project/normalize/{project}", name="ProjectApiNormalize", methods={"GET"})
186
     * @OA\Tag(name="Project API")
187
     * @OA\Parameter(ref="#/components/parameters/Project")
188
     * @OA\Response(
189
     *     response=200,
190
     *     description="The domain, URL, API path and database name.",
191
     *     @OA\JsonContent(
192
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
193
     *         @OA\Property(property="domain", type="string", example="en.wikipedia.org"),
194
     *         @OA\Property(property="url", type="string", example="https://en.wikipedia.org"),
195
     *         @OA\Property(property="api", type="string", example="https://en.wikipedia.org/w/api.php"),
196
     *         @OA\Property(property="database", type="string", example="enwiki"),
197
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
198
     *     )
199
     * )
200
     * @OA\Response(response=404, ref="#/components/responses/404")
201
     * @OA\Response(response=503, ref="#/components/responses/503")
202
     * @OA\Response(response=504, ref="#/components/responses/504")
203
     * @return JsonResponse
204
     */
205
    public function normalizeProjectApiAction(): JsonResponse
206
    {
207
        return $this->getFormattedApiResponse([
208
            'domain' => $this->project->getDomain(),
209
            'url' => $this->project->getUrl(),
210
            'api' => $this->project->getApiUrl(),
211
            'database' => $this->project->getDatabaseName(),
212
        ]);
213
    }
214
215
    /**
216
     * Get the localized names for each namespaces of the given project.
217
     * @Route("/api/project/namespaces/{project}", name="ProjectApiNamespaces", methods={"GET"})
218
     * @OA\Tag(name="Project API")
219
     * @OA\Parameter(ref="#/components/parameters/Project")
220
     * @OA\Response(
221
     *     response=200,
222
     *     description="List of localized namespaces keyed by their ID.",
223
     *     @OA\JsonContent(
224
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
225
     *         @OA\Property(property="url", type="string", example="https://en.wikipedia.org"),
226
     *         @OA\Property(property="api", type="string", example="https://en.wikipedia.org/w/api.php"),
227
     *         @OA\Property(property="database", type="string", example="enwiki"),
228
     *         @OA\Property(property="namespaces", type="object", example={"0": "", "3": "User talk"}),
229
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
230
     *     )
231
     * )
232
     * @OA\Response(response=404, ref="#/components/responses/404")
233
     * @OA\Response(response=503, ref="#/components/responses/503")
234
     * @OA\Response(response=504, ref="#/components/responses/504")
235
     * @return JsonResponse
236
     */
237
    public function namespacesApiAction(): JsonResponse
238
    {
239
        return $this->getFormattedApiResponse([
240
            'domain' => $this->project->getDomain(),
241
            'url' => $this->project->getUrl(),
242
            'api' => $this->project->getApiUrl(),
243
            'database' => $this->project->getDatabaseName(),
244
            'namespaces' => $this->project->getNamespaces(),
245
        ]);
246
    }
247
248
    /**
249
     * Get page assessment metadata for a project.
250
     * @Route("/api/project/assessments/{project}", name="ProjectApiAssessments", methods={"GET"})
251
     * @OA\Tag(name="Project API")
252
     * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:PageAssessments")
253
     * @OA\Parameter(ref="#/components/parameters/Project")
254
     * @OA\Response(
255
     *     response=200,
256
     *     description="List of classifications and importance levels, along with their associated colours and badges.",
257
     *     @OA\JsonContent(
258
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
259
     *         @OA\Property(property="assessments", type="object", example={
260
     *             "wikiproject_prefix": "Wikipedia:WikiProject ",
261
     *             "class": {
262
     *                 "FA": {
263
     *                     "badge": "b/bc/Featured_article_star.svg",
264
     *                     "color": "#9CBDFF",
265
     *                     "category": "Category:FA-Class articles"
266
     *                 }
267
     *             },
268
     *             "importance": {
269
     *                 "Top": {
270
     *                     "color": "#FF97FF",
271
     *                     "category": "Category:Top-importance articles",
272
     *                     "weight": 5
273
     *                 }
274
     *             }
275
     *         }),
276
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
277
     *     )
278
     * )
279
     * @OA\Response(response=404, ref="#/components/responses/404")
280
     * @return JsonResponse
281
     */
282
    public function projectAssessmentsApiAction(): JsonResponse
283
    {
284
        return $this->getFormattedApiResponse([
285
            'project' => $this->project->getDomain(),
286
            'assessments' => $this->project->getPageAssessments()->getConfig(),
287
        ]);
288
    }
289
290
    /**
291
     * Get assessment metadata for all projects.
292
     * @Route("/api/project/assessments", name="ApiAssessmentsConfig", methods={"GET"})
293
     * @OA\Tag(name="Project API")
294
     * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:PageAssessments")
295
     * @OA\Response(
296
     *     response=200,
297
     *     description="Page assessment metadata for all projects that have
298
                <a href='https://w.wiki/6o9c'>PageAssessments</a> installed.",
299
     *     @OA\JsonContent(
300
     *         @OA\Property(property="projects", type="array", @OA\Items(type="string"),
301
     *              example={"en.wikipedia.org", "fr.wikipedia.org"}
302
     *         ),
303
     *         @OA\Property(property="config", type="object", example={
304
     *             "en.wikipedia.org": {
305
     *                 "wikiproject_prefix": "Wikipedia:WikiProject ",
306
     *                 "class": {
307
     *                     "FA": {
308
     *                         "badge": "b/bc/Featured_article_star.svg",
309
     *                         "color": "#9CBDFF",
310
     *                         "category": "Category:FA-Class articles"
311
     *                     }
312
     *                 },
313
     *                 "importance": {
314
     *                     "Top": {
315
     *                         "color": "#FF97FF",
316
     *                         "category": "Category:Top-importance articles",
317
     *                         "weight": 5
318
     *                     }
319
     *                 }
320
     *             }
321
     *         }),
322
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
323
     *     )
324
     * )
325
     * @return JsonResponse
326
     */
327
    public function assessmentsConfigApiAction(): JsonResponse
328
    {
329
        // Here there is no Project, so we don't use XtoolsController::getFormattedApiResponse().
330
        $response = new JsonResponse();
331
        $response->setEncodingOptions(JSON_NUMERIC_CHECK);
332
        $response->setStatusCode(Response::HTTP_OK);
333
        $response->setData([
334
            'projects' => array_keys($this->getParameter('assessments')),
335
            'config' => $this->getParameter('assessments'),
336
        ]);
337
338
        return $response;
339
    }
340
341
    /**
342
     * Transform given wikitext to HTML using the XTools parser. Wikitext must be passed in as the query 'wikitext'.
343
     * @Route("/api/project/parser/{project}")
344
     * @return JsonResponse Safe HTML.
345
     */
346
    public function wikifyApiAction(): JsonResponse
347
    {
348
        return new JsonResponse(
349
            Edit::wikifyString($this->request->query->get('wikitext', ''), $this->project)
350
        );
351
    }
352
}
353