Completed
Push — namespace2 ( 791eac...5c23fb )
by Fabio
08:41
created

TActiveFileUpload::getTempPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
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
 * @package Prado\Web\UI\ActiveControls
9
 */
10
11
namespace Prado\Web\UI\ActiveControls;
12
13
/**
14
 * Load TActiveControlAdapter and TFileUpload.
15
 */
16
use Exception;
17
use Prado\Exceptions\TInvalidDataValueException;
18
use Prado\Prado;
19
use Prado\TPropertyValue;
20
use Prado\Web\UI\INamingContainer;
21
use Prado\Web\UI\WebControls\TFileUpload;
22
use Prado\Web\UI\WebControls\THiddenField;
23
use Prado\Web\UI\WebControls\TImage;
24
use Prado\Web\UI\WebControls\TInlineFrame;
25
26
/**
27
 * TActiveFileUpload
28
 *
29
 * TActiveFileUpload displays a file upload field on a page. Upon postback,
30
 * the text entered into the field will be treated as the name of the file
31
 * that will be uploaded to the server. The property {@link getHasFile HasFile}
32
 * indicates whether the file upload is successful. If successful, the file
33
 * may be obtained by calling {@link saveAs} to save it at a specified place.
34
 * You can use {@link getFileName FileName}, {@link getFileType FileType},
35
 * {@link getFileSize FileSize} to get the original client-side file name,
36
 * the file mime type, and the file size information. If the upload is not
37
 * successful, {@link getErrorCode ErrorCode} contains the error code
38
 * describing the cause of failure.
39
 *
40
 * TActiveFileUpload raises {@link onFileUpload OnFileUpload} event if a file is uploaded
41
 * (whether it succeeds or not).
42
 *
43
 * TActiveFileUpload actually does a postback in a hidden IFrame, and then does a callback.
44
 * This callback then raises the {@link onFileUpload OnFileUpload} event. After the postback
45
 * a status icon is displayed; either a green checkmark if the upload is successful,
46
 * or a red x if there was an error.
47
 *
48
 * TActiveFileUpload needs either an application level cache or a security manager to work securely.
49
 *
50
 * @author Bradley Booms <[email protected]>
51
 * @author Christophe Boulain <[email protected]>
52
 * @package Prado\Web\UI\ActiveControls
53
 */
54
class TActiveFileUpload extends TFileUpload implements IActiveControl, ICallbackEventHandler, INamingContainer
55
{
56
57
	const SCRIPT_PATH = 'prado/activefileupload';
58
59
	/**
60
	 * @var THiddenField a flag to tell which component is doing the callback.
61
	 */
62
	private $_flag;
63
	/**
64
	 * @var TImage that spins to show that the file is being uploaded.
65
	 */
66
	private $_busy;
67
	/**
68
	 * @var TImage that shows a green check mark for completed upload.
69
	 */
70
	private $_success;
71
	/**
72
	 * @var TImage that shows a red X for incomplete upload.
73
	 */
74
	private $_error;
75
	/**
76
	 * @var TInlineFrame used to submit the data in an "asynchronous" fashion.
77
	 */
78
	private $_target;
79
80
81
	/**
82
	 * Creates a new callback control, sets the adapter to
83
	 * TActiveControlAdapter. If you override this class, be sure to set the
84
	 * adapter appropriately by, for example, by calling this constructor.
85
	 */
86
	public function __construct(){
87
		parent::__construct();
88
		$this->setAdapter(new TActiveControlAdapter($this));
89
	}
90
91
92
	/**
93
	 * @param string asset file in the self::SCRIPT_PATH directory.
94
	 * @return string asset file url.
95
	 */
96
	protected function getAssetUrl($file='')
97
	{
98
		$base = $this->getPage()->getClientScript()->getPradoScriptAssetUrl();
99
		return $base.'/'.self::SCRIPT_PATH.'/'.$file;
100
	}
101
102
103
	/**
104
	 * This method is invoked when a file is uploaded.
105
	 * If you override this method, be sure to call the parent implementation to ensure
106
	 * the invocation of the attached event handlers.
107
	 * @param TEventParameter event parameter to be passed to the event handlers
108
	 */
109
	public function onFileUpload($param)
110
	{
111
		if ($this->_flag->getValue() && $this->getPage()->getIsPostBack() && $param == $this->_target->getUniqueID()){
112
			// save the file so that it will persist past the end of this return.
113
			$localName = str_replace('\\', '/', tempnam(Prado::getPathOfNamespace($this->getTempPath()),''));
114
			parent::saveAs($localName);
115
			$this->_localName = $localName;
116
117
			$params = new TActiveFileUploadCallbackParams;
118
			$params->localName = $localName;
119
			$params->fileName = addslashes($this->getFileName());
120
			$params->fileSize = $this->getFileSize();
121
			$params->fileType = $this->getFileType();
122
			$params->errorCode = $this->getErrorCode();
123
124
			// return some javascript to display a completion status.
125
			echo <<<EOS
126
<script language="Javascript">
127
	Options = new Object();
128
	Options.clientID = '{$this->getClientID()}';
129
	Options.targetID = '{$this->_target->getUniqueID()}';
130
	Options.fileName = '{$params->fileName}';
131
	Options.fileSize = '{$params->fileSize}';
132
	Options.fileType = '{$params->fileType}';
133
	Options.errorCode = '{$params->errorCode}';
134
	Options.callbackToken = '{$this->pushParamsAndGetToken($params)}';
135
	parent.Prado.WebUI.TActiveFileUpload.onFileUpload(Options);
136
</script>
137
EOS;
138
139
			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...
140
		}
141
	}
142
143
	/**
144
	 * @return string the path where the uploaded file will be stored temporarily, in namespace format
145
	 * default "Application.runtime.*"
146
	 */
147
	public function getTempPath(){
148
		return $this->getViewState('TempPath', 'Application.runtime.*');
149
	}
150
151
	/**
152
	 * @param string the path where the uploaded file will be stored temporarily in namespace format
153
	 * default "Application.runtime.*"
154
	 */
155
	public function setTempPath($value){
156
		$this->setViewState('TempPath',$value,'Application.runtime.*');
157
	}
158
159
	/**
160
	 * @return boolean 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.
161
	 * Note: When set to false, you will need to trigger the callback yourself.
162
	 */
163
	public function getAutoPostBack(){
164
		return $this->getViewState('AutoPostBack', true);
165
	}
166
167
	/**
168
	 * @param boolean 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.
169
	 * Note: When set to false, you will need to trigger the callback yourself.
170
	 */
171
	public function setAutoPostBack($value){
172
		$this->setViewState('AutoPostBack',TPropertyValue::ensureBoolean($value),true);
173
	}
174
175
	/**
176
	 * @return string A chuck of javascript that will need to be called if {{@link getAutoPostBack AutoPostBack} is set to false}
177
	 */
178
	public function getCallbackJavascript(){
179
		return "Prado.WebUI.TActiveFileUpload.fileChanged(\"{$this->getClientID()}\")";
180
	}
181
182
	/**
183
	 * @throws TInvalidDataValueException if the {@link getTempPath TempPath} is not writable.
184
	 */
185
	public function onInit($sender){
186
		parent::onInit($sender);
187
188
		if (!Prado::getApplication()->getCache())
189
		  if (!Prado::getApplication()->getSecurityManager())
190
			throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
191
192
		if (!is_writable(Prado::getPathOfNamespace($this->getTempPath()))){
193
			throw new TInvalidDataValueException("activefileupload_temppath_invalid", $this->getTempPath());
194
		}
195
	}
196
197
	/**
198
	 * Raises <b>OnFileUpload</b> event.
199
	 *
200
	 * This method is required by {@link ICallbackEventHandler} interface.
201
	 * This method is mainly used by framework and control developers.
202
	 * @param TCallbackEventParameter the event parameter
203
	 */
204
 	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...
205
 		$cp = $param->getCallbackParameter();
206
		if ($key = $cp->targetID == $this->_target->getUniqueID()){
207
208
			$params = $this->popParamsByToken($cp->callbackToken);
209
210
			$_FILES[$key]['name'] = stripslashes($params->fileName);
211
			$_FILES[$key]['size'] = intval($params->fileSize);
212
			$_FILES[$key]['type'] = $params->fileType;
213
			$_FILES[$key]['error'] = intval($params->errorCode);
214
			$_FILES[$key]['tmp_name'] = $params->localName;
215
			$this->loadPostData($key, null);
216
217
			$this->raiseEvent('OnFileUpload', $this, $param);
218
		}
219
	}
220
221
	/**
222
	 * Raises postdata changed event.
223
	 * This method calls {@link onFileUpload} method
224
	 * This method is primarily used by framework developers.
225
	 */
226
	public function raisePostDataChangedEvent()
227
	{
228
		$this->onFileUpload($this->getPage()->getRequest()->itemAt('TActiveFileUpload_TargetId'));
229
	}
230
231
	protected function pushParamsAndGetToken(TActiveFileUploadCallbackParams $params)
232
	{
233
		if ($cache = Prado::getApplication()->getCache())
234
			{
235
				// this is the most secure method, file info can't be forged from client side, no matter what
236
				$token = md5('TActiveFileUpload::Params::'.$this->ClientID.'::'+rand(1000*1000,9999*1000));
237
				$cache->set($token, serialize($params), 5*60); // expire in 5 minutes - the callback should arrive back in seconds, actually
238
			}
239
		else
240
		if ($mgr = Prado::getApplication()->getSecurityManager())
241
			{
242
				// this is a less secure method, file info can be still forged from client side, but only if attacker knows the secret application key
243
				$token = urlencode(base64_encode($mgr->encrypt(serialize($params))));
244
			}
245
		else
246
			throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
247
248
		return $token;
249
	}
250
251
	protected function popParamsByToken($token)
252
	{
253
		if ($cache = Prado::getApplication()->getCache())
254
			{
255
				$v = $cache->get($token);
256
				assert($v!='');
257
				$cache->delete($token); // remove it from cache so it can't be used again and won't take up space either
258
				$params = unserialize($v);
259
			}
260
		else
261
		if ($mgr = Prado::getApplication()->getSecurityManager())
262
			{
263
				$v = $mgr->decrypt(base64_decode(urldecode($token)));
264
				$params = unserialize($v);
265
			}
266
		else
267
			throw new Exception('TActiveFileUpload needs either an application level cache or a security manager to work securely');
268
269
		assert($params instanceof TActiveFileUploadCallbackParams);
270
271
		return $params;
272
	}
273
274
	/**
275
	 * Publish the javascript
276
	 */
277
	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...
278
	{
279
		parent::onPreRender($param);
280
281
		if(!$this->getPage()->getIsPostBack() && isset($_GET['TActiveFileUpload_InputId']) && isset($_GET['TActiveFileUpload_TargetId']) && $_GET['TActiveFileUpload_InputId'] == $this->getClientID())
282
		{
283
			// tricky workaround to intercept "uploaded file too big" error: real uploads happens in onFileUpload instead
284
			$this->_errorCode = UPLOAD_ERR_FORM_SIZE;
285
			$localName = str_replace('\\', '/', tempnam(Prado::getPathOfNamespace($this->getTempPath()),''));
286
			$fileName = addslashes($this->getFileName());
287
288
			$params = new TActiveFileUploadCallbackParams;
289
			$params->localName = $localName;
290
			$params->fileName = $fileName;
291
			$params->fileSize = $this->getFileSize();
292
			$params->fileType = $this->getFileType();
293
			$params->errorCode = $this->getErrorCode();
294
295
			echo <<<EOS
296
<script language="Javascript">
297
	Options = new Object();
298
	Options.clientID = '{$_GET['TActiveFileUpload_InputId']}';
299
	Options.targetID = '{$_GET['TActiveFileUpload_TargetId']}';
300
	Options.fileName = '{$params->fileName}';
301
	Options.fileSize = '{$params->fileSize}';
302
	Options.fileType = '{$params->fileType}';
303
	Options.errorCode = '{$params->errorCode}';
304
	Options.callbackToken = '{$this->pushParamsAndGetToken($params)}';
305
	parent.Prado.WebUI.TActiveFileUpload.onFileUpload(Options);
306
</script>
307
EOS;
308
		}
309
	}
310
311
	public function createChildControls()
312
	{
313
		$this->_flag = new THiddenField;
314
		$this->_flag->setID('Flag');
315
		$this->getControls()->add($this->_flag);
316
317
		$this->_busy = new TImage;
318
		$this->_busy->setID('Busy');
319
		$this->_busy->setImageUrl($this->getAssetUrl('ActiveFileUploadIndicator.gif'));
320
		$this->_busy->setStyle("display:none");
321
		$this->getControls()->add($this->_busy);
322
323
		$this->_success = new TImage;
324
		$this->_success->setID('Success');
325
		$this->_success->setImageUrl($this->getAssetUrl('ActiveFileUploadComplete.png'));
326
		$this->_success->setStyle("display:none");
327
		$this->getControls()->add($this->_success);
328
329
		$this->_error = new TImage;
330
		$this->_error->setID('Error');
331
		$this->_error->setImageUrl($this->getAssetUrl('ActiveFileUploadError.png'));
332
		$this->_error->setStyle("display:none");
333
		$this->getControls()->add($this->_error);
334
335
		$this->_target = new TInlineFrame;
336
		$this->_target->setID('Target');
337
		$this->_target->setFrameUrl($this->getAssetUrl('ActiveFileUploadBlank.html'));
338
		$this->_target->setStyle("width:0px; height:0px;");
339
		$this->_target->setShowBorder(false);
340
		$this->getControls()->add($this->_target);
341
	}
342
343
	/**
344
	 * Removes localfile on ending of the callback.
345
	 */
346
	public function onUnload($param){
347
		if ($this->getPage()->getIsCallback() &&
348
			$this->getHasFile() &&
349
			file_exists($this->getLocalName())){
350
				unlink($this->getLocalName());
351
		}
352
		parent::onUnload($param);
353
	}
354
355
	/**
356
	 * @return TBaseActiveCallbackControl standard callback control options.
357
	 */
358
	public function getActiveControl(){
359
		return $this->getAdapter()->getBaseActiveControl();
360
	}
361
362
	/**
363
	 * @return TCallbackClientSide client side request options.
364
	 */
365
	public function getClientSide()
366
	{
367
		return $this->getAdapter()->getBaseActiveControl()->getClientSide();
368
	}
369
370
	/**
371
	 * Adds ID attribute, and renders the javascript for active component.
372
	 * @param THtmlWriter the writer used for the rendering purpose
373
	 */
374
	public function addAttributesToRender($writer){
375
		parent::addAttributesToRender($writer);
376
		$writer->addAttribute('id',$this->getClientID());
377
378
		$this->getPage()->getClientScript()->registerPradoScript('activefileupload');
379
		$this->getActiveControl()->registerCallbackClientScript($this->getClientClassName(),$this->getClientOptions());
380
	}
381
382
	/**
383
	 * @return string corresponding javascript class name for this control.
384
	 */
385
	protected function getClientClassName(){
386
		return 'Prado.WebUI.TActiveFileUpload';
387
	}
388
389
	/**
390
	 * Gets the client side options for this control.
391
	 * @return array (	inputID => input client ID,
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|boolean>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
392
	 * 					flagID => flag client ID,
393
	 * 					targetName => target unique ID,
394
	 * 					formID => form client ID,
395
	 * 					indicatorID => upload indicator client ID,
396
	 * 					completeID => complete client ID,
397
	 * 					errorID => error client ID)
398
	 */
399
	protected function getClientOptions(){
400
		$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...
401
		$options['EventTarget'] = $this->getUniqueID();
402
403
		$options['inputID'] = $this->getClientID();
404
		$options['flagID'] = $this->_flag->getClientID();
405
		$options['targetID'] = $this->_target->getUniqueID();
406
		$options['formID'] = $this->getPage()->getForm()->getClientID();
407
		$options['indicatorID'] = $this->_busy->getClientID();
408
		$options['completeID'] = $this->_success->getClientID();
409
		$options['errorID'] = $this->_error->getClientID();
410
		$options['autoPostBack'] = $this->getAutoPostBack();
411
		return $options;
412
	}
413
414
	/**
415
	 * Saves the uploaded file.
416
	 * @param string the file name used to save the uploaded file
417
	 * @param boolean whether to delete the temporary file after saving.
418
	 * If true, you will not be able to save the uploaded file again.
419
	 * @return boolean true if the file saving is successful
420
	 */
421
	public function saveAs($fileName,$deleteTempFile=true){
422
		if (($this->getErrorCode()===UPLOAD_ERR_OK) && (file_exists($this->getLocalName()))){
423
			if ($deleteTempFile)
424
				return rename($this->getLocalName(),$fileName);
425
			else
426
				return copy($this->getLocalName(),$fileName);
427
		} else
428
			return false;
429
	}
430
431
	/**
432
	 * @return TImage the image displayed when an upload
433
	 * 		completes successfully.
434
	 */
435
	public function getSuccessImage(){
436
		$this->ensureChildControls();
437
		return $this->_success;
438
	}
439
440
	/**
441
	 * @return TImage the image displayed when an upload
442
	 * 		does not complete successfully.
443
	 */
444
	public function getErrorImage(){
445
		$this->ensureChildControls();
446
		return $this->_error;
447
	}
448
449
	/**
450
	 * @return TImage the image displayed when an upload
451
	 * 		is in progress.
452
	 */
453
	public function getBusyImage(){
454
		$this->ensureChildControls();
455
		return $this->_busy;
456
	}
457
}
458