Passed
Push — master ( a192f6...3a1f1a )
by Daimona
02:01
created

Wiki::setEditsAsBot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
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 1
dl 0
loc 3
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 Psr\Log\LoggerInterface;
13
14
/**
15
 * Class for wiki interaction, contains some requests shorthands
16
 */
17
class Wiki {
18
	/** @var bool */
19
	private static $loggedIn = false;
20
	/** @var LoggerInterface */
21
	private $logger;
22
	/** @var string */
23
	private $domain;
24
	/** @var string[] */
25
	private $tokens;
26
	/** @var LoginInfo|null */
27
	private $loginInfo;
28
	/** @var bool Whether our edits are bot edits */
29
	private $botEdits;
30
31
	/**
32
	 * @param LoggerInterface $logger
33
	 * @param string $domain The URL of the wiki, if different from default
34
	 */
35
	public function __construct( LoggerInterface $logger, string $domain = DEFAULT_URL ) {
36
		$this->logger = $logger;
37
		$this->domain = $domain;
38
	}
39
40
	/**
41
	 * @param LoginInfo $li
42
	 */
43
	public function setLoginInfo( LoginInfo $li ) : void {
44
		// FIXME This should be in the constructor, and it should not depend on config
45
		$this->loginInfo = $li;
46
	}
47
48
	/**
49
	 * @return LoginInfo
50
	 */
51
	public function getLoginInfo() : LoginInfo {
52
		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...
53
	}
54
55
	/**
56
	 * @param bool $bot
57
	 */
58
	public function setEditsAsBot( bool $bot ) : void {
59
		// FIXME same as setLoginInfo
60
		$this->botEdits = $bot;
61
	}
62
63
	/**
64
	 * @return bool
65
	 */
66
	public function getEditsAsBot() : bool {
67
		return $this->botEdits;
68
	}
69
70
	/**
71
	 * Gets the content of a wiki page
72
	 *
73
	 * @param string $title
74
	 * @param int|null $section
75
	 * @return string
76
	 * @throws MissingPageException
77
	 * @throws MissingSectionException
78
	 */
79
	public function getPageContent( string $title, int $section = null ) : string {
80
		$msg = "Retrieving content of $title" . ( $section !== null ? ", section $section" : '' );
81
		$this->logger->debug( $msg );
82
		$params = [
83
			'action' => 'query',
84
			'titles' => $title,
85
			'prop' => 'revisions',
86
			'rvslots' => 'main',
87
			'rvprop' => 'content'
88
		];
89
90
		if ( $section !== null ) {
91
			$params['rvsection'] = $section;
92
		}
93
94
		$req = $this->buildRequest( $params );
95
		$data = $req->execute();
96
		$page = reset( $data->query->pages );
97
		if ( isset( $page->missing ) ) {
98
			throw new MissingPageException( $title );
99
		}
100
101
		$mainSlot = $page->revisions[0]->slots->main;
102
103
		if ( $section !== null && isset( $mainSlot->nosuchsection ) ) {
104
			throw new MissingSectionException( $title, $section );
105
		}
106
		return $mainSlot->{ '*' };
107
	}
108
109
	/**
110
	 * Basically a wrapper for action=edit
111
	 *
112
	 * @param array $params
113
	 * @throws EditException
114
	 */
115
	public function editPage( array $params ) : void {
116
		$this->login();
117
118
		$params = [
119
			'action' => 'edit',
120
			'token' => $this->getToken( 'csrf' ),
121
		] + $params;
122
123
		if ( $this->getEditsAsBot() ) {
124
			$params['bot'] = 1;
125
		}
126
127
		$res = $this->buildRequest( $params )->setPost()->execute();
128
129
		$editData = $res->edit;
130
		if ( $editData->result !== 'Success' ) {
131
			if ( isset( $editData->captcha ) ) {
132
				throw new EditException( 'Got captcha!' );
133
			}
134
			throw new EditException( $editData->info ?? reset( $editData ) );
135
		}
136
	}
137
138
	/**
139
	 * Login wrapper. Checks if we're already logged in and clears tokens cache
140
	 * @throws LoginException
141
	 */
142
	public function login() : void {
143
		if ( $this->loginInfo === null ) {
144
			throw new CannotLoginException( 'Missing login data' );
145
		}
146
		if ( self::$loggedIn ) {
147
			return;
148
		}
149
150
		// Yes, this is an easter egg.
151
		$this->logger->info( 'Logging in. Username: BotRiconferme, password: correctHorseBatteryStaple' );
152
153
		$params = [
154
			'action' => 'login',
155
			'lgname' => $this->getLoginInfo()->getUsername(),
156
			'lgpassword' => $this->getLoginInfo()->getPassword(),
157
			'lgtoken' => $this->getToken( 'login' )
158
		];
159
160
		try {
161
			$res = $this->buildRequest( $params )->setPost()->execute();
162
		} catch ( APIRequestException $e ) {
163
			throw new LoginException( $e->getMessage() );
164
		}
165
166
		if ( !isset( $res->login->result ) || $res->login->result !== 'Success' ) {
167
			throw new LoginException( $res->login->reason ?? 'Unknown error' );
168
		}
169
170
		self::$loggedIn = true;
171
		// Clear tokens cache
172
		$this->tokens = [];
173
		$this->logger->info( 'Login succeeded' );
174
	}
175
176
	/**
177
	 * Get a token, cached.
178
	 *
179
	 * @param string $type
180
	 * @return string
181
	 */
182
	public function getToken( string $type ) : string {
183
		if ( !isset( $this->tokens[ $type ] ) ) {
184
			$params = [
185
				'action' => 'query',
186
				'meta'   => 'tokens',
187
				'type'   => $type
188
			];
189
190
			$req = $this->buildRequest( $params );
191
			$res = $req->execute();
192
193
			$this->tokens[ $type ] = $res->query->tokens->{ "{$type}token" };
194
		}
195
196
		return $this->tokens[ $type ];
197
	}
198
199
	/**
200
	 * Get the timestamp of the creation of the given page
201
	 *
202
	 * @param string $title
203
	 * @return int
204
	 */
205
	public function getPageCreationTS( string $title ) : int {
206
		$params = [
207
			'action' => 'query',
208
			'prop' => 'revisions',
209
			'titles' => $title,
210
			'rvprop' => 'timestamp',
211
			'rvslots' => 'main',
212
			'rvlimit' => 1,
213
			'rvdir' => 'newer'
214
		];
215
216
		$res = $this->buildRequest( $params )->execute();
217
		$data = $res->query->pages;
218
		return strtotime( reset( $data )->revisions[0]->timestamp );
219
	}
220
221
	/**
222
	 * Sysop-level inifinite protection for a given page
223
	 *
224
	 * @param string $title
225
	 * @param string $reason
226
	 */
227
	public function protectPage( string $title, string $reason ) : void {
228
		$this->logger->info( "Protecting page $title" );
229
		$this->login();
230
231
		$params = [
232
			'action' => 'protect',
233
			'title' => $title,
234
			'protections' => 'edit=sysop|move=sysop',
235
			'expiry' => 'infinite',
236
			'reason' => $reason,
237
			'token' => $this->getToken( 'csrf' )
238
		];
239
240
		$this->buildRequest( $params )->setPost()->execute();
241
	}
242
243
	/**
244
	 * Shorthand
245
	 * @param array $params
246
	 * @return RequestBase
247
	 */
248
	private function buildRequest( array $params ) : RequestBase {
249
		return RequestBase::newFromParams( $params )->setUrl( $this->domain );
250
	}
251
}
252