Wiki::getPageContent()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 6
nop 2
dl 0
loc 26
rs 9.3888
c 0
b 0
f 0
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme\Wiki;
4
5
use BotRiconferme\Exception\APIRequestException;
6
use BotRiconferme\Exception\CannotLoginException;
7
use BotRiconferme\Exception\EditException;
8
use BotRiconferme\Exception\LoginException;
9
use BotRiconferme\Exception\MissingPageException;
10
use BotRiconferme\Exception\MissingSectionException;
11
use BotRiconferme\Request\RequestBase;
12
use BotRiconferme\Request\RequestFactory;
13
use Psr\Log\LoggerInterface;
14
15
/**
16
 * Class for wiki interaction, contains some requests shorthands
17
 */
18
class Wiki {
19
	/** @var bool */
20
	private $loggedIn = false;
21
	/** @var LoggerInterface */
22
	private $logger;
23
	/** @var string[] */
24
	private $tokens;
25
	/** @var LoginInfo */
26
	private $loginInfo;
27
	/** @var RequestFactory */
28
	private $requestFactory;
29
	/** @var string */
30
	private $localUserIdentifier = '';
31
	/** @var string Used for logging */
32
	private $pagePrefix = '';
33
	/** @var string[] */
34
	private $cookies = [];
35
36
	/**
37
	 * @param LoginInfo $li
38
	 * @param LoggerInterface $logger
39
	 * @param RequestFactory $requestFactory
40
	 */
41
	public function __construct(
42
		LoginInfo $li,
43
		LoggerInterface $logger,
44
		RequestFactory $requestFactory
45
	) {
46
		$this->loginInfo = $li;
47
		$this->logger = $logger;
48
		$this->requestFactory = $requestFactory;
49
	}
50
51
	/**
52
	 * @return LoginInfo
53
	 */
54
	public function getLoginInfo(): LoginInfo {
55
		return $this->loginInfo;
56
	}
57
58
	/**
59
	 * @return RequestFactory
60
	 */
61
	public function getRequestFactory(): RequestFactory {
62
		return $this->requestFactory;
63
	}
64
65
	/**
66
	 * @param string $prefix
67
	 */
68
	public function setPagePrefix( string $prefix ): void {
69
		$this->pagePrefix = $prefix;
70
	}
71
72
	/**
73
	 * @param string $ident
74
	 */
75
	public function setLocalUserIdentifier( string $ident ): void {
76
		$this->localUserIdentifier = $ident;
77
	}
78
79
	/**
80
	 * @return string
81
	 */
82
	public function getLocalUserIdentifier(): string {
83
		return $this->localUserIdentifier;
84
	}
85
86
	/**
87
	 * @param string $title
88
	 * @param int|null $section
89
	 */
90
	private function logRead( string $title, int $section = null ): void {
91
		$fullTitle = $this->pagePrefix . $title;
92
		$msg = "Retrieving content of $fullTitle" . ( $section !== null ? ", section $section" : '' );
93
		$this->logger->info( $msg );
94
	}
95
96
	/**
97
	 * Gets the content of a wiki page
98
	 *
99
	 * @param string $title
100
	 * @param int|null $section
101
	 * @return string
102
	 * @throws MissingPageException
103
	 * @throws MissingSectionException
104
	 */
105
	public function getPageContent( string $title, int $section = null ): string {
106
		$this->logRead( $title, $section );
107
		$params = [
108
			'action' => 'query',
109
			'titles' => $title,
110
			'prop' => 'revisions',
111
			'rvslots' => 'main',
112
			'rvprop' => 'content'
113
		];
114
115
		if ( $section !== null ) {
116
			$params['rvsection'] = $section;
117
		}
118
119
		$req = $this->buildRequest( $params );
120
		$page = $req->executeAsQuery()->current();
121
		if ( isset( $page->missing ) ) {
122
			throw new MissingPageException( $title );
123
		}
124
125
		$mainSlot = $page->revisions[0]->slots->main;
126
127
		if ( $section !== null && isset( $mainSlot->nosuchsection ) ) {
128
			throw new MissingSectionException( $title, $section );
129
		}
130
		return $mainSlot->{ '*' };
131
	}
132
133
	/**
134
	 * Basically a wrapper for action=edit
135
	 *
136
	 * @param array $params
137
	 * @phan-param array<int|string|bool> $params
138
	 * @throws EditException
139
	 */
140
	public function editPage( array $params ): void {
141
		$this->login();
142
143
		$params = [
144
			'action' => 'edit',
145
			'token' => $this->getToken( 'csrf' ),
146
		] + $params;
147
148
		if ( BOT_EDITS === true ) {
149
			$params['bot'] = 1;
150
		}
151
152
		$res = $this->buildRequest( $params )->setPost()->executeSingle();
153
154
		$editData = $res->edit;
155
		if ( $editData->result !== 'Success' ) {
156
			if ( isset( $editData->captcha ) ) {
157
				throw new EditException( 'Got captcha!' );
158
			}
159
			throw new EditException( $editData->info ?? reset( $editData ) );
160
		}
161
	}
162
163
	/**
164
	 * Login wrapper. Checks if we're already logged in and clears tokens cache
165
	 * @throws LoginException
166
	 */
167
	public function login(): void {
168
		if ( $this->loginInfo === null ) {
169
			throw new CannotLoginException( 'Missing login data' );
170
		}
171
		if ( $this->loggedIn ) {
172
			return;
173
		}
174
175
		// Yes, this is an easter egg.
176
		$this->logger->info( 'Logging in. Username: BotRiconferme, password: correctHorseBatteryStaple' );
177
178
		$params = [
179
			'action' => 'login',
180
			'lgname' => $this->getLoginInfo()->getUsername(),
181
			'lgpassword' => $this->getLoginInfo()->getPassword(),
182
			'lgtoken' => $this->getToken( 'login' )
183
		];
184
185
		try {
186
			$res = $this->buildRequest( $params )->setPost()->executeSingle();
187
		} catch ( APIRequestException $e ) {
188
			throw new LoginException( $e->getMessage() );
189
		}
190
191
		if ( !isset( $res->login->result ) || $res->login->result !== 'Success' ) {
192
			throw new LoginException( $res->login->reason ?? 'Unknown error' );
193
		}
194
195
		$this->loggedIn = true;
196
		// Clear tokens cache
197
		$this->tokens = [];
198
		$this->logger->info( 'Login succeeded' );
199
	}
200
201
	/**
202
	 * Get a token, cached.
203
	 *
204
	 * @param string $type
205
	 * @return string
206
	 */
207
	public function getToken( string $type ): string {
208
		if ( !isset( $this->tokens[ $type ] ) ) {
209
			$params = [
210
				'action' => 'query',
211
				'meta'   => 'tokens',
212
				'type'   => $type
213
			];
214
			$res = $this->buildRequest( $params )->executeSingle();
215
			$this->tokens[ $type ] = $res->query->tokens->{ "{$type}token" };
216
		}
217
218
		return $this->tokens[ $type ];
219
	}
220
221
	/**
222
	 * Get the timestamp of the creation of the given page
223
	 *
224
	 * @param string $title
225
	 * @return int
226
	 */
227
	public function getPageCreationTS( string $title ): int {
228
		$params = [
229
			'action' => 'query',
230
			'prop' => 'revisions',
231
			'titles' => $title,
232
			'rvprop' => 'timestamp',
233
			'rvslots' => 'main',
234
			'rvlimit' => 1,
235
			'rvdir' => 'newer'
236
		];
237
238
		$page = $this->buildRequest( $params )->executeAsQuery()->current();
239
		return strtotime( $page->revisions[0]->timestamp );
240
	}
241
242
	/**
243
	 * Sysop-level inifinite protection for a given page
244
	 *
245
	 * @param string $title
246
	 * @param string $reason
247
	 */
248
	public function protectPage( string $title, string $reason ): void {
249
		$fullTitle = $this->pagePrefix . $title;
250
		$this->logger->info( "Protecting page $fullTitle" );
251
		$this->login();
252
253
		$params = [
254
			'action' => 'protect',
255
			'title' => $title,
256
			'protections' => 'edit=sysop|move=sysop',
257
			'expiry' => 'infinite',
258
			'reason' => $reason,
259
			'token' => $this->getToken( 'csrf' )
260
		];
261
262
		$this->buildRequest( $params )->setPost()->executeSingle();
263
	}
264
265
	/**
266
	 * Block a user, infinite expiry
267
	 *
268
	 * @param string $username
269
	 * @param string $reason
270
	 */
271
	public function blockUser( string $username, string $reason ): void {
272
		$this->logger->info( "Blocking user $username" );
273
		$this->login();
274
275
		$params = [
276
			'action' => 'block',
277
			// Don't allow talk page edit 'allowusertalk' => 1,
278
			'autoblock' => 1,
279
			'nocreate' => 1,
280
			'expiry' => 'indefinite',
281
			// No anononly
282
			'noemail' => 1,
283
			// No reblock
284
			'reason' => $reason,
285
			'user' => $username,
286
			'token' => $this->getToken( 'csrf' )
287
		];
288
289
		$this->buildRequest( $params )->setPost()->executeSingle();
290
	}
291
292
	/**
293
	 * Shorthand
294
	 * @param array $params
295
	 * @phan-param array<int|string|bool> $params
296
	 * @return RequestBase
297
	 */
298
	private function buildRequest( array $params ): RequestBase {
299
		return $this->requestFactory->createRequest(
300
			$params,
301
			$this->cookies,
302
			/** @param string[] $newCookies */
303
			function ( array $newCookies ) {
304
				$this->cookies = $newCookies + $this->cookies;
305
			}
306
		);
307
	}
308
}
309