JavaScript

Same origin policy problem with AJAX/AJAH

In my case the same origin policy problem came up because I needed to make our website content available on a foreign website by using the Reverse Proxy technique. Which means that another website grabs our content and includes it into theirs. So for the users it looks like the content comes native from the foreign website.

The thing which made it a problem was, that we use a lot of “AJAX/AJAH” requests to process  form data etc.

Now if our native domain is native.com, the partner domain is partner.com (which includes our content) but the AJAX resource is still native.com the access would be forbidden by the same origin policy.

For instance if you observe such a XHR with Firebug, you’ll get a 200 Status, but the response body is empty.

If you google for a solution for this problem you’ll stumble across the buzz word Cross-origin resource sharing (CORS). But this technique just works for recent browsers like IE9+ etc.

Another way to solve the problem is the usage of the script-tag which allows cross-origin access and works with almost every common browser.

So lets work out the solution – by the way I use the jQuery framework -

Instead of catching the AJAX data with

    $.get(ajaxurl, function(data) {
      $('#resultsContainer').html(data);
    });

you need to get the data by adding a new script tag with the ajaxurl:
(I avoid to explain the whole solution path, it would go beyond the scope)

    if($('#ajaxScript').length!=0)
    {
      $('#ajaxScript').remove();
    }

    // Build temporary script tag to get AJAX results
    var scriptUrl             =  ajaxurl+'&useAsJsFunction=1';
    var script                = document.createElement( 'script' );
    script.type               = 'text/javascript';
    script.src                = scriptUrl;
    script.id                 = 'ajaxScript';

    if (script.addEventListener) // for normal browsers
    {
      script.addEventListener('load', function(){
        setAjaxData();
      }, false);
    }
    else // for old IEs
    {
      script.onreadystatechange = function(){
        if (script.readyState in {loaded: 1, complete: 1}) {
          script.onreadystatechange = null;
          setAjaxData();
        }
      };
    }

    document.body.appendChild(script);
    $(script).remove();

    function setAjaxData()
    {
      var ajaxData = getAjaxDataString();
      $('#resultsContainer').html(ajaxData);

      // Clean up the objects:
      $(ajaxData).remove();
    }

Explaination:

1. I enhanced the old ajaxurl with the parameter “&useAsJsFunction=1″. So the script behind the URL will build a javascript function body around the HTML data:

<?php if($useAsJsFunction): ?>
  function getAjaxDataString()
  {
    var data = '<?php echo str_replace(array("\r\n", "\r", "\n"), "", trim($content)); ?>';
    return data;
  }

<?php else: ?>

<?php echo $content; ?>

<?php endif; ?>

2. I set the ID attribute to the script tag for an easy later access to remove it from the DOM after I get the data. Because or otherwise every AJAX call will enhance a new script tag to the DOM.

3. The browser needs a while to load the foreign script containing the getAjaxDataString()-function which returns the ajax data. So I tried a lot of different ways with the setTimeout function etc. But I found the best solution to this async problem on phpied.com (thx a lot for your post). Instead of setting up an arbitary timeout it’s better to use the event handler when the script is loaded (onreadystatechange respectively onload).

4. getAjaxDataString(): I needed to remove the linebreaks etc. from the HTML string to avoid JS syntax errors. The HTML string inside of $content has no additional escaping stuff. But this would be different if you use JS code inside the returning HTML string!

5. document.body.appendChild(script) appends the script tag to the DOM. There are different ways to add it, but adding it to the body was the most readable way for later code reviews. First I used the head-tag as parent, but this caused problems with IE8 and older browsers.

6. $(script).remove() and $(ajaxdata).remove() are just to release the memory of these sometimes big objects, because they’re created more the once during a session.

 

Your’re welcome to post comments or improvements to my explanations.

 

Links:

Edit:

  • I don’t know why, but it’s funny that this solution seems to be faster than the jQuery XHR.
  • It does not work with Opera.

Symfony: Instruct browser to refresh CSS and JS files

In Symfony the stylesheets and javascript files are defined in the view.yml. Sometimes the problem appears that the browsers cache these files, even if there where some changes in the last release without refreshing the new contents.

There is a common technique to instruct the browser to take the new file version: Browsers store different file versions for different GET paramters. So if you include a CSS file like this:

<script type="text/javascript" src="css/styles.css?v=21"></script>

and in the next version with a higher version number (v=22), the browser will use  (and cache) the newer version.

In our case we save the current version number (SVN) into the app.yml and we use this number as increasing GET-Parameter to force a cache refresh:

default:
  stylesheets: [styles.css?v=<?php echo sfConfig::get('app_revision'); ?>]

symfony: How to build a dojo dijit autocompleter sfForm widget

I was searching long time for a good auto completer widget, but I couldn’t find one. So I created it by my self.
I did the same with the build in symfony auto completer widget. When I have time I’ll post it, too.
I hope I could help someboby with this small tutorial. Please give me feedback if there are any questions or suggestions.

Now lets start:

1. Copy the dojo package to the js folder
Resulting path to dojo.js: web/js/dojoToolkit/dojo/dojo.js

2. Enable dojo.js
If not yet happend elsewhere:
Modify the view.yml: apps/[YOUR_APP]/config/view.yml

default:
javascripts: [dojoToolkit/dojo/dojo.js]

3. Create the widget
lib/widget/itWidgetFormInputTextDijitAutocomplete.php

class itWidgetFormInputTextDijitAutocomplete extends sfWidgetForm
{
/**
* Constructor.
*
* Available options:
*
* * model: Name of the Model
* * searchfield: Table field in which will be searched
* * action: Optional: request action or url
* * value_method: Optional: getter for form field value
* * output_method: Optional: getter for form field name
* * criteria_method: Optional: getter for additional cirteria
* * add_empty: Optional: add emtpy value option to the list
* * choices: An array of possible choices (n/a)
* * formatter: A callable to call to format the checkbox choices
*
* @see sfWidgetForm
*/
protected function configure($options = array(), $attributes = array())
{
$this->addRequiredOption('model');
$this->addRequiredOption('search_field');
$this->addOption('action', 'default/ajaxAutocomplete');
$this->addOption('value_method', 'getPrimaryKey');
$this->addOption('output_method', 'getPrimaryKey');
$this->addOption('choices', array());
$this->addOption('criteria_method', '');
$this->addOption('formatter', array($this, 'formatter'));
$this->addOption('add_empty', false);
}

/**
* Renders the widget as HTML.
*
* @param string The name of the HTML widget
* @param mixed The value of the widget
* @param array An array of HTML attributes
* @param array An array of errors
*
* @return string A HTML representation of the widget
*
* @see sfWidgetForm
*/
public function render($name, $value = null, $attributes = array(), $errors = array())
{
$choices = $this->getOption('choices');
if ($choices instanceof sfCallable)
{
$choices = $choices->call();
}

$baseAttributes = array(
'action' => $this->getOption('action'),
'name' => $name,
'type' => 'text',
'value' => $value,
'id' => $id = $this->generateId($name, self::escapeOnce($name)),
);

return call_user_func($this->getOption('formatter'), $this, $baseAttributes);
}

/**
* Renders the input fields as HTML.
*
* @param widget
* @param attributes
*
* @return string A HTML representation of the input fields
*/
public function formatter($widget, $attributes)
{
sfLoader::loadHelpers(array('Form', 'Url'));
$request = sfContext::getInstance()->getRequest();
$field_id = $attributes['id'];
$field_name = $attributes['name'];
$ajax_field_id = 'ajax_'.$attributes['id'];
$out_field_id = 'out_'.$attributes['id'];
$object = @call_user_func(array($this->getOption('model').'Peer', 'retrieveByPk'), $attributes['value']);
$default_out = $request->getParameter('ajax_'.$attributes['name'], is_object($object) ? call_user_func(array(&$object, $this->getOption('output_method'))) : '');
$default_value = $request->getParameter($attributes['name'], is_object($object) ? $attributes['value'] : '');

$html = '';
$ajaxParams=array(
'model' => $this->getOption('model'),
'value_method' => $this->getOption('value_method'),
'output_method' => $this->getOption('output_method'),
'search_field' => $this->getOption('search_field'),
'criteria_method' => $this->getOption('criteria_method'),
'field_name' => 'ajax_'.$attributes['name']
);
if($this->getOption('add_empty')===false) $ajaxParams['add_empty'] = 'false';
elseif($this->getOption('add_empty')===true) $ajaxParams['add_empty'] = 'true';
else $ajaxParams['add_empty'] = $this->getOption('add_empty');

$requestUrl = url_for($attributes['action'].'?'.http_build_query($ajaxParams), true);
$requestUrl = str_replace('frontend_dev.php/', '', $requestUrl);
$requestUrl = str_replace('backend_dev.php/', 'backend.php/', $requestUrl);

$html .= <<
<script type="text/javascript">
dojo.require("dojo.data.ItemFileReadStore");
dojo.require("dijit.form.FilteringSelect");
</script>
<div dojoType="dojo.data.ItemFileReadStore"
jsId="{$field_id}Store" url="{$requestUrl}"></div>
EOF;

$input_text = <<
<input dojoType="dijit.form.FilteringSelect"
store="{$field_id}Store"
class="medium"
id="{$field_id}"
name="{$field_name}"
value="{$default_value}"
hasDownArrow="true",
invalidMessage="",
ignoreCase="true" />
}

4. Create action for the AJAX request
apps/[APP_NAME]/modules/default/actions/actions.class.php

<?php
class defaultActions extends sfActions
{
public function executeAjaxAutocomplete($request)
{
$model = $request->getParameter('model');
$criteria_method = $request->getParameter('criteria_method');
$add_empty = $request->getParameter('add_empty', 'false');
$this->value_method = $request->getParameter('value_method');
$this->output_method = $request->getParameter('output_method');

$peer_object = $model.'Peer';

if($criteria_method!='') $criteria = call_user_func(array($peer_object, $criteria_method));
else $criteria = new Criteria();

$search_field = $request->getParameter('search_field');
$field_name = $request->getParameter('field_name');
$var = $request->getParameter($field_name);

$j_object = array('identifier' => 'abbreviation', 'items'=>array());

$search_field = strtoupper($search_field);

$sql_field = constant($peer_object.'::'.$search_field);

$c = $criteria;
$c->add($sql_field, $var . '%', Criteria::LIKE);
$this->list = call_user_func(array($peer_object,'doSelect'), $c);

if($add_empty!='false')
{
$j_object['items'][] = array(
'name' => $add_empty=='true' ? '' : $add_empty,
'label' => '',
'abbreviation' => ''
);
}

foreach($this->list AS $v)
{
$j_object['items'][] = array(
'name' => call_user_func(array(&$v, $this->output_method)),
'label' => call_user_func(array(&$v, $this->output_method)),
'abbreviation' => call_user_func(array(&$v, $this->value_method))
);
}
$this->j_object = $j_object;
}
}

5. Create template for the AJAX request
apps/[APP_NAME]/modules/default/templates/ajaxAutocompleteSuccess.php

<?php decorate_with(false); ?>
<?php print_r(json_encode($j_object)); ?>

6. Insert the new widget into your sfForm
apps/[APP_NAME]/modules/default/templates/ajaxAutocompleteSuccess.php

...
# $widgets['user_id'] = new sfWidgetFormInput();
$widgets['user_id'] = new stWidgetFormInputTextDijitAutocomplete(array(
'model' => 'User',
'search_field' => 'username',
'value_method' => 'getId',
'output_method' => 'getUsername',
'criteria_method' => 'getCriteriaRegisteredUsers'
));

6. Clear symfony cache

Links:

  • http://api.dojotoolkit.org/jsdoc/1.3.2/dijit.form.FilteringSelect

JavaScript: unterminated string literal

Was hab ich mich damit rum geschlagen und kein einziges Suchergebnis bei g* konnte mir die Lösung bieten.
Kurz zum Problem. Ich lese mit PHP Text aus einer MySQL-Datenbank, welcher auch Zeilenumbrüche enthalten kann.
Diese Zeilenumbrüche werden mit

\n

oder

\r\n

codiert. Ich weiß, dass in JS Zeilenumbrüche in Strings verboten sind und wollte diese nun ersetzten. Dafür gibt es mich folgende Möglichkeiten:

nl2br brachte nichts und kann in der Ausführung auch nicht wirklich beeinflusst werden. Ok dann mit str_replace oder preg_replace.
Was ich versucht habe war nun folgendes:

$str = str_replace('\r\n','
',$str);
$str = str_replace('\r','
',$str);
$str = str_replace('\n','
',$str);

Nichts hat geholfen, der Fehler war noch da und der Umbruch war sogar noch im Quellcode zu sehen.
Dann fiehl mir Gott sei Dank ein, dass PHP einen Unterschied macht ob man SingleQuotes (‘) oder DoubleQuotes (“) benutzt. Code in SingleQuotes wird bspw. nicht nach PHP-Variablen geparst. Also versuchte ich es mal so:

$str = str_replace("\r\n","
",$str);

Und siehe da, die Sonderzeichen wurden wie gewünscht ersetzt und der JavaScript-Fehler unterminated string literal war verschwunden. Bitte. Danke.


Copyright © 2007-2012 iTopia. All rights reserved.
Jarrah theme by Templates Next | Powered by WordPress