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