follow up r61355, initial, incomplete dealing with TimStarlings CR
[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 $fileSize = 0;
26 protected $repoPath;
27 protected $pageText;
28 protected $watch;
29
30 public $status;
31
32 // Parent class requires this function even though it is only
33 // used from SpecialUpload.php and we don't do chunked uploading
34 // from SpecialUpload -- best to raise an exception for
35 // now.
36 public function initializeFromRequest( &$request ) {
37 throw new MWException( 'not implemented' );
38 }
39
40 public function initialize( $done, $filename, $sessionKey, $path,
41 $fileSize, $sessionData )
42 {
43 $this->initFromSessionKey( $sessionKey, $sessionData );
44
45 if ( !$this->sessionKey && !$done ) {
46 // session key not set, init the chunk upload system:
47 $this->chunkMode = self::INIT;
48 $this->mDesiredDestName = $filename;
49 } else if ( $this->sessionKey && !$done ) {
50 $this->chunkMode = self::CHUNK;
51 } else if ( $this->sessionKey && $done ) {
52 $this->chunkMode = self::DONE;
53 }
54
55 if ( $this->chunkMode == self::CHUNK || $this->chunkMode == self::DONE ) {
56 $this->mTempPath = $path;
57 $this->fileSize += $fileSize;
58 }
59 }
60
61 /**
62 * Set session information for chunked uploads and allocate a unique key.
63 * @param $comment string
64 * @param $pageText string
65 * @param $watch boolean
66 *
67 * @returns string the session key for this chunked upload
68 */
69 protected function setupChunkSession( $comment, $pageText, $watch ) {
70 $this->sessionKey = $this->getSessionKey();
71 $_SESSION['wsUploadData'][$this->sessionKey] = array(
72 'comment' => $comment,
73 'pageText' => $pageText,
74 'watch' => $watch,
75 'mFilteredName' => $this->mFilteredName,
76 'repoPath' => null,
77 'mDesiredDestName' => $this->mDesiredDestName,
78 'version' => self::SESSION_VERSION,
79 );
80 return $this->sessionKey;
81 }
82
83 /**
84 * Initialize a continuation of a chunked upload from a session key
85 * @param $sessionKey string
86 * @param $request WebRequest
87 *
88 * @returns void
89 */
90 protected function initFromSessionKey( $sessionKey, $sessionData ) {
91 // testing against null because we don't want to cause obscure
92 // bugs when $sessionKey is full of "0"
93 if ( !$sessionKey === null ) {
94 $this->status = Status::newFromFatal( 'import-token-mismatch' );
95 return;
96 }
97 $this->sessionKey = $sessionKey;
98
99 if ( isset( $sessionData[$this->sessionKey]['version'] )
100 && $sessionData[$this->sessionKey]['version'] == self::SESSION_VERSION )
101 {
102 $this->comment = $sessionData[$this->sessionKey]['comment'];
103 $this->pageText = $sessionData[$this->sessionKey]['pageText'];
104 $this->watch = $sessionData[$this->sessionKey]['watch'];
105 $this->mFilteredName = $sessionData[$this->sessionKey]['mFilteredName'];
106 $this->repoPath = $sessionData[$this->sessionKey]['repoPath'];
107 $this->mDesiredDestName = $sessionData[$this->sessionKey]['mDesiredDestName'];
108 } else {
109 $this->status = Status::newFromFatal( 'Missing session data.' );
110 }
111 }
112
113 /**
114 * Handle a chunk of the upload. Overrides the parent method
115 * because Chunked Uploading clients (i.e. Firefogg) require
116 * specific API responses.
117 * @see UploadBase::performUpload
118 */
119 public function performUpload( $comment, $pageText, $watch, $user ) {
120 wfDebug( "\n\n\performUpload(chunked): comment:" . $comment . ' pageText: ' . $pageText . ' watch:' . $watch );
121 global $wgUser, $wgOut;
122
123 if ( $this->chunkMode == self::INIT ) {
124 // firefogg expects a specific result per:
125 // http://www.firefogg.org/dev/chunk_post.html
126
127 // it's okay to return the token here because
128 // a) the user must have requested the token to get here and
129 // b) should only happen over POST
130 // c) we need the token to validate chunks are coming from a non-xss request
131 $token = urlencode( $wgUser->editToken() );
132 ob_clean();
133 echo FormatJson::encode( array(
134 'uploadUrl' => wfExpandUrl( wfScript( 'api' ) ) . "?action=upload&" .
135 "token={$token}&format=json&enablechunks=true&chunksessionkey=" .
136 $this->setupChunkSession( $comment, $pageText, $watch ) ) );
137 $wgOut->disable();
138 } else if ( $this->chunkMode == self::CHUNK ) {
139 $status = $this->appendChunk();
140 if ( !$status->isOK() ) {
141 return $status;
142 }
143 // return success:
144 // firefogg expects a specific result
145 // http://www.firefogg.org/dev/chunk_post.html
146 ob_clean();
147 echo FormatJson::encode(
148 array( 'result' => 1, 'filesize' => $this->fileSize )
149 );
150 $wgOut->disable();
151 } else if ( $this->chunkMode == self::DONE ) {
152 if ( !$comment )
153 $comment = $this->comment;
154
155 if ( !$pageText )
156 $pageText = $this->pageText;
157
158 if ( !$watch )
159 $watch = $this->watch;
160
161 $status = parent::performUpload( $comment, $pageText, $watch, $user );
162 if ( !$status->isGood() ) {
163 return $status;
164 }
165 $file = $this->getLocalFile();
166
167 // firefogg expects a specific result
168 // http://www.firefogg.org/dev/chunk_post.html
169 ob_clean();
170 echo FormatJson::encode( array(
171 'result' => 1,
172 'done' => 1,
173 'resultUrl' => $file->getDescriptionUrl() )
174 );
175 $wgOut->disable();
176 }
177 }
178
179 /**
180 * Append a chunk to the Repo file
181 *
182 * @param string $srcPath Path to file to append from
183 * @param string $toAppendPath Path to file to append to
184 * @return Status Status
185 */
186 protected function appendToUploadFile( $srcPath, $toAppendPath ) {
187 $repo = RepoGroup::singleton()->getLocalRepo();
188 $status = $repo->append( $srcPath, $toAppendPath );
189 return $status;
190 }
191
192 /**
193 * Append a chunk to the temporary file.
194 *
195 * @return void
196 */
197 protected function appendChunk() {
198 global $wgMaxUploadSize;
199
200 if ( !$this->repoPath ) {
201 $this->status = $this->saveTempUploadedFile( $this->mDesiredDestName, $this->mTempPath );
202
203 if ( $status->isOK() ) {
204 $this->repoPath = $status->value;
205 $_SESSION['wsUploadData'][$this->sessionKey]['repoPath'] = $this->repoPath;
206 }
207 return $status;
208 }
209 if ( $this->getRealPath( $this->repoPath ) ) {
210 $this->status = $this->appendToUploadFile( $this->repoPath, $this->mTempPath );
211 } else {
212 $this->status = Status::newFatal( 'filenotfound', $this->repoPath );
213 }
214 if ( $this->fileSize > $wgMaxUploadSize )
215 $this->status = Status::newFatal( 'largefileserver' );
216 }
217
218 public function verifyUpload() {
219 if ( $this->chunkMode != self::DONE ) {
220 return Status::newGood();
221 }
222 return parent::verifyUpload();
223 }
224
225 public function checkWarnings() {
226 if ( $this->chunkMode != self::DONE ) {
227 return null;
228 }
229 return parent::checkWarnings();
230 }
231
232 public function getImageInfo( $result ) {
233 if ( $this->chunkMode != self::DONE ) {
234 return null;
235 }
236 return parent::getImageInfo( $result );
237 }
238 }