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

src/Service/FileUploader.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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