x-tools /
xtools
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 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
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
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
Loading history...
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
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
$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
Used in request-header context
General Strategies to prevent injectionIn 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
Used in variable context
General Strategies to prevent injectionIn 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 |