Completed
Push — main ( 2daa48...b5d932 )
by
unknown
08:38
created

FileUploader::uploadByChunks()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 9.312
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
namespace Addwiki\Mediawiki\Api\Service;
4
5
use Addwiki\Mediawiki\Api\Client\MultipartRequest;
6
use Addwiki\Mediawiki\Api\Client\SimpleRequest;
7
use Exception;
8
9
/**
10
 * @access private
11
 *
12
 * @author Addshore
13
 */
14
class FileUploader extends Service {
15
16
	protected ?int $chunkSize = null;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '?', expecting T_FUNCTION or T_CONST
Loading history...
17
18
	/**
19
	 * Set the chunk size used for chunked uploading.
20
	 *
21
	 * Chunked uploading is available in MediaWiki 1.20 and above, although prior to version 1.25,
22
	 * SVGs could not be uploaded via chunked uploading.
23
	 *
24
	 * @link https://www.mediawiki.org/wiki/API:Upload#Chunked_uploading
25
	 *
26
	 * @param int $chunkSize In bytes.
27
	 */
28
	public function setChunkSize( int $chunkSize ): void {
29
		$this->chunkSize = $chunkSize;
30
	}
31
32
	/**
33
	 * Upload a file.
34
	 *
35
	 * @param string $targetName The name to give the file on the wiki (no 'File:' prefix required).
36
	 * @param string $location Can be local path or remote URL.
37
	 * @param string $text Initial page text for new files.
38
	 * @param string|null $comment Upload comment. Also used as the initial page text for new files if
39
	 * text parameter not provided.
40
	 * @param string|null $watchlist Unconditionally add or remove the page from your watchlist, use
41
	 * preferences or do not change watch. Possible values: 'watch', 'preferences', 'nochange'.
42
	 * @param bool $ignoreWarnings Ignore any warnings. This must be set to upload a new version of
43
	 * an existing image.
44
	 */
45
	public function upload(
46
		string $targetName,
47
		string $location,
48
		string $text = '',
49
		?string $comment = '',
50
		?string $watchlist = 'preferences',
51
		bool $ignoreWarnings = false
52
	): bool {
53
		$params = [
54
			'filename' => $targetName,
55
			'token' => $this->api->getToken(),
56
		];
57
		// Watchlist behaviour.
58
		if ( in_array( $watchlist, [ 'watch', 'nochange' ] ) ) {
59
			$params['watchlist'] = $watchlist;
60
		}
61
		// Ignore warnings?
62
		if ( $ignoreWarnings ) {
63
			$params['ignorewarnings'] = '1';
64
		}
65
		// Page text.
66
		if ( !empty( $text ) ) {
67
			$params['text'] = $text;
68
		}
69
		// Revision comment.
70
		if ( $comment !== null && !empty( $comment ) ) {
71
			$params['comment'] = $comment;
72
		}
73
74
		if ( is_file( $location ) ) {
75
			// Normal single-request upload.
76
			$params['filesize'] = filesize( $location );
77
			$params['file'] = fopen( $location, 'r' );
78
			if ( $this->chunkSize !== null && $this->chunkSize > 0 ) {
79
				// Chunked upload.
80
				$params = $this->uploadByChunks( $params );
81
			}
82
		} else {
83
			// Upload from URL.
84
			$params['url'] = $location;
85
		}
86
87
		$response = $this->api->postRequest( new SimpleRequest( 'upload', $params ) );
88
		return ( $response['upload']['result'] === 'Success' );
89
	}
90
91
	/**
92
	 * Upload a file by chunks and get the parameters for the final upload call.
93
	 * @param mixed[] $params The request parameters.
94
	 * @return mixed[]
95
	 * @throws Exception
96
	 */
97
	protected function uploadByChunks( array $params ) {
98
		// Get the file handle for looping, but don't keep it in the request parameters.
99
		$fileHandle = $params['file'];
100
		unset( $params['file'] );
101
		// Track the chunks and offset.
102
		$chunksDone = 0;
103
		$params['offset'] = 0;
104
		while ( true ) {
105
			// 1. Make the request.
106
			$params['chunk'] = fread( $fileHandle, $this->chunkSize );
107
			$contentDisposition = 'form-data; name="chunk"; filename="' . $params['filename'] . '"';
108
			$request = MultipartRequest::factory()
109
				->setParams( $params )
110
				->setAction( 'upload' )
111
				->setMultipartParams( [
112
					'chunk' => [ 'headers' => [ 'Content-Disposition' => $contentDisposition ] ],
113
				] );
114
			$response = $this->api->postRequest( $request );
115
116
			// 2. Deal with the response.
117
			++$chunksDone;
118
			$params['offset'] = ( $chunksDone * $this->chunkSize );
119
			if ( !isset( $response['upload']['filekey'] ) ) {
120
				// This should never happen. Even the last response still has the filekey.
121
				throw new Exception( 'Unable to get filekey for chunked upload' );
122
			}
123
			$params['filekey'] = $response['upload']['filekey'];
124
			if ( $response['upload']['result'] === 'Continue' ) {
125
				// Amend parameters for next upload POST request.
126
				$params['offset'] = $response['upload']['offset'];
127
			} else {
128
				// The final upload POST will be done in self::upload()
129
				// to commit the upload out of the stash area.
130
				unset( $params['chunk'], $params['offset'] );
131
				return $params;
132
			}
133
		}
134
	}
135
}
136