OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "native_client/src/trusted/plugin/file_downloader.h" | 5 #include "native_client/src/trusted/plugin/file_downloader.h" |
6 | 6 |
7 #include <stdio.h> | 7 #include <stdio.h> |
8 #include <string.h> | 8 #include <string.h> |
9 #include <string> | 9 #include <string> |
10 | 10 |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
47 file_io_trusted_interface_ = static_cast<const PPB_FileIOTrusted*>( | 47 file_io_trusted_interface_ = static_cast<const PPB_FileIOTrusted*>( |
48 pp::Module::Get()->GetBrowserInterface(PPB_FILEIOTRUSTED_INTERFACE)); | 48 pp::Module::Get()->GetBrowserInterface(PPB_FILEIOTRUSTED_INTERFACE)); |
49 url_loader_trusted_interface_ = static_cast<const PPB_URLLoaderTrusted*>( | 49 url_loader_trusted_interface_ = static_cast<const PPB_URLLoaderTrusted*>( |
50 pp::Module::Get()->GetBrowserInterface(PPB_URLLOADERTRUSTED_INTERFACE)); | 50 pp::Module::Get()->GetBrowserInterface(PPB_URLLOADERTRUSTED_INTERFACE)); |
51 temp_buffer_.resize(kTempBufferSize); | 51 temp_buffer_.resize(kTempBufferSize); |
52 } | 52 } |
53 | 53 |
54 bool FileDownloader::OpenStream( | 54 bool FileDownloader::OpenStream( |
55 const nacl::string& url, | 55 const nacl::string& url, |
56 const pp::CompletionCallback& callback, | 56 const pp::CompletionCallback& callback, |
57 StreamCallbackSource* stream_callback_source) { | 57 StreamCallbackSource* stream_callback_source, |
58 bool finish_streaming) { | |
58 data_stream_callback_source_ = stream_callback_source; | 59 data_stream_callback_source_ = stream_callback_source; |
Derek Schuff
2013/06/10 16:48:24
could just set open_and_stream_ = true in the cons
jvoung (off chromium)
2013/06/13 20:23:32
Done.
| |
59 return Open(url, DOWNLOAD_STREAM, callback, true, NULL); | 60 return Open(url, DOWNLOAD_STREAM, callback, true, NULL, finish_streaming); |
60 } | 61 } |
61 | 62 |
62 bool FileDownloader::Open( | 63 bool FileDownloader::Open( |
63 const nacl::string& url, | 64 const nacl::string& url, |
64 DownloadMode mode, | 65 DownloadMode mode, |
65 const pp::CompletionCallback& callback, | 66 const pp::CompletionCallback& callback, |
66 bool record_progress, | 67 bool record_progress, |
67 PP_URLLoaderTrusted_StatusCallback progress_callback) { | 68 PP_URLLoaderTrusted_StatusCallback progress_callback, |
69 bool finish_streaming) { | |
68 PLUGIN_PRINTF(("FileDownloader::Open (url=%s)\n", url.c_str())); | 70 PLUGIN_PRINTF(("FileDownloader::Open (url=%s)\n", url.c_str())); |
69 if (callback.pp_completion_callback().func == NULL || | 71 if (callback.pp_completion_callback().func == NULL || |
70 instance_ == NULL || | 72 instance_ == NULL || |
71 file_io_trusted_interface_ == NULL) | 73 file_io_trusted_interface_ == NULL) |
72 return false; | 74 return false; |
73 | 75 |
74 CHECK(instance_ != NULL); | 76 CHECK(instance_ != NULL); |
75 open_time_ = NaClGetTimeOfDayMicroseconds(); | 77 open_time_ = NaClGetTimeOfDayMicroseconds(); |
76 status_code_ = -1; | 78 status_code_ = -1; |
77 url_to_open_ = url; | 79 url_to_open_ = url; |
78 url_ = url; | 80 url_ = url; |
79 file_open_notify_callback_ = callback; | 81 file_open_notify_callback_ = callback; |
80 mode_ = mode; | 82 mode_ = mode; |
83 open_and_stream_ = finish_streaming; | |
81 buffer_.clear(); | 84 buffer_.clear(); |
82 pp::URLRequestInfo url_request(instance_); | 85 pp::URLRequestInfo url_request(instance_); |
83 | 86 |
84 // Allow CORS. | 87 // Allow CORS. |
85 // Note that "SetAllowCrossOriginRequests" (currently) has the side effect of | 88 // Note that "SetAllowCrossOriginRequests" (currently) has the side effect of |
86 // preventing credentials from being sent on same-origin requests. We | 89 // preventing credentials from being sent on same-origin requests. We |
87 // therefore avoid setting this flag unless we know for sure it is a | 90 // therefore avoid setting this flag unless we know for sure it is a |
88 // cross-origin request, resulting in behavior similar to XMLHttpRequest. | 91 // cross-origin request, resulting in behavior similar to XMLHttpRequest. |
89 if (!instance_->DocumentCanRequest(url)) | 92 if (!instance_->DocumentCanRequest(url)) |
90 url_request.SetAllowCrossOriginRequests(true); | 93 url_request.SetAllowCrossOriginRequests(true); |
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
216 return (now - open_time_) / NACL_MICROS_PER_MILLI; | 219 return (now - open_time_) / NACL_MICROS_PER_MILLI; |
217 } | 220 } |
218 | 221 |
219 bool FileDownloader::InitialResponseIsValid(int32_t pp_error) { | 222 bool FileDownloader::InitialResponseIsValid(int32_t pp_error) { |
220 if (pp_error != PP_OK) { // Url loading failed. | 223 if (pp_error != PP_OK) { // Url loading failed. |
221 file_open_notify_callback_.Run(pp_error); | 224 file_open_notify_callback_.Run(pp_error); |
222 return false; | 225 return false; |
223 } | 226 } |
224 | 227 |
225 // Process the response, validating the headers to confirm successful loading. | 228 // Process the response, validating the headers to confirm successful loading. |
226 pp::URLResponseInfo url_response(url_loader_.GetResponseInfo()); | 229 url_response_ = url_loader_.GetResponseInfo(); |
227 if (url_response.is_null()) { | 230 if (url_response_.is_null()) { |
228 PLUGIN_PRINTF(( | 231 PLUGIN_PRINTF(( |
229 "FileDownloader::InitialResponseIsValid (url_response=NULL)\n")); | 232 "FileDownloader::InitialResponseIsValid (url_response_=NULL)\n")); |
230 file_open_notify_callback_.Run(PP_ERROR_FAILED); | 233 file_open_notify_callback_.Run(PP_ERROR_FAILED); |
231 return false; | 234 return false; |
232 } | 235 } |
233 // Note that URLs in the chrome-extension scheme produce different error | 236 |
234 // codes than other schemes. This is because chrome-extension URLs are | 237 pp::Var full_url = url_response_.GetURL(); |
235 // really a special kind of file scheme, and therefore do not produce HTTP | |
236 // status codes. | |
237 pp::Var full_url = url_response.GetURL(); | |
238 if (!full_url.is_string()) { | 238 if (!full_url.is_string()) { |
239 PLUGIN_PRINTF(( | 239 PLUGIN_PRINTF(( |
240 "FileDownloader::InitialResponseIsValid (url is not a string)\n")); | 240 "FileDownloader::InitialResponseIsValid (url is not a string)\n")); |
241 file_open_notify_callback_.Run(PP_ERROR_FAILED); | 241 file_open_notify_callback_.Run(PP_ERROR_FAILED); |
242 return false; | 242 return false; |
243 } | 243 } |
244 url_ = full_url.AsString(); | |
245 | |
246 // Note that URLs in the data-URI scheme produce different error | |
247 // codes than other schemes. This is because data-URI are really a | |
248 // special kind of file scheme, and therefore do not produce HTTP | |
249 // status codes. | |
244 bool status_ok = false; | 250 bool status_ok = false; |
245 status_code_ = url_response.GetStatusCode(); | 251 status_code_ = url_response_.GetStatusCode(); |
246 switch (url_scheme_) { | 252 switch (url_scheme_) { |
247 case SCHEME_CHROME_EXTENSION: | 253 case SCHEME_CHROME_EXTENSION: |
248 PLUGIN_PRINTF(("FileDownloader::InitialResponseIsValid (chrome-extension " | 254 PLUGIN_PRINTF(("FileDownloader::InitialResponseIsValid (chrome-extension " |
249 "response status_code=%"NACL_PRId32")\n", status_code_)); | 255 "response status_code=%"NACL_PRId32")\n", status_code_)); |
250 status_ok = (status_code_ == kExtensionUrlRequestStatusOk); | 256 status_ok = (status_code_ == kExtensionUrlRequestStatusOk); |
251 break; | 257 break; |
252 case SCHEME_DATA: | 258 case SCHEME_DATA: |
253 PLUGIN_PRINTF(("FileDownloader::InitialResponseIsValid (data URI " | 259 PLUGIN_PRINTF(("FileDownloader::InitialResponseIsValid (data URI " |
254 "response status_code=%"NACL_PRId32")\n", status_code_)); | 260 "response status_code=%"NACL_PRId32")\n", status_code_)); |
255 status_ok = (status_code_ == kDataUriRequestStatusOk); | 261 status_ok = (status_code_ == kDataUriRequestStatusOk); |
(...skipping 11 matching lines...) Expand all Loading... | |
267 } | 273 } |
268 | 274 |
269 return true; | 275 return true; |
270 } | 276 } |
271 | 277 |
272 void FileDownloader::URLLoadStartNotify(int32_t pp_error) { | 278 void FileDownloader::URLLoadStartNotify(int32_t pp_error) { |
273 PLUGIN_PRINTF(("FileDownloader::URLLoadStartNotify (pp_error=%" | 279 PLUGIN_PRINTF(("FileDownloader::URLLoadStartNotify (pp_error=%" |
274 NACL_PRId32")\n", pp_error)); | 280 NACL_PRId32")\n", pp_error)); |
275 | 281 |
276 if (!InitialResponseIsValid(pp_error)) | 282 if (!InitialResponseIsValid(pp_error)) |
283 // InitialResponseIsValid() calls file_open_notify_callback_ on errors. | |
277 return; | 284 return; |
285 | |
286 if (open_and_stream_) | |
287 return FinishStreaming(file_open_notify_callback_); | |
288 | |
289 file_open_notify_callback_.Run(pp_error); | |
290 } | |
291 | |
292 void FileDownloader::URLBufferStartNotify(int32_t pp_error) { | |
293 PLUGIN_PRINTF(("FileDownloader::URLBufferStartNotify (pp_error=%" | |
294 NACL_PRId32")\n", pp_error)); | |
295 | |
296 if (!InitialResponseIsValid(pp_error)) | |
297 // InitialResponseIsValid() calls file_open_notify_callback_ on errors. | |
298 return; | |
299 | |
300 if (open_and_stream_) | |
301 return FinishStreaming(file_open_notify_callback_); | |
302 | |
303 file_open_notify_callback_.Run(pp_error); | |
304 } | |
305 | |
306 void FileDownloader::FinishStreaming( | |
307 const pp::CompletionCallback& callback) { | |
308 stream_finish_callback_ = callback; | |
309 | |
278 // Finish streaming the body providing an optional callback. | 310 // Finish streaming the body providing an optional callback. |
279 pp::CompletionCallback onload_callback = | 311 if (streaming_to_file()) { |
280 callback_factory_.NewOptionalCallback( | 312 pp::CompletionCallback onload_callback = |
281 &FileDownloader::URLLoadFinishNotify); | 313 callback_factory_.NewOptionalCallback( |
282 pp_error = url_loader_.FinishStreamingToFile(onload_callback); | 314 &FileDownloader::URLLoadFinishNotify); |
283 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); | 315 int32_t pp_error = url_loader_.FinishStreamingToFile(onload_callback); |
284 PLUGIN_PRINTF(("FileDownloader::URLLoadStartNotify (async_notify_ok=%d)\n", | 316 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); |
285 async_notify_ok)); | 317 PLUGIN_PRINTF(("FileDownloader::FinishStreaming (async_notify_ok=%d)\n", |
286 if (!async_notify_ok) { | 318 async_notify_ok)); |
287 // Call manually to free allocated memory and report errors. This calls | 319 if (!async_notify_ok) { |
288 // |file_open_notify_callback_| with |pp_error| as the parameter. | 320 // Call manually to free allocated memory and report errors. This calls |
289 onload_callback.Run(pp_error); | 321 // |stream_finish_callback_| with |pp_error| as the parameter. |
322 onload_callback.Run(pp_error); | |
323 } | |
324 } else { | |
325 pp::CompletionCallback onread_callback = | |
326 callback_factory_.NewOptionalCallback( | |
327 &FileDownloader::URLReadBodyNotify); | |
328 int32_t temp_size = static_cast<int32_t>(temp_buffer_.size()); | |
329 int32_t pp_error = url_loader_.ReadResponseBody(&temp_buffer_[0], | |
330 temp_size, | |
331 onread_callback); | |
332 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); | |
333 PLUGIN_PRINTF(( | |
334 "FileDownloader::FinishStreaming (async_notify_ok=%d)\n", | |
335 async_notify_ok)); | |
336 if (!async_notify_ok) { | |
337 onread_callback.Run(pp_error); | |
338 } | |
290 } | 339 } |
291 } | 340 } |
292 | 341 |
293 void FileDownloader::URLLoadFinishNotify(int32_t pp_error) { | 342 void FileDownloader::URLLoadFinishNotify(int32_t pp_error) { |
294 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (pp_error=%" | 343 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (pp_error=%" |
295 NACL_PRId32")\n", pp_error)); | 344 NACL_PRId32")\n", pp_error)); |
296 if (pp_error != PP_OK) { // Streaming failed. | 345 if (pp_error != PP_OK) { // Streaming failed. |
297 file_open_notify_callback_.Run(pp_error); | 346 stream_finish_callback_.Run(pp_error); |
298 return; | 347 return; |
299 } | 348 } |
300 | 349 |
301 pp::URLResponseInfo url_response(url_loader_.GetResponseInfo()); | 350 // Validate response again on load (though it should be the same |
302 // Validated on load. | 351 // as it was during InitialResponseIsValid?). |
303 CHECK(url_response.GetStatusCode() == NACL_HTTP_STATUS_OK || | 352 url_response_ = url_loader_.GetResponseInfo(); |
304 url_response.GetStatusCode() == kExtensionUrlRequestStatusOk); | 353 CHECK(url_response_.GetStatusCode() == NACL_HTTP_STATUS_OK || |
354 url_response_.GetStatusCode() == kExtensionUrlRequestStatusOk); | |
305 | 355 |
306 // Record the full url from the response. | 356 // Record the full url from the response. |
307 pp::Var full_url = url_response.GetURL(); | 357 pp::Var full_url = url_response_.GetURL(); |
308 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (full_url=%s)\n", | 358 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (full_url=%s)\n", |
309 full_url.DebugString().c_str())); | 359 full_url.DebugString().c_str())); |
310 if (!full_url.is_string()) { | 360 if (!full_url.is_string()) { |
311 file_open_notify_callback_.Run(PP_ERROR_FAILED); | 361 stream_finish_callback_.Run(PP_ERROR_FAILED); |
312 return; | 362 return; |
313 } | 363 } |
314 url_ = full_url.AsString(); | 364 url_ = full_url.AsString(); |
315 | 365 |
316 // The file is now fully downloaded. | 366 // The file is now fully downloaded. |
317 pp::FileRef file(url_response.GetBodyAsFileRef()); | 367 pp::FileRef file(url_response_.GetBodyAsFileRef()); |
318 if (file.is_null()) { | 368 if (file.is_null()) { |
319 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (file=NULL)\n")); | 369 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (file=NULL)\n")); |
320 file_open_notify_callback_.Run(PP_ERROR_FAILED); | 370 stream_finish_callback_.Run(PP_ERROR_FAILED); |
321 return; | 371 return; |
322 } | 372 } |
323 | 373 |
324 // Open the file providing an optional callback. | 374 // Open the file providing an optional callback. |
325 pp::CompletionCallback onopen_callback = | 375 pp::CompletionCallback onopen_callback = |
326 callback_factory_.NewOptionalCallback(&FileDownloader::FileOpenNotify); | 376 callback_factory_.NewOptionalCallback( |
377 &FileDownloader::StreamFinishNotify); | |
327 pp_error = file_reader_.Open(file, PP_FILEOPENFLAG_READ, onopen_callback); | 378 pp_error = file_reader_.Open(file, PP_FILEOPENFLAG_READ, onopen_callback); |
328 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); | 379 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); |
329 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (async_notify_ok=%d)\n", | 380 PLUGIN_PRINTF(("FileDownloader::URLLoadFinishNotify (async_notify_ok=%d)\n", |
330 async_notify_ok)); | 381 async_notify_ok)); |
331 if (!async_notify_ok) { | 382 if (!async_notify_ok) { |
332 // Call manually to free allocated memory and report errors. This calls | 383 // Call manually to free allocated memory and report errors. This calls |
333 // |file_open_notify_callback_| with |pp_error| as the parameter. | 384 // |stream_finish_callback_| with |pp_error| as the parameter. |
334 onopen_callback.Run(pp_error); | 385 onopen_callback.Run(pp_error); |
335 } | 386 } |
336 } | 387 } |
337 | 388 |
338 void FileDownloader::URLBufferStartNotify(int32_t pp_error) { | |
339 PLUGIN_PRINTF(("FileDownloader::URLBufferStartNotify (pp_error=%" | |
340 NACL_PRId32")\n", pp_error)); | |
341 | |
342 if (!InitialResponseIsValid(pp_error)) | |
343 return; | |
344 // Finish streaming the body asynchronously providing a callback. | |
345 pp::CompletionCallback onread_callback = | |
346 callback_factory_.NewOptionalCallback(&FileDownloader::URLReadBodyNotify); | |
347 | |
348 int32_t temp_size = static_cast<int32_t>(temp_buffer_.size()); | |
349 pp_error = url_loader_.ReadResponseBody(&temp_buffer_[0], | |
350 temp_size, | |
351 onread_callback); | |
352 bool async_notify_ok = (pp_error == PP_OK_COMPLETIONPENDING); | |
353 PLUGIN_PRINTF(("FileDownloader::URLBufferStartNotify (async_notify_ok=%d)\n", | |
354 async_notify_ok)); | |
355 if (!async_notify_ok) { | |
356 onread_callback.Run(pp_error); | |
357 } | |
358 } | |
359 | |
360 void FileDownloader::URLReadBodyNotify(int32_t pp_error) { | 389 void FileDownloader::URLReadBodyNotify(int32_t pp_error) { |
361 PLUGIN_PRINTF(("FileDownloader::URLReadBodyNotify (pp_error=%" | 390 PLUGIN_PRINTF(("FileDownloader::URLReadBodyNotify (pp_error=%" |
362 NACL_PRId32")\n", pp_error)); | 391 NACL_PRId32")\n", pp_error)); |
363 if (pp_error < PP_OK) { | 392 if (pp_error < PP_OK) { |
364 file_open_notify_callback_.Run(pp_error); | 393 stream_finish_callback_.Run(pp_error); |
365 } else if (pp_error == PP_OK) { | 394 } else if (pp_error == PP_OK) { |
366 if (streaming_to_user()) { | 395 if (streaming_to_user()) { |
367 data_stream_callback_source_->GetCallback().Run(PP_OK); | 396 data_stream_callback_source_->GetCallback().Run(PP_OK); |
368 } | 397 } |
369 FileOpenNotify(PP_OK); | 398 StreamFinishNotify(PP_OK); |
370 } else { | 399 } else { |
371 if (streaming_to_buffer()) { | 400 if (streaming_to_buffer()) { |
372 buffer_.insert(buffer_.end(), &temp_buffer_[0], &temp_buffer_[pp_error]); | 401 buffer_.insert(buffer_.end(), &temp_buffer_[0], &temp_buffer_[pp_error]); |
373 } else if (streaming_to_user()) { | 402 } else if (streaming_to_user()) { |
374 PLUGIN_PRINTF(("Running data_stream_callback, temp_buffer_=%p\n", | 403 PLUGIN_PRINTF(("Running data_stream_callback, temp_buffer_=%p\n", |
375 &temp_buffer_[0])); | 404 &temp_buffer_[0])); |
376 StreamCallback cb = data_stream_callback_source_->GetCallback(); | 405 StreamCallback cb = data_stream_callback_source_->GetCallback(); |
377 *(cb.output()) = &temp_buffer_; | 406 *(cb.output()) = &temp_buffer_; |
378 cb.Run(pp_error); | 407 cb.Run(pp_error); |
379 } | 408 } |
(...skipping 11 matching lines...) Expand all Loading... | |
391 } | 420 } |
392 } | 421 } |
393 | 422 |
394 bool FileDownloader::GetDownloadProgress( | 423 bool FileDownloader::GetDownloadProgress( |
395 int64_t* bytes_received, | 424 int64_t* bytes_received, |
396 int64_t* total_bytes_to_be_received) const { | 425 int64_t* total_bytes_to_be_received) const { |
397 return url_loader_.GetDownloadProgress(bytes_received, | 426 return url_loader_.GetDownloadProgress(bytes_received, |
398 total_bytes_to_be_received); | 427 total_bytes_to_be_received); |
399 } | 428 } |
400 | 429 |
401 void FileDownloader::FileOpenNotify(int32_t pp_error) { | 430 nacl::string FileDownloader::GetResponseHeaders() const { |
402 PLUGIN_PRINTF(("FileDownloader::FileOpenNotify (pp_error=%"NACL_PRId32")\n", | 431 pp::Var headers = url_response_.GetHeaders(); |
403 pp_error)); | 432 if (!headers.is_string()) { |
404 file_open_notify_callback_.Run(pp_error); | 433 PLUGIN_PRINTF(( |
434 "FileDownloader::GetResponseHeaders (headers are not a string)\n")); | |
435 return nacl::string(); | |
436 } | |
437 return headers.AsString(); | |
438 } | |
439 | |
440 void FileDownloader::StreamFinishNotify(int32_t pp_error) { | |
441 PLUGIN_PRINTF(( | |
442 "FileDownloader::StreamFinishNotify (pp_error=%"NACL_PRId32")\n", | |
443 pp_error)); | |
444 stream_finish_callback_.Run(pp_error); | |
405 } | 445 } |
406 | 446 |
407 bool FileDownloader::streaming_to_file() const { | 447 bool FileDownloader::streaming_to_file() const { |
408 return mode_ == DOWNLOAD_TO_FILE; | 448 return mode_ == DOWNLOAD_TO_FILE; |
409 } | 449 } |
410 | 450 |
411 bool FileDownloader::streaming_to_buffer() const { | 451 bool FileDownloader::streaming_to_buffer() const { |
412 return mode_ == DOWNLOAD_TO_BUFFER; | 452 return mode_ == DOWNLOAD_TO_BUFFER; |
413 } | 453 } |
414 | 454 |
415 bool FileDownloader::streaming_to_user() const { | 455 bool FileDownloader::streaming_to_user() const { |
416 return mode_ == DOWNLOAD_STREAM; | 456 return mode_ == DOWNLOAD_STREAM; |
417 } | 457 } |
418 | 458 |
419 bool FileDownloader::not_streaming() const { | 459 bool FileDownloader::not_streaming() const { |
420 return mode_ == DOWNLOAD_NONE; | 460 return mode_ == DOWNLOAD_NONE; |
421 } | 461 } |
422 | 462 |
423 } // namespace plugin | 463 } // namespace plugin |
OLD | NEW |