Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.29% covered (warning)
64.29%
18 / 28
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ShellboxClientFactory
64.29% covered (warning)
64.29%
18 / 28
66.67% covered (warning)
66.67%
4 / 6
18.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClient
30.77% covered (danger)
30.77%
4 / 13
0.00% covered (danger)
0.00%
0 / 1
3.33
 getRemoteRpcClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRpcClient
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getUrl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace MediaWiki\Shell;
4
5use GuzzleHttp\Psr7\Uri;
6use GuzzleHttp\RequestOptions;
7use MediaWiki\Http\HttpRequestFactory;
8use RuntimeException;
9use Shellbox\Client;
10use Shellbox\RPC\LocalRpcClient;
11use Shellbox\RPC\RpcClient;
12
13/**
14 * This is a service which provides a configured client to access a remote
15 * Shellbox installation.
16 *
17 * @since 1.36
18 */
19class ShellboxClientFactory {
20    /** @var HttpRequestFactory */
21    private $requestFactory;
22    /** @var (string|false|null)[]|null */
23    private $urls;
24    /** @var string|null */
25    private $key;
26
27    /** The default request timeout, in seconds */
28    public const DEFAULT_TIMEOUT = 10;
29
30    /**
31     * @internal Use MediaWikiServices::getShellboxClientFactory()
32     * @param HttpRequestFactory $requestFactory The factory which will be used
33     *   to make HTTP clients.
34     * @param (string|false|null)[]|null $urls The Shellbox base URL mapping
35     * @param string|null $key The shared secret key used for HMAC authentication
36     */
37    public function __construct( HttpRequestFactory $requestFactory, $urls, $key ) {
38        $this->requestFactory = $requestFactory;
39        $this->urls = $urls;
40        $this->key = $key;
41    }
42
43    /**
44     * Test whether remote Shellbox is enabled by configuration.
45     *
46     * @param string|null $service Same as the service option for getClient.
47     * @return bool
48     */
49    public function isEnabled( ?string $service = null ): bool {
50        return $this->getUrl( $service ) !== null;
51    }
52
53    /**
54     * Get a Shellbox client with the specified options. If remote Shellbox is
55     * not configured (isEnabled() returns false), an exception will be thrown.
56     *
57     * @param array $options Associative array of options:
58     *   - timeout: The request timeout in seconds
59     *   - service: the shellbox backend name to get the URL from the mapping
60     * @return Client
61     * @throws RuntimeException
62     */
63    public function getClient( array $options = [] ) {
64        $url = $this->getUrl( $options['service'] ?? null );
65        if ( $url === null ) {
66            throw new RuntimeException( 'To use a remote shellbox to run shell commands, ' .
67                '$wgShellboxUrls and $wgShellboxSecretKey must be configured.' );
68        }
69
70        return new Client(
71            $this->requestFactory->createGuzzleClient( [
72                RequestOptions::TIMEOUT => $options['timeout'] ?? self::DEFAULT_TIMEOUT,
73                RequestOptions::HTTP_ERRORS => false,
74            ] ),
75            new Uri( $url ),
76            $this->key,
77            [ 'allowUrlFiles' => true ]
78        );
79    }
80
81    /**
82     * Get a Shellbox RPC client with the specified options. If remote Shellbox is
83     * not configured (isEnabled() returns false), an exception will be thrown.
84     *
85     * @param array $options Associative array of options:
86     *   - timeout: The request timeout in seconds
87     *   - service: the shellbox backend name to get the URL from the mapping
88     * @return RpcClient
89     * @throws RuntimeException
90     */
91    public function getRemoteRpcClient( array $options = [] ): RpcClient {
92        return $this->getClient( $options );
93    }
94
95    /**
96     * Get a Shellbox RPC client with specified options. If remote Shellbox is
97     * not configured (isEnabled() returns false), a local fallback is returned.
98     *
99     * @param array $options
100     * @return RpcClient
101     */
102    public function getRpcClient( array $options = [] ): RpcClient {
103        $url = $this->getUrl( $options['service'] ?? null );
104        if ( $url === null ) {
105            return new LocalRpcClient();
106        }
107        return $this->getRemoteRpcClient( $options );
108    }
109
110    private function getUrl( ?string $service ): ?string {
111        if ( $this->urls === null || $this->key === null || $this->key === '' ) {
112            return null;
113        }
114        // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable False positive
115        $url = $this->urls[$service] ?? $this->urls['default'] ?? null;
116        if ( !is_string( $url ) ) {
117            return null;
118        }
119        return $url;
120    }
121
122}