4e3e1df8b3ea795eae83d39d55dada530e7e17a2
[lhc/web/wiklou.git] / includes / upload / UploadFromChunks.php
1 <?php
2 /**
3 * @file
4 * @ingroup upload
5 *
6 * First, destination checks are made, and, if ignorewarnings is not
7 * checked, errors / warning is returned.
8 *
9 * 1. We return the uploadUrl.
10 * 2. We then accept chunk uploads from the client.
11 * 3. Return chunk id on each POSTED chunk.
12 * 4. Once the client posts "done=1", the files are concatenated together.
13 *
14 * (More info at: http://firefogg.org/dev/chunk_post.html)
15 */
16 class UploadFromChunks extends UploadBase {
17
18 const INIT = 1;
19 const CHUNK = 2;
20 const DONE = 3;
21
22 protected $chunkMode; // INIT, CHUNK, DONE
23 protected $sessionKey;
24 protected $comment;
25 protected $repoPath;
26 protected $pageText;
27 protected $watch;
28
29 public $status;
30
31 // Parent class requires this function even though it is only
32 // used from SpecialUpload.php and we don't do chunked uploading
33 // from SpecialUpload -- best to raise an exception for
34 // now.
35 public function initializeFromRequest( &$request ) {
36 throw new MWException( 'not implemented' );
37 }
38
39 public function initialize( $done, $filename, $sessionKey, $path, $fileSize, $sessionData ) {
40 $this->status = Status::newGood();
41
42 $this->initializePathInfo( $filename, $path, 0, true );
43 if ( $sessionKey !== null ) {
44 $this->initFromSessionKey( $sessionKey, $sessionData, $fileSize );
45
46 if ( $done ) {
47 $this->chunkMode = self::DONE;
48 } else {
49 $this->mTempPath = $path;
50 $this->chunkMode = self::CHUNK;
51 }
52 } else {
53 // session key not set, init the chunk upload system:
54 $this->chunkMode = self::INIT;
55 }
56
57 if ( $this->status->isOk()
58 && ( $this->mDesiredDestName === null || $this->mFileSize === null ) ) {
59 $this->status = Status::newFatal( 'chunk-init-error' );
60 }
61 }
62
63 /**
64 * Set session information for chunked uploads and allocate a unique key.
65 * @param $comment string
66 * @param $pageText string
67 * @param $watch boolean
68 *
69 * @returns string the session key for this chunked upload
70 */
71 protected function setupChunkSession( $comment, $pageText, $watch ) {
72 if ( !isset( $this->sessionKey ) ) {
73 $this->sessionKey = $this->getSessionKey();
74 }
75 foreach ( array( 'mFilteredName', 'repoPath', 'mFileSize', 'mDesiredDestName' )
76 as $key ) {
77 if ( isset( $this->$key ) ) {
78 $_SESSION['wsUploadData'][$this->sessionKey][$key] = $this->$key;
79 }
80 }
81 if ( isset( $comment ) ) {
82 $_SESSION['wsUploadData'][$this->sessionKey]['commment'] = $comment;
83 }
84 if ( isset( $pageText ) ) {
85 $_SESSION['wsUploadData'][$this->sessionKey]['pageText'] = $pageText;
86 }
87 if ( isset( $watch ) ) {
88 $_SESSION['wsUploadData'][$this->sessionKey]['watch'] = $watch;
89 }
90 $_SESSION['wsUploadData'][$this->sessionKey]['version'] = self::SESSION_VERSION;
91
92 return $this->sessionKey;
93 }
94
95 /**
96 * Initialize a continuation of a chunked upload from a session key
97 * @param $sessionKey string
98 * @param $request WebRequest
99 * @param $fileSize int Size of this chunk
100 *
101 * @returns void
102 */
103 protected function initFromSessionKey( $sessionKey, $sessionData, $fileSize ) {
104 // testing against null because we don't want to cause obscure
105 // bugs when $sessionKey is full of "0"
106 $this->sessionKey = $sessionKey;
107
108 if ( isset( $sessionData[$this->sessionKey]['version'] )
109 && $sessionData[$this->sessionKey]['version'] == self::SESSION_VERSION )
110 {
111 foreach ( array( 'comment', 'pageText', 'watch', 'mFilteredName', 'repoPath', 'mFileSize', 'mDesiredDestName' )
112 as $key ) {
113 if ( isset( $sessionData[$this->sessionKey][$key] ) ) {
114 $this->$key = $sessionData[$this->sessionKey][$key];
115 }
116 }
117
118 $this->mFileSize += $fileSize;
119 } else {
120 $this->status = Status::newFatal( 'invalid-session-key' );
121 }
122 }
123
124 /**
125 * Handle a chunk of the upload. Overrides the parent method
126 * because Chunked Uploading clients (i.e. Firefogg) require
127 * specific API responses.
128 * @see UploadBase::performUpload
129 */
130 public function performUpload( $comment, $pageText, $watch, $user ) {
131 wfDebug( "\n\n\performUpload(chunked): comment:" . $comment . ' pageText: ' . $pageText . ' watch:' . $watch );
132 global $wgUser, $wgOut;
133
134 if ( $this->chunkMode == self::INIT ) {
135 // firefogg expects a specific result per:
136 // http://www.firefogg.org/dev/chunk_post.html
137
138 // it's okay to return the token here because
139 // a) the user must have requested the token to get here and
140 // b) should only happen over POST
141 // c) we need the token to validate chunks are coming from a non-xss request
142 return Status::newGood(
143 array( 'uploadUrl' => wfExpandUrl( wfScript( 'api' ) ) . "?" .
144 wfArrayToCGI( array(
145 'action' => 'upload',
146 'token' => $wgUser->editToken(),
147 'format' => 'json',
148 'filename' => $this->mDesiredDestName,
149 'enablechunks' => 'true',
150 'chunksession' =>
151 $this->setupChunkSession( $comment, $pageText, $watch ) ) ) ) );
152 } else if ( $this->chunkMode == self::CHUNK ) {
153 $this->setupChunkSession();
154 $this->appendChunk();
155 if ( !$this->status->isOK() ) {
156 return $this->status;
157 }
158 // return success:
159 // firefogg expects a specific result
160 // http://www.firefogg.org/dev/chunk_post.html
161 return Status::newGood(
162 array( 'result' => 1, 'filesize' => $this->mFileSize )
163 );
164 } else if ( $this->chunkMode == self::DONE ) {
165 $this->finalizeFile();
166 // We ignore the passed-in parameters because these were set on the first contact.
167 $status = parent::performUpload( $this->comment, $this->pageText, $this->watch, $user );
168
169 if ( !$status->isGood() ) {
170 return $status;
171 }
172 $file = $this->getLocalFile();
173
174 // firefogg expects a specific result
175 // http://www.firefogg.org/dev/chunk_post.html
176 return Status::newGood(
177 array( 'result' => 1, 'done' => 1, 'resultUrl' => wfExpandUrl( $file->getDescriptionUrl() ) )
178 );
179 }
180
181 return Status::newGood();
182 }
183
184 /**
185 * Append a chunk to the Repo file
186 *
187 * @param string $srcPath Path to file to append from
188 * @param string $toAppendPath Path to file to append to
189 * @return Status Status
190 */
191 protected function appendToUploadFile( $srcPath, $toAppendPath ) {
192 $repo = RepoGroup::singleton()->getLocalRepo();
193 $status = $repo->append( $srcPath, $toAppendPath );
194 return $status;
195 }
196
197 /**
198 * Append a chunk to the temporary file.
199 *
200 * @return void
201 */
202 protected function appendChunk() {
203 global $wgMaxUploadSize;
204
205 if ( !$this->repoPath ) {
206 $this->status = $this->saveTempUploadedFile( $this->mDesiredDestName, $this->mTempPath );
207
208 if ( $this->status->isOK() ) {
209 $this->repoPath = $this->status->value;
210 $_SESSION['wsUploadData'][$this->sessionKey]['repoPath'] = $this->repoPath;
211 }
212 return;
213 }
214 if ( $this->getRealPath( $this->repoPath ) ) {
215 $this->status = $this->appendToUploadFile( $this->repoPath, $this->mTempPath );
216
217 if ( $this->mFileSize > $wgMaxUploadSize )
218 $this->status = Status::newFatal( 'largefileserver' );
219
220 } else {
221 $this->status = Status::newFatal( 'filenotfound', $this->repoPath );
222 }
223 }
224
225 /**
226 * Append the final chunk and ready file for parent::performUpload()
227 * @return void
228 */
229 protected function finalizeFile() {
230 $this->appendChunk();
231 $this->mTempPath = $this->getRealPath( $this->repoPath );
232 }
233
234 public function verifyUpload() {
235 if ( $this->chunkMode != self::DONE ) {
236 return array( 'status' => UploadBase::OK );
237 }
238 return parent::verifyUpload();
239 }
240
241 public function checkWarnings() {
242 if ( $this->chunkMode != self::DONE ) {
243 return null;
244 }
245 return parent::checkWarnings();
246 }
247
248 public function getImageInfo( $result ) {
249 if ( $this->chunkMode != self::DONE ) {
250 return null;
251 }
252 return parent::getImageInfo( $result );
253 }
254 }