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