Test Failed
Branch master (206474)
by Fabio
18:24
created

TActiveFileUpload::setAutoPostBack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * TActiveFileUpload.php
4
 *
5
 * @author Bradley Booms <[email protected]>
6
 * @author Christophe Boulain <[email protected]>
7
 * @author Gabor Berczi <[email protected]> (issue 349 remote vulnerability fix)
8
 * @author LANDWEHR Computer und Software GmbH <[email protected]>
9
 * @package Prado\Web\UI\ActiveControls
10
 */
11
12
namespace Prado\Web\UI\ActiveControls;
13
14
/**
15
 * Load TActiveControlAdapter and TFileUpload.
16
 */
17
use Exception;
18
use Prado\Exceptions\TInvalidDataValueException;
19
use Prado\Prado;
20
use Prado\TPropertyValue;
21
use Prado\Web\UI\INamingContainer;
22
use Prado\Web\UI\WebControls\TFileUpload;
23
use Prado\Web\UI\WebControls\THiddenField;
24
use Prado\Web\UI\WebControls\TImage;
25
use Prado\Web\UI\WebControls\TInlineFrame;
26
use Prado\Web\Javascripts\TJavaScript;
27
28
/**
29
 * TActiveFileUpload
30
 *
31
 * TActiveFileUpload displays a file upload field on a page. Upon postback,
32
 * the text entered into the field will be treated as the name of the file
33
 * that will be uploaded to the server. The property {@link getHasFile HasFile}
34
 * indicates whether the file upload is successful. If successful, the file
35
 * may be obtained by calling {@link saveAs} to save it at a specified place.
36
 * You can use {@link getFileName FileName}, {@link getFileType FileType},
37
 * {@link getFileSize FileSize} to get the original client-side file name,
38
 * the file mime type, and the file size information. If the upload is not
39
 * successful, {@link getErrorCode ErrorCode} contains the error code
40
 * describing the cause of failure.
41
 *
42
 * TActiveFileUpload raises {@link onFileUpload OnFileUpload} event if a file is uploaded
43
 * (whether it succeeds or not).
44
 *
45
 * TActiveFileUpload actually does a postback in a hidden IFrame, and then does a callback.
46
 * This callback then raises the {@link onFileUpload OnFileUpload} event. After the postback
47
 * a status icon is displayed; either a green checkmark if the upload is successful,
48
 * or a red x if there was an error.
49
 *
50
 * TActiveFileUpload needs either an application level cache or a security manager to work securely.
51
 *
52
 * Since Prado 4.0 the TActiveFileUpload supports HTML5 multiple file uploads by setting the
53
 * {@link setMultiple Multiple} attribute to true. See the description of the parent class
54
 * {@see TFileUpload} for further details.
55
 *
56
 * @author Bradley Booms <[email protected]>
57
 * @author Christophe Boulain <[email protected]>
58
 * @author LANDWEHR Computer und Software GmbH <[email protected]>
59
 * @package Prado\Web\UI\ActiveControls
60
 */
61
class TActiveFileUpload extends TFileUpload implements IActiveControl, ICallbackEventHandler, INamingContainer
62
{
63
	const SCRIPT_PATH = 'activefileupload';
64
65
	/**
66
	 * @var THiddenField a flag to tell which component is doing the callback.
67
	 */
68
	private $_flag;
69
	/**
70
	 * @var TImage that spins to show that the file is being uploaded.
71
	 */
72
	private $_busy;
73
	/**
74
	 * @var TImage that shows a green check mark for completed upload.
75
	 */
76
	private $_success;
77
	/**
78
	 * @var TImage that shows a red X for incomplete upload.
79
	 */
80
	private $_error;
81
	/**
82
	 * @var TInlineFrame used to submit the data in an "asynchronous" fashion.
83
	 */
84
	private $_target;
85
86
87
	/**
88
	 * Creates a new callback control, sets the adapter to
89
	 * TActiveControlAdapter. If you override this class, be sure to set the
90
	 * adapter appropriately by, for example, by calling this constructor.
91
	 */
92
	public function __construct()
93
	{
94
		parent::__construct();
95
		$this->setAdapter(new TActiveControlAdapter($this));
96
	}
97
98
99
	/**
100
	 * @param string $file asset file in the self::SCRIPT_PATH directory.
101
	 * @return string asset file url.
102
	 */
103
	protected function getAssetUrl($file = '')
104
	{
105
		$base = $this->getPage()->getClientScript()->getPradoScriptAssetUrl();
106
		return $base . '/' . self::SCRIPT_PATH . '/' . $file;
107
	}
108
109
110
	/**
111
	 * This method is invoked when a file is uploaded.
112
	 * If you override this method, be sure to call the parent implementation to ensure
113
	 * the invocation of the attached event handlers.
114
	 * @param TEventParameter $param event parameter to be passed to the event handlers
115
	 */
116
	public function onFileUpload($param)
117
	{
118
		if ($this->_flag->getValue() && $this->getPage()->getIsPostBack() && $param == $this->_target->getUniqueID()) {
119
			$params = new TActiveFileUploadCallbackParams;
120
			// save the files so that they will persist past the end of this return.
121 View Code Duplication
			foreach ($this->getFiles() as $file) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
				$localName = str_replace('\\', '/', tempnam(Prado::getPathOfNamespace($this->getTempPath()), ''));
123
				$file->saveAs($localName);
124
				$file->setLocalName($localName);
125
				$params->files[] = $file->toArray();
126
			}
127
128
			// return some javascript to display a completion status.
129
			echo "<script language='Javascript'>
130
          	 Options = new Object();
131
          	 Options.clientID = '{$this->getClientID()}';
132
          	 Options.targetID = '{$this->_target->getUniqueID()}';
133
          	 Options.errorCode = '" . (int) !$this->getHasAllFiles() . "';
134
          	 Options.callbackToken = '{$this->pushParamsAndGetToken($params)}';
135
          	 Options.fileName = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileName') : $this->getFileName(), JSON_HEX_APOS) . "';
136
             Options.fileSize = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileSize') : $this->getFileSize(), JSON_HEX_APOS) . "';
137
             Options.fileType = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileType') : $this->getFileType(), JSON_HEX_APOS) . "';
138
             Options.errorCode = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'errorCode') : $this->getErrorCode(), JSON_HEX_APOS) . "';
139
          	 parent.Prado.WebUI.TActiveFileUpload.onFileUpload(Options);
140
           </script>";
141
142
			exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method onFileUpload() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
143
		}
144
	}
145
146
	/**
147
	 * @return string the path where the uploaded file will be stored temporarily, in namespace format
148
	 * default "Application.runtime.*"
149
	 */
150
	public function getTempPath()
151
	{
152
		return $this->getViewState('TempPath', 'Application.runtime.*');
153
	}
154
155
	/**
156
	 * @param string $value the path where the uploaded file will be stored temporarily in namespace format
157
	 * default "Application.runtime.*"
158
	 */
159
	public function setTempPath($value)
160
	{
161
		$this->setViewState('TempPath', $value, 'Application.runtime.*');
162
	}
163
164
	/**
165
	 * @return bool a value indicating whether an automatic callback to the server will occur whenever the user modifies the text in the TTextBox control and then tabs out of the component. Defaults to true.
166
	 * Note: When set to false, you will need to trigger the callback yourself.
167
	 */
168
	public function getAutoPostBack()
169
	{
170
		return $this->getViewState('AutoPostBack', true);
171
	}
172
173
	/**
174
	 * @param bool $value a value indicating whether an automatic callback to the server will occur whenever the user modifies the text in the TTextBox control and then tabs out of the component. Defaults to true.
175
	 * Note: When set to false, you will need to trigger the callback yourself.
176
	 */
177
	public function setAutoPostBack($value)
178
	{
179
		$this->setViewState('AutoPostBack', TPropertyValue::ensureBoolean($value), true);
180
	}
181
182
	/**
183
	 * @return string A chuck of javascript that will need to be called if {{@link getAutoPostBack AutoPostBack} is set to false}
184
	 */
185
	public function getCallbackJavascript()
186
	{
187
		return "Prado.WebUI.TActiveFileUpload.fileChanged(\"{$this->getClientID()}\")";
188
	}
189
190
	/**
191
	 * @param mixed $sender
192
	 * @throws TInvalidDataValueException if the {@link getTempPath TempPath} is not writable.
193
	 */
194
	public function onInit($sender)
195
	{
196
		parent::onInit($sender);
197
198
		if (!Prado::getApplication()->getCache()) {
199
			if (!Prado::getApplication()->getSecurityManager()) {
200
				throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
201
			}
202
		}
203
204
		if (!is_writable(Prado::getPathOfNamespace($this->getTempPath()))) {
205
			throw new TInvalidDataValueException("activefileupload_temppath_invalid", $this->getTempPath());
206
		}
207
	}
208
209
	/**
210
	 * Raises <b>OnFileUpload</b> event.
211
	 *
212
	 * This method is required by {@link ICallbackEventHandler} interface.
213
	 * This method is mainly used by framework and control developers.
214
	 * @param TCallbackEventParameter $param the event parameter
215
	 */
216
	public function raiseCallbackEvent($param)
0 ignored issues
show
Coding Style introduced by
raiseCallbackEvent uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
217
	{
218
		$cp = $param->getCallbackParameter();
219
		if ($key = $cp->targetID == $this->_target->getUniqueID()) {
220
			$params = $this->popParamsByToken($cp->callbackToken);
221
			foreach ($params->files as $index => $file) {
222
				$_FILES[$key]['name'][$index] = stripslashes($file['fileName']);
223
				$_FILES[$key]['size'][$index] = (int) ($file['fileSize']);
224
				$_FILES[$key]['type'][$index] = $file['fileType'];
225
				$_FILES[$key]['error'][$index] = (int) ($file['errorCode']);
226
				$_FILES[$key]['tmp_name'][$index] = $file['localName'];
227
			}
228
			$this->loadPostData($key, null);
229
230
			$this->raiseEvent('OnFileUpload', $this, $param);
231
		}
232
	}
233
234
	/**
235
	 * Raises postdata changed event.
236
	 * This method calls {@link onFileUpload} method
237
	 * This method is primarily used by framework developers.
238
	 */
239
	public function raisePostDataChangedEvent()
240
	{
241
		$this->onFileUpload($this->getPage()->getRequest()->itemAt('TActiveFileUpload_TargetId'));
242
	}
243
244
	protected function pushParamsAndGetToken(TActiveFileUploadCallbackParams $params)
245
	{
246
		if ($cache = Prado::getApplication()->getCache()) {
247
			// this is the most secure method, file info can't be forged from client side, no matter what
248
			$token = md5('TActiveFileUpload::Params::' . $this->ClientID . '::' . rand(1000 * 1000, 9999 * 1000));
249
			$cache->set($token, serialize($params), 5 * 60); // expire in 5 minutes - the callback should arrive back in seconds, actually
250
		} elseif ($mgr = Prado::getApplication()->getSecurityManager()) {
251
			// this is a less secure method, file info can be still forged from client side, but only if attacker knows the secret application key
252
			$token = urlencode(base64_encode($mgr->encrypt(serialize($params))));
253
		} else {
254
			throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
255
		}
256
257
		return $token;
258
	}
259
260
	protected function popParamsByToken($token)
261
	{
262
		if ($cache = Prado::getApplication()->getCache()) {
263
			$v = $cache->get($token);
264
			assert($v != '');
265
			$cache->delete($token); // remove it from cache so it can't be used again and won't take up space either
266
			$params = unserialize($v);
267
		} elseif ($mgr = Prado::getApplication()->getSecurityManager()) {
268
			$v = $mgr->decrypt(base64_decode(urldecode($token)));
269
			$params = unserialize($v);
270
		} else {
271
			throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
272
		}
273
274
		assert($params instanceof TActiveFileUploadCallbackParams);
275
276
		return $params;
277
	}
278
279
	/**
280
	 * Publish the javascript
281
	 * @param mixed $param
282
	 */
283
	public function onPreRender($param)
0 ignored issues
show
Coding Style introduced by
onPreRender uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
284
	{
285
		parent::onPreRender($param);
286
287
		if (!$this->getPage()->getIsPostBack() && isset($_GET['TActiveFileUpload_InputId']) && isset($_GET['TActiveFileUpload_TargetId']) && $_GET['TActiveFileUpload_InputId'] == $this->getClientID()) {
288
			$params = new TActiveFileUploadCallbackParams;
289 View Code Duplication
			foreach ($this->getFiles() as $file) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
				$localName = str_replace('\\', '/', tempnam(Prado::getPathOfNamespace($this->getTempPath()), ''));
291
				$file->setLocalName($localName);
292
				// tricky workaround to intercept "uploaded file too big" error: real uploads happens in onFileUpload instead
293
				$file->setErrorCode(UPLOAD_ERR_FORM_SIZE);
294
				$params->files[] = $file->toArray();
295
			}
296
297
			echo "<script language='Javascript'>
298
          	 Options = new Object();
299
          	 Options.clientID = '{$_GET['TActiveFileUpload_InputId']}';
300
          	 Options.targetID = '{$_GET['TActiveFileUpload_TargetId']}';
301
			       Options.errorCode = '" . (int) !$this->getHasAllFiles() . "';
302
          	 Options.callbackToken = '{$this->pushParamsAndGetToken($params)}';
303
          	 Options.fileName = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileName') : $this->getFileName(), JSON_HEX_APOS) . "';
304
             Options.fileSize = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileSize') : $this->getFileSize(), JSON_HEX_APOS) . "';
305
             Options.fileType = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'fileType') : $this->getFileType(), JSON_HEX_APOS) . "';
306
             Options.errorCode = '" . TJavaScript::jsonEncode($this->getMultiple() ? array_column($params->files, 'errorCode') : $this->getErrorCode(), JSON_HEX_APOS) . "';
307
           	 parent.Prado.WebUI.TActiveFileUpload.onFileUpload(Options);
308
           </script>";
309
		}
310
	}
311
312
	public function createChildControls()
313
	{
314
		$this->getPage()->getClientScript()->registerPradoScript('activefileupload');
315
316
		$this->_flag = new THiddenField;
317
		$this->_flag->setID('Flag');
318
		$this->getControls()->add($this->_flag);
319
320
		$this->_busy = new TImage;
321
		$this->_busy->setID('Busy');
322
		$this->_busy->setImageUrl($this->getAssetUrl('ActiveFileUploadIndicator.gif'));
323
		$this->_busy->setStyle("display:none");
324
		$this->getControls()->add($this->_busy);
325
326
		$this->_success = new TImage;
327
		$this->_success->setID('Success');
328
		$this->_success->setImageUrl($this->getAssetUrl('ActiveFileUploadComplete.png'));
329
		$this->_success->setStyle("display:none");
330
		$this->getControls()->add($this->_success);
331
332
		$this->_error = new TImage;
333
		$this->_error->setID('Error');
334
		$this->_error->setImageUrl($this->getAssetUrl('ActiveFileUploadError.png'));
335
		$this->_error->setStyle("display:none");
336
		$this->getControls()->add($this->_error);
337
338
		$this->_target = new TInlineFrame;
339
		$this->_target->setID('Target');
340
		$this->_target->setFrameUrl($this->getAssetUrl('ActiveFileUploadBlank.html'));
341
		$this->_target->setStyle("width:0px; height:0px;");
342
		$this->_target->setShowBorder(false);
343
		$this->getControls()->add($this->_target);
344
	}
345
346
	/**
347
	 * Removes localfile on ending of the callback.
348
	 * @param mixed $param
349
	 */
350
	public function onUnload($param)
351
	{
352
		if ($this->getPage()->getIsCallback()) {
353
			foreach ($this->getFiles() as $file) {
354
				if ($file->getHasFile() && file_exists($file->getLocalName())) {
355
					unlink($file->getLocalName());
356
				}
357
			}
358
		}
359
		parent::onUnload($param);
360
	}
361
362
	/**
363
	 * @return TBaseActiveCallbackControl standard callback control options.
364
	 */
365
	public function getActiveControl()
366
	{
367
		return $this->getAdapter()->getBaseActiveControl();
368
	}
369
370
	/**
371
	 * @return TCallbackClientSide client side request options.
372
	 */
373
	public function getClientSide()
374
	{
375
		return $this->getAdapter()->getBaseActiveControl()->getClientSide();
376
	}
377
378
	/**
379
	 * Adds ID attribute, and renders the javascript for active component.
380
	 * @param THtmlWriter $writer the writer used for the rendering purpose
381
	 */
382
	public function addAttributesToRender($writer)
383
	{
384
		parent::addAttributesToRender($writer);
385
		$writer->addAttribute('id', $this->getClientID());
386
387
		$this->getActiveControl()->registerCallbackClientScript($this->getClientClassName(), $this->getClientOptions());
388
	}
389
390
	/**
391
	 * @return string corresponding javascript class name for this control.
392
	 */
393
	protected function getClientClassName()
394
	{
395
		return 'Prado.WebUI.TActiveFileUpload';
396
	}
397
398
	/**
399
	 * Gets the client side options for this control.
400
	 * @return array (	inputID => input client ID,
401
	 * 					flagID => flag client ID,
402
	 * 					targetName => target unique ID,
403
	 * 					formID => form client ID,
404
	 * 					indicatorID => upload indicator client ID,
405
	 * 					completeID => complete client ID,
406
	 * 					errorID => error client ID)
407
	 */
408
	protected function getClientOptions()
409
	{
410
		$options['ID'] = $this->getClientID();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
411
		$options['EventTarget'] = $this->getUniqueID();
412
413
		$options['inputID'] = $this->getClientID();
414
		$options['flagID'] = $this->_flag->getClientID();
415
		$options['targetID'] = $this->_target->getUniqueID();
416
		$options['formID'] = $this->getPage()->getForm()->getClientID();
417
		$options['indicatorID'] = $this->_busy->getClientID();
418
		$options['completeID'] = $this->_success->getClientID();
419
		$options['errorID'] = $this->_error->getClientID();
420
		$options['autoPostBack'] = $this->getAutoPostBack();
421
		return $options;
422
	}
423
424
	/**
425
	 * Saves the uploaded file.
426
	 * @param string $fileName the file name used to save the uploaded file
427
	 * @param bool $deleteTempFile whether to delete the temporary file after saving.
428
	 * @param int $index the index of the uploaded file, defaults to 0.
429
	 * If true, you will not be able to save the uploaded file again.
430
	 * @return bool true if the file saving is successful
431
	 */
432
	public function saveAs($fileName, $deleteTempFile = true, $index = 0)
433
	{
434
		if (($this->getErrorCode($index) === UPLOAD_ERR_OK) && (file_exists($this->getLocalName($index)))) {
435
			if ($deleteTempFile) {
436
				return rename($this->getLocalName($index), $fileName);
437
			} else {
438
				return copy($this->getLocalName($index), $fileName);
439
			}
440
		} else {
441
			return false;
442
		}
443
	}
444
445
	/**
446
	 * @return TImage the image displayed when an upload
447
	 * 		completes successfully.
448
	 */
449
	public function getSuccessImage()
450
	{
451
		$this->ensureChildControls();
452
		return $this->_success;
453
	}
454
455
	/**
456
	 * @return TImage the image displayed when an upload
457
	 * 		does not complete successfully.
458
	 */
459
	public function getErrorImage()
460
	{
461
		$this->ensureChildControls();
462
		return $this->_error;
463
	}
464
465
	/**
466
	 * @return TImage the image displayed when an upload
467
	 * 		is in progress.
468
	 */
469
	public function getBusyImage()
470
	{
471
		$this->ensureChildControls();
472
		return $this->_busy;
473
	}
474
}
475