Completed
Push — master ( ec2f81...11966d )
by adam
03:28
created

FileUploader::upload()   C

Complexity

Conditions 8
Paths 48

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 45
ccs 0
cts 26
cp 0
rs 5.3846
cc 8
eloc 27
nc 48
nop 6
crap 72
1
<?php
2
3
namespace Mediawiki\Api\Service;
4
5
use Exception;
6
use Mediawiki\Api\MediawikiApi;
7
use Mediawiki\Api\MultipartRequest;
8
use Mediawiki\Api\SimpleRequest;
9
10
/**
11
 * @access private
12
 *
13
 * @author Addshore
14
 */
15
class FileUploader {
16
17
	/**
18
	 * @var MediawikiApi
19
	 */
20
	private $api;
21
22
	/** @var int */
23
	protected $chunkSize;
24
25
	/**
26
	 * @param MediawikiApi $api
27
	 */
28
	public function __construct( MediawikiApi $api ) {
29
		$this->api = $api;
30
	}
31
32
	/**
33
	 * Set the chunk size used for chunked uploading.
34
	 *
35
	 * Chunked uploading is available in MediaWiki 1.20 and above, although prior to version 1.25,
36
	 * SVGs could not be uploaded via chunked uploading.
37
	 *
38
	 * @link https://www.mediawiki.org/wiki/API:Upload#Chunked_uploading
39
	 *
40
	 * @param int $chunkSize In bytes.
41
	 */
42
	public function setChunkSize( $chunkSize ) {
43
		$this->chunkSize = $chunkSize;
44
	}
45
46
	/**
47
	 * Upload a file.
48
	 *
49
	 * @param string $targetName The name to give the file on the wiki (no 'File:' prefix required).
50
	 * @param string $location Can be local path or remote URL.
51
	 * @param string $text Initial page text for new files.
52
	 * @param string $comment Upload comment. Also used as the initial page text for new files if
53
	 * text parameter not provided.
54
	 * @param string $watchlist Unconditionally add or remove the page from your watchlist, use
55
	 * preferences or do not change watch. Possible values: 'watch', 'preferences', 'nochange'.
56
	 * @param bool $ignoreWarnings Ignore any warnings. This must be set to upload a new version of
57
	 * an existing image.
58
	 *
59
	 * @return bool
60
	 */
61
	public function upload(
62
		$targetName,
63
		$location,
64
		$text = '',
65
		$comment = '',
66
		$watchlist = 'preferences',
67
		$ignoreWarnings = false
68
	) {
69
		$params = [
70
			'filename' => $targetName,
71
			'token' => $this->api->getToken(),
72
		];
73
		// Watchlist behaviour.
74
		if ( in_array( $watchlist, [ 'watch', 'nochange' ] ) ) {
75
			$params['watchlist'] = $watchlist;
76
		}
77
		// Ignore warnings?
78
		if ( $ignoreWarnings ) {
79
			$params['ignorewarnings'] = '1';
80
		}
81
		// Page text.
82
		if ( !empty( $text ) ) {
83
			$params['text'] = $text;
84
		}
85
		// Revision comment.
86
		if ( !empty( $comment ) ) {
87
			$params['comment'] = $comment;
88
		}
89
90
		if ( is_file( $location ) ) {
91
			// Normal single-request upload.
92
			$params['filesize'] = filesize( $location );
93
			$params['file'] = fopen( $location, 'r' );
94
			if ( is_int( $this->chunkSize ) && $this->chunkSize > 0 ) {
95
				// Chunked upload.
96
				$params = $this->uploadByChunks( $params );
97
			}
98
		} else {
99
			// Upload from URL.
100
			$params['url'] = $location;
101
		}
102
103
		$response = $this->api->postRequest( new SimpleRequest( 'upload', $params ) );
0 ignored issues
show
Bug introduced by
It seems like $params defined by $this->uploadByChunks($params) on line 96 can also be of type null; however, Mediawiki\Api\SimpleRequest::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
104
		return ( $response['upload']['result'] === 'Success' );
105
	}
106
107
	/**
108
	 * Upload a file by chunks and get the parameters for the final upload call.
109
	 * @param mixed[] $params The request parameters.
110
	 * @return mixed[]
111
	 * @throws Exception
112
	 */
113
	protected function uploadByChunks( $params ) {
114
		// Get the file handle for looping, but don't keep it in the request parameters.
115
		$fileHandle = $params['file'];
116
		unset( $params['file'] );
117
		// Track the chunks and offset.
118
		$chunksDone = 0;
119
		$params['offset'] = 0;
120
		while ( true ) {
121
122
			// 1. Make the request.
123
			$params['chunk'] = fread( $fileHandle, $this->chunkSize );
124
			$contentDisposition = 'form-data; name="chunk"; filename="' . $params['filename'] . '"';
125
			$request = MultipartRequest::factory()
126
				->setParams( $params )
127
				->setAction( 'upload' )
128
				->setMultipartParams( [
129
					'chunk' => [ 'headers' => [ 'Content-Disposition' => $contentDisposition ] ],
130
				] );
131
			$response = $this->api->postRequest( $request );
132
133
			// 2. Deal with the response.
134
			$chunksDone++;
135
			$params['offset'] = ( $chunksDone * $this->chunkSize );
136
			if ( !isset( $response['upload']['filekey'] ) ) {
137
				// This should never happen. Even the last response still has the filekey.
138
				throw new Exception( 'Unable to get filekey for chunked upload' );
139
			}
140
			$params['filekey'] = $response['upload']['filekey'];
141
			if ( $response['upload']['result'] === 'Continue' ) {
142
				// Amend parameters for next upload POST request.
143
				$params['offset'] = $response['upload']['offset'];
144
			} else {
145
				// The final upload POST will be done in self::upload()
146
				// to commit the upload out of the stash area.
147
				unset( $params['chunk'], $params['offset'] );
148
				return $params;
149
			}
150
		}
151
	}
152
}
153