Archive for February, 2012

Chrome application redirection loop problem

Recently I tried to pack my game http://qfel13.github.com/xit/ into chrome app. To enable offline playing in my game. When I did this there was problem with redirection to level editor.

Chrome communicated that there is redirection loop.

I checked my code of redirection and there was something like this:

window.href = '/editor/#load';

The problem was that href didn’t point to actual page so ‘/editor/index.html’ but only to folder for which webserver automatically served index.html file.

The same problem was with redirection from editor to game

window.href = '../#load';

Needed to be changed to:

window.href = '../index.html#load';

Everything started to work well after this changes. You can find game app here.

, , ,

Leave a comment

Uploading file in Yii using CUploadedFile

This article explains how to implement file uploading in Yii framework. Let’s start with basics. First, we need to have the form with multipart/form-data encoding and input field of file type.

$form = $this->beginWidget('GxActiveForm', array(
	'id' => 'form-id',
	'enableAjaxValidation' => false,
	'htmlOptions' => array(
		'enctype' => 'multipart/form-data',
	),
));
...
<?php echo $form->fileField($model, 'filename', array('size' => 48)); ?>	

Note! I’m using GxActiveForm instead of CActiveForm because to generate CRUD I’m using giix module which is enhancement of standard gii generator.

Secondly, in the model we need to add file validator which can check the file size or extension.

	public function rules()
	{
		return array_merge(parent::rules(), array(
				array('filename', 
						'file',
						'allowEmpty' => true,
						// 'types'=>'jpg, gif, png',
						'maxSize'=>1024 * 1024 * 50, // 50MB
						'tooLarge'=>'The file was larger than 50MB. Please upload a smaller file.',
				),
			)
		);
	}	

Again, I’m using giix extension which generates base model classes and classes which extends them. Therefore to change default behavior of base class I’m just changing only some values returned by parent class. Please also note that setting ‘maxSize’ may not be sufficient. You also need to set configuration variables in php.ini (upload_max_filesize, post_max_size, memory_limit, etc.)

At the end we also need to add some code to controller (create and update actions). Let’s consider actionUpdate().

$oldfilename = $model->filename; // let's store original filename (if it was defined)
$model->setAttributes($_POST['SomeModel']);

// Now check if there was anything uploaded and store new name if so
$file = CUploadedFile::getInstance($model, 'filename');
if (is_object($file) && get_class($file)==='CUploadedFile') {			
    $model->filename = $file;
} else {
    $model->filename = $oldfilename;
}	

if ($model->save()) {
    if (is_object($file) && get_class($file)==='CUploadedFile') {
        // again, if anything was uploaded and if we have db done then move the file from tmp to the right place
        $model->filename->saveAs(Yii::app()->basePath . '/files/' . $file->filename->name);
        if ($oldfilename != $model->filename->name) {
            unlink($model->fileWithPath(Yii::app()->basePath . '/files/' . $oldfilename));
        }
    }					
    $this->redirect(array('view', 'id' => $model->id));
}

Ok, but still there are some problems. First and foremost, we don’t have a way to remove already added file (we can only replace it with new one). Secondly, if we need to have some access restrictions to uploaded files then we need to randomize their names and have them available via some dispatching php script.

To delete file we will add checkbox to indicate that the file should be deleted, so in the view add following code under fileField:

<?php if ($this->getAction()->getId() == 'update' && strlen($model->filename) > 0) { ?>
    <br /><?php echo CHtml::checkBox('delfile'); ?> check to delete file <?php echo $model->fileLink(); ?>
<?php  } ?>

At the same time new method fileLink() is added to the model to provide link to the action which will provide file itself (and where we will be able to add some access restrictions) and we are adding file deletion methods:

    public function fileLink() {
    	return CHtml::link($this->filename, CHtml::normalizeUrl(array('file', 'id' => $this->id)));
    }

    public function deleteFile() {
    	if (strlen($this->filename) > 0 && file_exists($this->fileWithPath()) && !is_dir($this->fileWithPath())) {
    		unlink($this->fileWithPath());
    	}
    }
    
    protected function afterDelete() {
    	parent::afterDelete();
    	$this->deleteFile();
    }

Now, in the controller we need to add support for file deletion (on update action only, because when calling $model->delete() the above method afterDelete() will be called and this will call file deletion). So, to simplify:

if (isset($_POST['delfile'])) {
    $model->deleteFile();
}

and at the end the action which will provide the file itself:

	public function actionFile($id) {
		$model = $this->loadModel($id, 'So');	
		if (file_exists($model->fileWithPath())) {
			header("Pragma: no-cache");
			header("Expires: 0");
			header('Content-Description: File Transfer');
			header('Content-Type: ' . CFileHelper::getMimeType($model->fileWithPath()));
			header('Content-Disposition: attachment; filename="'.$model->filename.'"');
			header('Content-Transfer-Encoding: binary');
			header('Expires: 0');
			header('Cache-Control: must-revalidate');
			header('Pragma: public');
			header('Content-Length: ' . filesize($model->fileWithPath()));		
			readfile($model->fileWithPath());			
			Yii::app()->end();
		} else {
			throw new CHttpException(404, 'Not found');
		}
	}

5 Comments

How to customize appearance of CJuiAutoComplete in Yii

Before we go to the point of this post let’s start with some basics. It is quite easy to use CJuiAutoComplete widget in your form. Basically you have to add the widget to your form:


$form->widget('zii.widgets.jui.CJuiAutoComplete', array(
  'name' => 'field_name',
  'value' => $model->field_name,
  'sourceUrl' => array('some_view/autoComplete'),
  'options'=>array(
  'minLength' => '2',
  'showAnim' => 'fold',
  'select' => "js: function(event, data) {
    alert(data.item['id']);
  }"
)));

This widget will use ‘sourceUrl’ (ie. autoComplete action of some_view) as a source of possible option to choose from. The function defined in ‘select’ parameter will be triggered when user chooses a value from the list. Other options are rather self descriptive and less important. More info you can find in yii and jquery documentation.

Now, the widget expects that data returned by the action is in JSON format. Here is an example of such action:

public function actionAutoComplete($term) {
  $retVal = array();

  if (strlen($term) >= 2) {
    $model = Wbs::model();

    $criteria = new CDbCriteria();
    $criteria->compare('some_field', $term, true);
    $criteria->compare('other_field', $term, true, 'OR');
    $criteria->compare('another_field', $term, true, 'OR');

    $criteria->limit = 10;

    foreach($model->findAll($criteria) as $item) {
      $retVal[] = array(
        'label' => $item->some_field,
        'value' => $item->some_field,
        'id' => $item->id,
        'other_field' => $item->other_field,
        'another_field' => $item->another_field,
     );
   }
 }

 echo CJSON::encode($retVal);
 Yii::app()->end();
}

Now we’re going to the point. It’s plain to see that it is not a problem to add some other fields to the query, but how to show some more values on the list of autocomplete? In this case jquery documentation can be very helpful (while yii’s docs says nothing about it). What you need to do is add method (or rather replace default one) which renders each element on a list of possible values:

jQuery('#field_name').data('autocomplete')._renderItem = function( ul, item ) {
  return $('<li></li>')
    .data('item.autocomplete', item)
    .append('<a>' + item.some_value + '<br><i>' + item.other_value + '</i></a>')
    .appendTo(ul);
};

I think that it’s easy to see in the above code that the “item” is one element of JSON array returned from autoComplete action and in the function you can use its values.

And there are some important things. First and foremost, ‘#field_name’ has to match the name which is used when CJuiAutoComplete widget is created. Secondly, this javascript code has to be executed when you are sure that this autocomplete element is already initialized in the document! To do this you can use yii’s registerScript() method with CClientScript::POS_READY parameter.

Yii::app()->clientScript->registerScript('autocomplete', "
  jQuery('#field_name').data('autocomplete')._renderItem = function( ul, item ) {
    return $('<li></li>')
      .data('item.autocomplete', item)
      .append('<a>' + item.some_value + '<br><i>' + item.another_value + '</i></a>')
      .appendTo(ul);
  };",
  CClientScript::POS_READY
);

, ,

5 Comments