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