Passed
Push — master ( 1d252d...bd204f )
by Daimona
01:55
created

Wiki::getRequestFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
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 static $loggedIn = false;
21
	/** @var LoggerInterface */
22
	private $logger;
23
	/** @var string[] */
24
	private $tokens;
25
	/** @var LoginInfo|null */
26
	private $loginInfo;
27
	/** @var bool Whether our edits are bot edits */
28
	private $botEdits;
29
	/** @var RequestFactory */
30
	private $requestFactory;
31
	/** @var string */
32
	private $localUserIdentifier = '';
33
34
	/**
35
	 * @param LoginInfo $li
36
	 * @param LoggerInterface $logger
37
	 * @param RequestFactory $requestFactory
38
	 */
39
	public function __construct(
40
		LoginInfo $li,
41
		LoggerInterface $logger,
42
		RequestFactory $requestFactory
43
	) {
44
		$this->loginInfo = $li;
45
		$this->logger = $logger;
46
		$this->requestFactory = $requestFactory;
47
	}
48
49
	/**
50
	 * @return LoginInfo
51
	 */
52
	public function getLoginInfo() : LoginInfo {
53
		return $this->loginInfo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->loginInfo could return the type null which is incompatible with the type-hinted return BotRiconferme\Wiki\LoginInfo. Consider adding an additional type-check to rule them out.
Loading history...
54
	}
55
56
	/**
57
	 * @param bool $bot
58
	 */
59
	public function setEditsAsBot( bool $bot ) : void {
60
		// FIXME same as setLoginInfo
61
		$this->botEdits = $bot;
62
	}
63
64
	/**
65
	 * @return bool
66
	 */
67
	public function getEditsAsBot() : bool {
68
		return $this->botEdits;
69
	}
70
71
	/**
72
	 * @return RequestFactory
73
	 */
74
	public function getRequestFactory() : RequestFactory {
75
		return $this->requestFactory;
76
	}
77
78
	/**
79
	 * @param string $ident
80
	 */
81
	public function setLocalUserIdentifier( string $ident ) : void {
82
		$this->localUserIdentifier = $ident;
83
	}
84
85
	/**
86
	 * @return string
87
	 */
88
	public function getLocalUserIdentifier() : string {
89
		return $this->localUserIdentifier;
90
	}
91
92
	/**
93
	 * Gets the content of a wiki page
94
	 *
95
	 * @param string $title
96
	 * @param int|null $section
97
	 * @return string
98
	 * @throws MissingPageException
99
	 * @throws MissingSectionException
100
	 */
101
	public function getPageContent( string $title, int $section = null ) : string {
102
		$msg = "Retrieving content of $title" . ( $section !== null ? ", section $section" : '' );
103
		$this->logger->debug( $msg );
104
		$params = [
105
			'action' => 'query',
106
			'titles' => $title,
107
			'prop' => 'revisions',
108
			'rvslots' => 'main',
109
			'rvprop' => 'content'
110
		];
111
112
		if ( $section !== null ) {
113
			$params['rvsection'] = $section;
114
		}
115
116
		$req = $this->buildRequest( $params );
117
		$data = $req->execute();
118
		$page = reset( $data->query->pages );
119
		if ( isset( $page->missing ) ) {
120
			throw new MissingPageException( $title );
121
		}
122
123
		$mainSlot = $page->revisions[0]->slots->main;
124
125
		if ( $section !== null && isset( $mainSlot->nosuchsection ) ) {
126
			throw new MissingSectionException( $title, $section );
127
		}
128
		return $mainSlot->{ '*' };
129
	}
130
131
	/**
132
	 * Basically a wrapper for action=edit
133
	 *
134
	 * @param array $params
135
	 * @throws EditException
136
	 */
137
	public function editPage( array $params ) : void {
138
		$this->login();
139
140
		$params = [
141
			'action' => 'edit',
142
			'token' => $this->getToken( 'csrf' ),
143
		] + $params;
144
145
		if ( $this->getEditsAsBot() ) {
146
			$params['bot'] = 1;
147
		}
148
149
		$res = $this->buildRequest( $params )->setPost()->execute();
150
151
		$editData = $res->edit;
152
		if ( $editData->result !== 'Success' ) {
153
			if ( isset( $editData->captcha ) ) {
154
				throw new EditException( 'Got captcha!' );
155
			}
156
			throw new EditException( $editData->info ?? reset( $editData ) );
157
		}
158
	}
159
160
	/**
161
	 * Login wrapper. Checks if we're already logged in and clears tokens cache
162
	 * @throws LoginException
163
	 */
164
	public function login() : void {
165
		if ( $this->loginInfo === null ) {
166
			throw new CannotLoginException( 'Missing login data' );
167
		}
168
		if ( self::$loggedIn ) {
169
			return;
170
		}
171
172
		// Yes, this is an easter egg.
173
		$this->logger->info( 'Logging in. Username: BotRiconferme, password: correctHorseBatteryStaple' );
174
175
		$params = [
176
			'action' => 'login',
177
			'lgname' => $this->getLoginInfo()->getUsername(),
178
			'lgpassword' => $this->getLoginInfo()->getPassword(),
179
			'lgtoken' => $this->getToken( 'login' )
180
		];
181
182
		try {
183
			$res = $this->buildRequest( $params )->setPost()->execute();
184
		} catch ( APIRequestException $e ) {
185
			throw new LoginException( $e->getMessage() );
186
		}
187
188
		if ( !isset( $res->login->result ) || $res->login->result !== 'Success' ) {
189
			throw new LoginException( $res->login->reason ?? 'Unknown error' );
190
		}
191
192
		self::$loggedIn = true;
193
		// Clear tokens cache
194
		$this->tokens = [];
195
		$this->logger->info( 'Login succeeded' );
196
	}
197
198
	/**
199
	 * Get a token, cached.
200
	 *
201
	 * @param string $type
202
	 * @return string
203
	 */
204
	public function getToken( string $type ) : string {
205
		if ( !isset( $this->tokens[ $type ] ) ) {
206
			$params = [
207
				'action' => 'query',
208
				'meta'   => 'tokens',
209
				'type'   => $type
210
			];
211
212
			$req = $this->buildRequest( $params );
213
			$res = $req->execute();
214
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
		$res = $this->buildRequest( $params )->execute();
239
		$data = $res->query->pages;
240
		return strtotime( reset( $data )->revisions[0]->timestamp );
241
	}
242
243
	/**
244
	 * Sysop-level inifinite protection for a given page
245
	 *
246
	 * @param string $title
247
	 * @param string $reason
248
	 */
249
	public function protectPage( string $title, string $reason ) : void {
250
		$this->logger->info( "Protecting page $title" );
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()->execute();
263
	}
264
265
	/**
266
	 * Shorthand
267
	 * @param array $params
268
	 * @return RequestBase
269
	 */
270
	private function buildRequest( array $params ) : RequestBase {
271
		return $this->requestFactory->newFromParams( $params );
272
	}
273
}
274