1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * @group large
7 * @group Upload
8 * @group Database
9 *
10 * @covers UploadFromUrl
11 */
12class UploadFromUrlTest extends ApiTestCase {
13	use MockHttpTrait;
14
15	private $user;
16
17	protected function setUp(): void {
18		parent::setUp();
19		$this->user = self::$users['sysop']->getUser();
20
21		$this->setMwGlobals( [
22			'wgEnableUploads' => true,
23			'wgAllowCopyUploads' => true,
24		] );
25		$this->setGroupPermissions( 'sysop', 'upload_by_url', true );
26
27		if ( MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
28			->newFile( 'UploadFromUrlTest.png' )->exists()
29		) {
30			$this->deleteFile( 'UploadFromUrlTest.png' );
31		}
32
33		$this->installMockHttp();
34	}
35
36	/**
37	 * Ensure that the job queue is empty before continuing
38	 */
39	public function testClearQueue() {
40		$job = JobQueueGroup::singleton()->pop();
41		while ( $job ) {
42			$job = JobQueueGroup::singleton()->pop();
43		}
44		$this->assertFalse( $job );
45	}
46
47	public function testIsAllowedHostEmpty() {
48		$this->setMwGlobals( [
49			'wgCopyUploadsDomains' => [],
50		] );
51
52		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://foo.bar' ) );
53	}
54
55	public function testIsAllowedHostDirectMatch() {
56		$this->setMwGlobals( [
57			'wgCopyUploadsDomains' => [
58				'foo.baz',
59				'bar.example.baz',
60			],
61		] );
62
63		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://example.com' ) );
64
65		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://foo.baz' ) );
66		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://.foo.baz' ) );
67
68		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://example.baz' ) );
69		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://bar.example.baz' ) );
70	}
71
72	public function testIsAllowedHostLastWildcard() {
73		$this->setMwGlobals( [
74			'wgCopyUploadsDomains' => [
75				'*.baz',
76			],
77		] );
78
79		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://baz' ) );
80		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.example' ) );
81		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.example.baz' ) );
82		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo/bar.baz' ) );
83
84		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://foo.baz' ) );
85		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://subdomain.foo.baz' ) );
86	}
87
88	public function testIsAllowedHostWildcardInMiddle() {
89		$this->setMwGlobals( [
90			'wgCopyUploadsDomains' => [
91				'foo.*.baz',
92			],
93		] );
94
95		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.baz' ) );
96		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.bar.bar.baz' ) );
97		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.bar.baz.baz' ) );
98		$this->assertFalse( UploadFromUrl::isAllowedHost( 'https://foo.com/.baz' ) );
99
100		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://foo.example.baz' ) );
101		$this->assertTrue( UploadFromUrl::isAllowedHost( 'https://foo.bar.baz' ) );
102	}
103
104	/**
105	 * @depends testClearQueue
106	 */
107	public function testSetupUrlDownload( $data ) {
108		$token = $this->user->getEditToken();
109		$exception = false;
110
111		try {
112			$this->doApiRequest( [
113				'action' => 'upload',
114			] );
115		} catch ( ApiUsageException $e ) {
116			$exception = true;
117			$this->assertEquals( 'The "token" parameter must be set.', $e->getMessage() );
118		}
119		$this->assertTrue( $exception, "Got exception" );
120
121		$exception = false;
122		try {
123			$this->doApiRequest( [
124				'action' => 'upload',
125				'token' => $token,
126			], $data );
127		} catch ( ApiUsageException $e ) {
128			$exception = true;
129			$this->assertEquals( 'One of the parameters "filekey", "file" and "url" is required.',
130				$e->getMessage() );
131		}
132		$this->assertTrue( $exception, "Got exception" );
133
134		$exception = false;
135		try {
136			$this->doApiRequest( [
137				'action' => 'upload',
138				'url' => 'http://www.example.com/test.png',
139				'token' => $token,
140			], $data );
141		} catch ( ApiUsageException $e ) {
142			$exception = true;
143			$this->assertEquals( 'The "filename" parameter must be set.', $e->getMessage() );
144		}
145		$this->assertTrue( $exception, "Got exception" );
146
147		$this->getServiceContainer()->getUserGroupManager()->removeUserFromGroup( $this->user, 'sysop' );
148		$exception = false;
149		try {
150			$this->doApiRequest( [
151				'action' => 'upload',
152				'url' => 'http://www.example.com/test.png',
153				'filename' => 'UploadFromUrlTest.png',
154				'token' => $token,
155			], $data );
156		} catch ( ApiUsageException $e ) {
157			$exception = true;
158			// Two error messages are possible depending on the number of groups in the wiki with upload rights:
159			// - The action you have requested is limited to users in the group:
160			// - The action you have requested is limited to users in one of the groups:
161			$this->assertStringStartsWith( "The action you have requested is limited to users in",
162				$e->getMessage() );
163		}
164		$this->assertTrue( $exception, "Got exception" );
165	}
166
167	private function assertUploadOk( UploadBase $upload ) {
168		$verificationResult = $upload->verifyUpload();
169
170		if ( $verificationResult['status'] !== UploadBase::OK ) {
171			$this->fail(
172				'Upload verification returned ' . $upload->getVerificationErrorCode(
173					$verificationResult['status']
174				)
175			);
176		}
177	}
178
179	/**
180	 * @depends testClearQueue
181	 */
182	public function testSyncDownload( $data ) {
183		$file = __DIR__ . '/../../data/upload/png-plain.png';
184		$this->installMockHttp( file_get_contents( $file ) );
185
186		$this->getServiceContainer()->getUserGroupManager()->addUserToGroup( $this->user, 'users' );
187		$data = $this->doApiRequestWithToken( [
188			'action' => 'upload',
189			'filename' => 'UploadFromUrlTest.png',
190			'url' => 'http://upload.wikimedia.org/wikipedia/mediawiki/b/bc/Wiki.png',
191			'ignorewarnings' => true,
192		], $data );
193
194		$this->assertEquals( 'Success', $data[0]['upload']['result'] );
195		$this->deleteFile( 'UploadFromUrlTest.png' );
196
197		return $data;
198	}
199
200	protected function deleteFile( $name ) {
201		$t = Title::newFromText( $name, NS_FILE );
202		$this->assertTrue( $t->exists(), "File '$name' exists" );
203
204		if ( $t->exists() ) {
205			$file = MediaWikiServices::getInstance()->getRepoGroup()
206				->findFile( $name, [ 'ignoreRedirect' => true ] );
207			$empty = "";
208			FileDeleteForm::doDelete( $t, $file, $empty, "none", true, $this->user );
209			$page = WikiPage::factory( $t );
210			$page->doDeleteArticleReal( "testing", $this->user );
211		}
212		$t = Title::newFromText( $name, NS_FILE );
213
214		$this->assertFalse( $t->exists(), "File '$name' was deleted" );
215	}
216
217	public function testUploadFromUrl() {
218		$file = __DIR__ . '/../../data/upload/png-plain.png';
219		$this->installMockHttp( file_get_contents( $file ) );
220
221		$upload = new UploadFromUrl();
222		$upload->initialize( 'Test.png', 'http://www.example.com/test.png' );
223		$status = $upload->fetchFile();
224
225		$this->assertTrue( $status->isOK() );
226		$this->assertUploadOk( $upload );
227	}
228
229	public function testUploadFromUrlWithRedirect() {
230		$file = __DIR__ . '/../../data/upload/png-plain.png';
231		$this->installMockHttp( [
232			// First response is a redirect
233			$this->makeFakeHttpRequest(
234				'Blaba',
235				302,
236				[ 'Location' => 'http://static.example.com/files/test.png' ]
237			),
238			// Second response is a file
239			$this->makeFakeHttpRequest(
240				file_get_contents( $file )
241			),
242		] );
243
244		$upload = new UploadFromUrl();
245		$upload->initialize( 'Test.png', 'http://www.example.com/test.png' );
246		$status = $upload->fetchFile();
247
248		$this->assertTrue( $status->isOK() );
249		$this->assertUploadOk( $upload );
250	}
251
252}
253