Passed
Push — master ( cc9b5c...1b315c )
by Daimona
01:50
created

Wiki   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 197
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 87
c 3
b 1
f 0
dl 0
loc 197
rs 10
wmc 21

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getPageContent() 0 29 6
A __construct() 0 3 1
A getToken() 0 15 2
A editPage() 0 20 4
A login() 0 29 5
A getPageCreationTS() 0 14 1
A buildRequest() 0 2 1
A protectPage() 0 14 1
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme\Wiki;
4
5
use BotRiconferme\Config;
6
use BotRiconferme\Exception\EditException;
7
use BotRiconferme\Exception\LoginException;
8
use BotRiconferme\Exception\APIRequestException;
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
27
	/**
28
	 * @param LoggerInterface $logger
29
	 * @param string $domain The URL of the wiki, if different from default
30
	 */
31
	public function __construct( LoggerInterface $logger, string $domain = DEFAULT_URL ) {
32
		$this->logger = $logger;
33
		$this->domain = $domain;
34
	}
35
36
	/**
37
	 * Gets the content of a wiki page
38
	 *
39
	 * @param string $title
40
	 * @param int|null $section
41
	 * @return string
42
	 * @throws MissingPageException
43
	 * @throws MissingSectionException
44
	 */
45
	public function getPageContent( string $title, int $section = null ) : string {
46
		$msg = "Retrieving content of $title" . ( $section !== null ? ", section $section" : '' );
47
		$this->logger->debug( $msg );
48
		$params = [
49
			'action' => 'query',
50
			'titles' => $title,
51
			'prop' => 'revisions',
52
			'rvslots' => 'main',
53
			'rvprop' => 'content'
54
		];
55
56
		if ( $section !== null ) {
57
			$params['rvsection'] = $section;
58
		}
59
60
		$req = $this->buildRequest( $params );
61
		$data = $req->execute();
62
		$page = reset( $data->query->pages );
63
		if ( isset( $page->missing ) ) {
64
			throw new MissingPageException( $title );
65
		}
66
67
		$mainSlot = $page->revisions[0]->slots->main;
68
69
		// @phan-suppress-next-line PhanImpossibleTypeComparison $section can be null
70
		if ( $section !== null && isset( $mainSlot->nosuchsection ) ) {
71
			throw new MissingSectionException( $title, $section );
72
		}
73
		return $mainSlot->{ '*' };
74
	}
75
76
	/**
77
	 * Basically a wrapper for action=edit
78
	 *
79
	 * @param array $params
80
	 * @throws EditException
81
	 */
82
	public function editPage( array $params ) {
83
		$this->login();
84
85
		$params = [
86
			'action' => 'edit',
87
			'token' => $this->getToken( 'csrf' ),
88
		] + $params;
89
90
		if ( Config::getInstance()->get( 'bot-edits' ) ) {
91
			$params['bot'] = 1;
92
		}
93
94
		$res = $this->buildRequest( $params )->setPost()->execute();
95
96
		$editData = $res->edit;
97
		if ( $editData->result !== 'Success' ) {
98
			if ( isset( $editData->captcha ) ) {
99
				throw new EditException( 'Got captcha!' );
100
			}
101
			throw new EditException( $editData->info ?? reset( $editData ) );
102
		}
103
	}
104
105
	/**
106
	 * Login wrapper. Checks if we're already logged in and clears tokens cache
107
	 * @throws LoginException
108
	 */
109
	public function login() {
110
		if ( self::$loggedIn ) {
111
			return;
112
		}
113
114
		// Yes, this is an easter egg.
115
		$this->logger->info( 'Logging in. Username: BotRiconferme, password: correctHorseBatteryStaple' );
116
117
		$params = [
118
			'action' => 'login',
119
			'lgname' => Config::getInstance()->get( 'username' ),
120
			'lgpassword' => Config::getInstance()->get( 'password' ),
121
			'lgtoken' => $this->getToken( 'login' )
122
		];
123
124
		try {
125
			$res = $this->buildRequest( $params )->setPost()->execute();
126
		} catch ( APIRequestException $e ) {
127
			throw new LoginException( $e->getMessage() );
128
		}
129
130
		if ( !isset( $res->login->result ) || $res->login->result !== 'Success' ) {
131
			throw new LoginException( 'Unknown error' );
132
		}
133
134
		self::$loggedIn = true;
135
		// Clear tokens cache
136
		$this->tokens = [];
137
		$this->logger->info( 'Login succeeded' );
138
	}
139
140
	/**
141
	 * Get a token, cached.
142
	 *
143
	 * @param string $type
144
	 * @return string
145
	 */
146
	public function getToken( string $type ) : string {
147
		if ( !isset( $this->tokens[ $type ] ) ) {
148
			$params = [
149
				'action' => 'query',
150
				'meta'   => 'tokens',
151
				'type'   => $type
152
			];
153
154
			$req = $this->buildRequest( $params );
155
			$res = $req->execute();
156
157
			$this->tokens[ $type ] = $res->query->tokens->{ "{$type}token" };
158
		}
159
160
		return $this->tokens[ $type ];
161
	}
162
163
	/**
164
	 * Get the timestamp of the creation of the given page
165
	 *
166
	 * @param string $title
167
	 * @return int
168
	 */
169
	public function getPageCreationTS( string $title ) : int {
170
		$params = [
171
			'action' => 'query',
172
			'prop' => 'revisions',
173
			'titles' => $title,
174
			'rvprop' => 'timestamp',
175
			'rvslots' => 'main',
176
			'rvlimit' => 1,
177
			'rvdir' => 'newer'
178
		];
179
180
		$res = $this->buildRequest( $params )->execute();
181
		$data = $res->query->pages;
182
		return strtotime( reset( $data )->revisions[0]->timestamp );
183
	}
184
185
	/**
186
	 * Sysop-level inifinite protection for a given page
187
	 *
188
	 * @param string $title
189
	 * @param string $reason
190
	 */
191
	public function protectPage( string $title, string $reason ) {
192
		$this->logger->info( "Protecting page $title" );
193
		$this->login();
194
195
		$params = [
196
			'action' => 'protect',
197
			'title' => $title,
198
			'protections' => 'edit=sysop|move=sysop',
199
			'expiry' => 'infinite',
200
			'reason' => $reason,
201
			'token' => $this->getToken( 'csrf' )
202
		];
203
204
		$this->buildRequest( $params )->setPost()->execute();
205
	}
206
207
	/**
208
	 * Shorthand
209
	 * @param array $params
210
	 * @return RequestBase
211
	 */
212
	private function buildRequest( array $params ) : RequestBase {
213
		return RequestBase::newFromParams( $params )->setUrl( $this->domain );
214
	}
215
}
216