Home » support » General discussion » Invalid XHTML - Solution
| Invalid XHTML - Solution [message #25753] |
Mon, 16 April 2007 12:30  |
ReeD Messages: 128 Registered: March 2007 |
Senior Member |
|
|
symfony documentation states that form tag helpers produce valid XHTML, but this is not always correct.
When using automatic fillin, when a form is redisplayed, all <input ... /> tags become <input ...> (the trailing / is removed).
Also radiobutton_tag() helper outputs multiple identical ids (if multiple radio buttons have the same name) which is plain wrong.
Are these bugs confirmed? I could probably do without the radiobutton_tag() helper, but I would hate to stop using other input tag helpers.
EDIT: Actually I noticed that not only input tags are missng the trailing / after form validation, but other tags as well (meta, link). Since fillin basically reparses the XHTML document using DOM, I'm wondering if this is the problem of DOM parsing or a symfony bug.
Btw, this is happening on Vista Ultimate x86, Apache 2.2.x, PHP 5.2.1.
[Updated on: Tue, 17 April 2007 11:30]
|
|
| |
| Re: Invalid XHTML [message #25806 is a reply to message #25780 ] |
Tue, 17 April 2007 02:10   |
vanchuck Messages: 14 Registered: November 2006 |
Junior Member |
|
|
I've been struggling with this for the last couple of days myself, and end up with the same problem...
Fillin breaks XHTML validation. Switching fillin content_type to XML gives a form not found error, even though the resulting page IS valid XHTML and there is only one form on the page (also tried giving it a name and id, and still no luck).
I've created a filter that runs Tidy on the page output (which will close the tags automatically), but I haven't tested it in production to see what it does to server load and page loading times. I also wrote my own caching mechanism which optionally only applies the Tidy to the page output as they're being cached, so it's only done once-- but generally speaking I don't cache forms, so the Tidy-Before-Cache mechanism doesn't solve this fillin problem.
If you ask me though, the real solution is to dump XHTML completely. Please read this for some background info: http://www.webdevout.net/articles/beware-of-xhtml
The key point is that XHTML documents are rendered as HTML anyway, with just some extra rules thrown in. The only way to get them to render as XML (which is the whole supposed purpose of XHTML) is to set the content type response header to application/xhtml+xml -- only then are they rendered as XML. And IE (even IE7) doesn't support that at all, it just asks you to download the page. Even in browsers that support application/xhtml+xml, if there is one illegal character or unclosed tag, the page won't show up. At all. Meaning this fillin issue would become fatal.
Basically, there is zero advantage to outputting XHTML over HTML 4.01 (with content-type = text/html). XHTML being more "proper" or "better" than HTML 4.01 STRICT in this context is a misguided fallacy.
My opinion is that there are 2 options for how SF handles this issue:
1) everything is output as HTML4.01 STRICT
2) the framework is expanded to provide content-negotiation to browsers which will output XHTML1.0 with content-type = application/xhtml+xml on supported browsers OR HTML4.01 as text/html on unsupported browsers (IE). Also allow a config var to say whether you want to enable XHTML output. And then fix this "form not found" error 
For now I'd like to see #1 be at least an option. These changes are discussed in this thread: http://www.symfony-project.com/forum/index.php/m/25605/ and are suggested in this ticket: http://trac.symfony-project.com/trac/ticket/1661
-vanchuck
|
|
|
| Re: Invalid XHTML [message #25819 is a reply to message #25753 ] |
Tue, 17 April 2007 10:09   |
ReeD Messages: 128 Registered: March 2007 |
Senior Member |
|
|
Looking at the source, it seems fine:
sfFillInForm.class.php
<?php public function fillInDom($dom, $formName, $formId, $values)
{
$xpath = new DomXPath($dom);
/* ... */
// find our form
if ($formName)
{
$xpath_query = '//form[@name="'.$formName.'"]';
}
elseif ($formId)
{
$xpath_query = '//form[@id="'.$formId.'"]';
}
else
{
$xpath_query = '//form';
}
$form = $xpath->query($xpath_query)->item(0);
if (!$form)
{
if (!$formName && !$formId)
{
throw new sfException('No form found in this page');
}
else
{
throw new sfException(sprintf('The form "%s" cannot be found', $formName ? $formName : $formId));
}
}
} ?>
I tried pasting the XHTML code from the browser to a separate file and tried running this:
<?php $dom = new DOMDocument('1.0', 'UTF-8');
$dom->load('stuff.txt'); //XHMTL is here
$xpath = new DOMXPath($dom);
$list = $xpath->query('//form');
$form = $list->item(0);
var_dump($form); ?>
I get NULL, that is the same result symfony gets. I wonder why? I validated my XHTML using an XML validator online and it's ok...
[Updated on: Tue, 17 April 2007 11:15]
|
|
|
| Re: Invalid XHTML [message #25822 is a reply to message #25753 ] |
Tue, 17 April 2007 10:35   |
ReeD Messages: 128 Registered: March 2007 |
Senior Member |
|
|
I think I found what is causing this behaviour. DOMXPath cannot select elements of an XML document with default namespace defined. That's why the form element is never found.
Solution 1:
What you need to do is prefix the namespace, for example:
<html xmlns:xhtml="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
After you do this, DOMXPath will find your form correctly, but XHTML will not be completely valid. You will get undefined attribute (xmlns:xhtml) error.
Solution 2:
Leave the default namespace in the document and register a new one using DOMXPath::registerNamespace() and then prefix each element with 'xhtml:'. For example:
<?php public function fillInDom($dom, $formName, $formId, $values)
{
$xpath = new DomXPath($dom);
$xpath->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
/* ... */
// find our form
if ($formName)
{
$xpath_query = '//xhtml:form[@name="'.$formName.'"]';
}
elseif ($formId)
{
$xpath_query = '//xhtml:form[@id="'.$formId.'"]';
}
else
{
$xpath_query = '//xhtml:form';
}
$form = $xpath->query($xpath_query)->item(0);
if (!$form)
{
if (!$formName && !$formId)
{
throw new sfException('No form found in this page');
}
else
{
throw new sfException(sprintf('The form "%s" cannot be found', $formName ? $formName : $formId));
}
}
/* ... */
} ?>
This is quite a dirty fix, but your XHTML will be valid.
[Updated on: Tue, 17 April 2007 11:15]
|
|
|
| Re: Invalid XHTML [message #25882 is a reply to message #25822 ] |
Tue, 17 April 2007 20:07   |
vanchuck Messages: 14 Registered: November 2006 |
Junior Member |
|
|
Good detective work, ReeD, cheers...
I just spent about an hour testing various combinations of solutions. I couldn't get your fillInDom modifications to work for me for some reason.
If you supply the custom xmlns:xhtml namespace, the page is indeed invalid XHTML, and if the page is served as application/xhtml+xml (instead of text/html) the browser can't sort it out and gives the unstyled X(HT)ML data.
I did find, however, that simply leaving the XML namespace out of the layout completely also allows the parser to properly find the form and fill everything in. This works fine when the page is served as text/html, and in fact it validates fine as XHTML without explicitly defining the namespace. On pages where the fillInFormFilter has processed, you'll find the source of the output has an <?xml version... ?> and <html xmlns:... > tags (added by the parser) but of course pages that have not been parsed by the fillInFormFilter don't have this...
The only problem with this is that of course pages without the xmlns: aren't valid xml and only work because the browser is rendering the page using its HTML parser (since it's being sent as text/html). So if you serve the page as application/xhtml+xml, any pages without the xmlns: bit show up as unstyled XML.
So it's still just a hack.
... all this trouble just for some self-closing tags!
|
|
|
| Another solution [message #25888 is a reply to message #25882 ] |
Tue, 17 April 2007 22:30   |
vanchuck Messages: 14 Registered: November 2006 |
Junior Member |
|
|
Well, I still couldn't get the DOMXPath alterations to work properly-- it would find the form element okay, but the fillin wouldn't work after that.
In the meantime I came up with another solution, one that outputs valid XHTML and has working fillin, with no core modifications.
It's hackish but it works... just start your template like this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<?php $attributes = $sf_context->getRequest()->getAttribute('fillin', null, 'symfony/filter'); ?>
<?php if (isset($attributes['enabled']) && $attributes['enabled']): ?>
<html>
<?php else: ?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<?php endif; ?>
As explained in my previous post, the XML-based fillin form works fine if you leave out the namespace declaration. When the fillin filter is not enabled for the current request, the proper namespace is output, keeping things proper.
|
|
|
| Re: Invalid XHTML [message #25889 is a reply to message #25882 ] |
Tue, 17 April 2007 22:42   |
ReeD Messages: 128 Registered: March 2007 |
Senior Member |
|
|
| vanchuck wrote on Tue, 17 April 2007 20:07 | I just spent about an hour testing various combinations of solutions. I couldn't get your fillInDom modifications to work for me for some reason.
|
I'm guessing this is because you did not prefix each elemnt with xhtml: in the indicated method. There are more elements to prefix besides 'form', I simply didn't indicate all of them. I think pasting the whole method could have been a better idea:
public function fillInDom($dom, $formName, $formId, $values)
{
$xpath = new DomXPath($dom);
$xpath->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
$query = 'descendant::xhtml:input[@name and (not(@type)';
foreach ($this->types as $type)
{
$query .= ' or @type="'.$type.'"';
}
$query .= ')] | descendant::xhtml:textarea[@name] | descendant::xhtml:select[@name]';
// find our form
if ($formName)
{
$xpath_query = '//xhtml:form[@name="'.$formName.'"]';
}
elseif ($formId)
{
$xpath_query = '//xhtml:form[@id="'.$formId.'"]';
}
else
{
$xpath_query = '//xhtml:form';
}
$form = $xpath->query($xpath_query)->item(0);
if (!$form)
{
if (!$formName && !$formId)
{
throw new sfException('No form found in this page');
}
else
{
throw new sfException(sprintf('The form "%s" cannot be found', $formName ? $formName : $formId));
}
}
foreach ($xpath->query($query, $form) as $element)
{
$name = (string) $element->getAttribute('name');
$value = (string) $element->getAttribute('value');
$type = (string) $element->getAttribute('type');
// skip fields
if (!$this->hasValue($values, $name) || in_array($name, $this->skipFields))
{
continue;
}
if ($element->nodeName == 'input')
{
if ($type == 'checkbox' || $type == 'radio')
{
// checkbox and radio
$element->removeAttribute('checked');
if ($this->hasValue($values, $name) && ($this->getValue($values, $name) == $value || !$element->hasAttribute('value')))
{
$element->setAttribute('checked', 'checked');
}
}
else
{
// text input
$element->removeAttribute('value');
if ($this->hasValue($values, $name))
{
$element->setAttribute('value', $this->escapeValue($this->getValue($values, $name), $name));
}
}
}
else if ($element->nodeName == 'textarea')
{
$el = $element->cloneNode(false);
$el->appendChild($dom->createTextNode($this->escapeValue($this->getValue($values, $name), $name)));
$element->parentNode->replaceChild($el, $element);
}
else if ($element->nodeName == 'select')
{
// select
$value = $this->getValue($values, $name);
$multiple = $element->hasAttribute('multiple');
foreach ($xpath->query('descendant::xhtml:option', $element) as $option)
{
$option->removeAttribute('selected');
if ($multiple && is_array($value))
{
if (in_array($option->getAttribute('value'), $value))
{
$option->setAttribute('selected', 'selected');
}
}
else if ($value == $option->getAttribute('value'))
{
$option->setAttribute('selected', 'selected');
}
}
}
}
return $dom;
}
| vanchuck wrote on Tue, 17 April 2007 20:07 | If you supply the custom xmlns:xhtml namespace, the page is indeed invalid XHTML, and if the page is served as application/xhtml+xml (instead of text/html) the browser can't sort it out and gives the unstyled X(HT)ML data.
|
Don't serve the page as application/xhtml+xml. I think W3 indicates that XHTML 1.0 can be served as text/html when required. Also, there's no need to add xmlns:xhtml to the document if you're after XML validation, just leave the default xmlns.
| vanchuck wrote on Tue, 17 April 2007 20:07 | I did find, however, that simply leaving the XML namespace out of the layout completely also allows the parser to properly find the form and fill everything in.
|
Yes, that's right. But W3 insists xmlns being there. 
| vanchuck wrote on Tue, 17 April 2007 20:07 | On pages where the fillInFormFilter has processed, you'll find the source of the output has an <?xml version... ?> and <html xmlns:... > tags (added by the parser) but of course pages that have not been parsed by the fillInFormFilter don't have this...
|
I guess there's nothing wrong with having <?xml version... ?>. Also, xmlns:something is not added for me, that is, the namespace stays default (xmlns, without any prefix).
So for it's working just fine for me. The document is valid before/after validation and the forms are filled in correctly. After all, that's what I am after.
|
|
|
| Re: Another solution [message #25890 is a reply to message #25888 ] |
Tue, 17 April 2007 22:46   |
ReeD Messages: 128 Registered: March 2007 |
Senior Member |
|
|
| vanchuck wrote on Tue, 17 April 2007 22:30 | Well, I still couldn't get the DOMXPath alterations to work properly-- it would find the form element okay, but the fillin wouldn't work after that.
|
That's exactly what I thought could have happened to you, see my post above.
| vanchuck wrote on Tue, 17 April 2007 22:30 | As explained in my previous post, the XML-based fillin form works fine if you leave out the namespace declaration.
|
Yes, but W3 recommends having xmlns defined.
The code fix is very simple. I guess further versions of symfony should include it (unless there's something really wrong with this fix or it breaks something else which is unlikely).
[Updated on: Tue, 17 April 2007 22:51]
|
|
|
| Re: Another solution [message #25895 is a reply to message #25890 ] |
Wed, 18 April 2007 04:39   |
vanchuck Messages: 14 Registered: November 2006 |
Junior Member |
|
|
| ReeD wrote on Wed, 18 April 2007 04:46 |
| vanchuck wrote on Tue, 17 April 2007 22:30 | Well, I still couldn't get the DOMXPath alterations to work properly-- it would find the form element okay, but the fillin wouldn't work after that.
|
That's exactly what I thought could have happened to you, see my post above.
|
Ah, thanks for pasting the full deal-- of course it makes sense now that you have to also change the namespace of the items it is replacing!
| ReeD wrote on Wed, 18 April 2007 04:46 |
| vanchuck wrote on Tue, 17 April 2007 22:30 | As explained in my previous post, the XML-based fillin form works fine if you leave out the namespace declaration.
|
Yes, but W3 recommends having xmlns defined.
|
Sorry, what I meant is that you can leave the <html> tag with no xmlns-- in the layout file. Then when the DOM parser of the fillInFormFilter outputs the result and then gets sent on to the browser, it actually outputs it with the xmlns tag there (along with the <? xml ... ?> tag) so the end result is still valid.
Of course, when the fillInFilter isn't run, THEN the result would be invalid XHTML. My solution as posted above was just to have some code which ouput the xmlns bit if the fillInFilter wasn't being run during the current request.
I've since refined the solution to incorporate it into a filter. The filter will output the document as application/xhtml+xml if the browser supports it (people claim page rendering times are increased) or html/text if not. The filter also strips out the xmlns from the response (prior to fillInFormFilter running) if the fillInFormFilter is being executed, which lets the DOM parser work without modification and doesn't require any changes to the layout templates.
| ReeD wrote on Wed, 18 April 2007 04:46 | The code fix is very simple. I guess further versions of symfony should include it (unless there's something really wrong with this fix or it breaks something else which is unlikely).
|
Your fix looks simple and easy... I'm just trying not to make core changes now to make sure I deployment doesn't become a huge pain! If you want I can submit a patch and see what Fabien et al think about it.
Since it's only used for form fillin, I can't see too many conflicts. The only possibility is if someone is outputting using XML+XSLT instead of XHTML. In that case, would forcing this XHTML-based namespace mess things up? I think it's "just a name", but I don't know enough about it to say for sure...
|
|
| | | | | | | | |
| Re: Invalid XHTML - Solution [message #33004 is a reply to message #32046 ] |
Tue, 07 August 2007 16:11   |
cristianbaciu Messages: 38 Registered: June 2007 Location: Cluj Napoca, Romania |
Member |
|
|
Hi guys. Until a valid solution for XHTML and AJAX is made I've made this helper:
<?php
function fillIn($value, $type, $initValue= NULL, $checked=false){
if($type=='text' || $type == 'textarea' || $type == 't')
{
// text input
if (is_null($value))// && $value == $initValue
{
return $initValue;
}
else {
return $value;
}
}
else if ($type == 'checkbox' || $type == 'radio' || $type == 'c' || $type == 'r' )
{
// checkbox and radio
if (!is_null($value) && !is_null($initValue) && ($value == $initValue))
{
return true;
}
else if(is_null($value) && empty($_POST) && empty($_GET) && ($checked == true))
{
return true;
}
else
{
return false;
}
}
else if ($type == 'select' || $type == 's')
{
// select
if(is_null($value))
{
return (array) $initValue;
}
else
{
if(is_array($value) || is_object($value))
{
$selected_options = array();
foreach ($value as $val)
{
array_push($selected_options, urldecode(urldecode($val)));//urldecode - prototype bug
}
return $selected_options;
}
else
{
return (array) $value;
}
}
}
return false;
}
?>
and in the view you must write something like this
input_tag('name', fillIn($sf_request->getParameter('name'),'text','init value'), array (
'class' => 'input-text',
))
radiobutton_tag ('name', 'value1', fillIn($sf_request->getParameter('administrare'),'r','value1',true), array(
'class' => 'name','id' => 'name_value1',));
radiobutton_tag ('name', 'value2', fillIn($sf_request->getParameter('name'),'r','value2'), array(
'class' => 'name','id' => 'name_value2',));
checkbox_tag('name', 'value', fillIn($sf_request->getParameter('name'),'c','value', true))
select_tag('name',
options_for_select( array('0'=>'--------------------','Google' => 'Google', 'Yahoo' => 'Yahoo', 'Live.com' => 'Live.com',), fillIn($sf_request->getParameter('name'),'select',array('Google')),'')
,array('multiple'=>true)
)
UPDATE:
function has a bug
[Updated on: Wed, 08 August 2007 00:15]
|
|
| |
| Re: Invalid XHTML - Solution [message #35146 is a reply to message #33010 ] |
Fri, 07 September 2007 14:59   |
cristianbaciu Messages: 38 Registered: June 2007 Location: Cluj Napoca, Romania |
Member |
|
|
In symfony 1.0.7 you can use this filter as a patch for fillin and Ajax.
<?php
class myfillinFilter extends sfFilter
{
public function execute ($filterChain)
{
// execute next filter
$filterChain->execute();
$context = $this->getContext();
$response = $context->getResponse();
$request = $context->getRequest();
//content
$xml = $response->getContent();
if($request->isXmlHttpRequest()){
$parameters = $request->getAttribute('fillin', null, 'symfony/filter');
$parameters = @$parameters['param'];
$this->parameterHolder->add($parameters);
$flag = false; //did we change $xml
$is_xml = substr($xml,0,5);
//add xml and doctype
if($is_xml != '<?xml' && (strtolower($this->getParameter('content_type')) == 'xml')){
$flag = true;
$xml_pre_1 = '<?xml version="1.0" encoding="'.sfConfig::get('sf_charset', 'UTF-8').'"?>';
$xml_pre_2 = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
$xml_pre_3 = '<sf_additional_tag>';
$xml_pre = $xml_pre_1.$xml_pre_2.$xml_pre_3;
$xml_post = '</sf_additional_tag>';
$xml = $xml_pre.$xml.$xml_post;
$fillInForm = new sfFillInForm();
// converters
foreach ($this->getParameter('converters', array()) as $functionName => $fields)
{
$fillInForm->addConverter($functionName, $fields);
}
// skip fields
$fillInForm->setSkipFields((array) $this->getParameter('skip_fields', array()));
// types
$excludeTypes = (array) $this->getParameter('exclude_types', array('hidden', 'password'));
$checkTypes = (array) $this->getParameter('check_types', array('text', 'checkbox', 'radio', 'password', 'hidden'));
$fillInForm->setTypes(array_diff($checkTypes, $excludeTypes));
// fill in
$method = 'fillIn'.ucfirst(strtolower($this->getParameter('content_type', 'Html')));
try
{
$xml = $fillInForm->$method($xml, $this->getParameter('name'), $this->getParameter('id'), $request->getParameterHolder()->getAll());
if($flag){
$xml = str_ireplace($xml_pre_1,'',$xml);
$xml = str_ireplace($xml_pre_2,'',$xml);
$xml = str_ireplace($xml_pre_3,'',$xml);
$xml = str_ireplace($xml_post,'',$xml);
}
$response->setContent($xml);
}
catch (sfException $e)
{
if (sfConfig::get('sf_logging_enabled'))
{
$this->getContext()->getLogger()->err(sprintf('{myfillinFilter} %s', $e->getMessage()));
}
}
}
}
}
}
?>
[Updated on: Fri, 07 September 2007 15:37]
|
|
| | | |
Goto Forum:
|