Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.62% covered (warning)
84.62%
33 / 39
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResponseFactory
84.62% covered (warning)
84.62%
33 / 39
66.67% covered (warning)
66.67%
4 / 6
8.23
0.00% covered (danger)
0.00%
0 / 1
 newSuccessResponse
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 newHttpExceptionFromError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 newErrorResponseFromException
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newErrorResponse
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 newFrameworkAlikePermissionDeniedResponse
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 lookupHttpStatus
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php declare( strict_types=1 );
2
3namespace Wikibase\Lexeme\MediaWiki\RestApi;
4
5use LogicException;
6use MediaWiki\Rest\HttpException;
7use MediaWiki\Rest\Response;
8use MediaWiki\Rest\StringStream;
9use Wikibase\Lexeme\Interactors\UseCaseError;
10use Wikimedia\Http\HttpStatus;
11use Wikimedia\Timestamp\ConvertibleTimestamp;
12use Wikimedia\Timestamp\TimestampFormat as TS;
13
14/**
15 * @license GPL-2.0-or-later
16 */
17class ResponseFactory {
18
19    private const array HTTP_STATUS_LOOKUP_TABLE = [
20        // 400 errors:
21        UseCaseError::INVALID_PATH_PARAMETER => 400,
22        UseCaseError::MISSING_FIELD => 400,
23        UseCaseError::INVALID_VALUE => 400,
24        UseCaseError::INVALID_KEY => 400,
25        UseCaseError::VALUE_TOO_LONG => 400,
26        UseCaseError::REFERENCED_RESOURCE_NOT_FOUND => 400,
27        UseCaseError::RESOURCE_TOO_LARGE => 400,
28        UseCaseError::STATEMENT_GROUP_PROPERTY_ID_MISMATCH => 400,
29        // 404 errors:
30        UseCaseError::RESOURCE_NOT_FOUND => 404,
31        // 403 errors:
32        UseCaseError::PERMISSION_DENIED => 403,
33        // 429 errors:
34        UseCaseError::REQUEST_LIMIT_REACHED => 429,
35        // 409 errors:
36        UseCaseError::REDIRECTED_LEXEME => 409,
37    ];
38
39    public function newSuccessResponse(
40        string $body,
41        int $revId,
42        string $lastModified,
43        int $statusCode = 200,
44    ): Response {
45        $httpResponse = new Response();
46        $httpResponse->setStatus( $statusCode );
47        $httpResponse->setHeader( 'Content-Type', 'application/json' );
48        $httpResponse->setHeader( 'Last-Modified', ConvertibleTimestamp::convert( TS::RFC2822, $lastModified ) );
49        $httpResponse->setHeader( 'ETag', "\"$revId\"" );
50        $httpResponse->setBody( new StringStream( $body ) );
51
52        return $httpResponse;
53    }
54
55    public function newHttpExceptionFromError( UseCaseError $e ): HttpException {
56        return new HttpException(
57            $e->errorMessage,
58            $this->lookupHttpStatus( $e->errorCode ),
59            [ 'code' => $e->errorCode, 'context' => $e->context ],
60        );
61    }
62
63    public function newErrorResponseFromException( UseCaseError $e ): Response {
64        return $this->newErrorResponse( $e->errorCode, $e->errorMessage, $e->context );
65    }
66
67    private function newErrorResponse( string $code, string $message, array $context = [] ): Response {
68        if ( $code === UseCaseError::PERMISSION_DENIED_UNKNOWN_REASON ) {
69            return $this->newFrameworkAlikePermissionDeniedResponse();
70        }
71
72        $httpResponse = new Response();
73        $httpResponse->setHeader( 'Content-Type', 'application/json' );
74        $httpResponse->setHeader( 'Content-Language', 'en' );
75        $httpResponse->setStatus( $this->lookupHttpStatus( $code ) );
76        $httpResponse->setBody(
77            new StringStream( json_encode(
78                // array_filter drops 'context' from the body when it is empty
79                array_filter( [ 'code' => $code, 'message' => $message, 'context' => $context ] ),
80                JSON_UNESCAPED_SLASHES
81            ) )
82        );
83
84        return $httpResponse;
85    }
86
87    private function newFrameworkAlikePermissionDeniedResponse(): Response {
88        $httpResponse = new Response();
89        $httpResponse->setHeader( 'Content-Type', 'application/json' );
90        $httpResponse->setHeader( 'Content-Language', 'en' );
91        $httpResponse->setStatus( 403 );
92        $httpResponse->setBody( new StringStream( json_encode( [
93            'error' => 'rest-write-denied',
94            'httpCode' => 403,
95            'httpReason' => HttpStatus::getMessage( 403 ),
96        ] ) ) );
97
98        return $httpResponse;
99    }
100
101    private function lookupHttpStatus( string $errorCode ): int {
102        if ( !array_key_exists( $errorCode, self::HTTP_STATUS_LOOKUP_TABLE ) ) {
103            throw new LogicException( "Error code '$errorCode' not found in lookup table" );
104        }
105        return self::HTTP_STATUS_LOOKUP_TABLE[$errorCode];
106    }
107
108}