Commit ee6f79d4 authored by Oliver Bock's avatar Oliver Bock

Merge branch 'drupal-generic' into drupal-eah-albert

parents 23cad04e a6913a75
......@@ -42,24 +42,6 @@ function boincwork_admin_prefs_upload_form_validate($form, &$form_state) {
': <br/>' . htmlentities($lines[$errors[0]->line - 1]), 'error');
form_set_error('upload', t('XML file failed validation'));
}
// Convert XML to array for validation
$xml = load_configuration($form_state['values']['prefs_xml']);
// Validate against schema (as implemented here in PHP...)
if (!isset($xml['project_specific_preferences'])) {
drupal_set_message(t('prefs_xml::document is malformed',
array('@xml_path' => $xml_path)), 'warning');
form_set_error('upload', t('XML file failed validation'));
}
elseif (!is_array($xml['project_specific_preferences'])) {
drupal_set_message(t('prefs_xml::document is empty',
array('@xml_path' => $xml_path)), 'warning');
form_set_error('upload', t('XML file failed validation'));
}
elseif (!boincwork_validate_prefs($xml['project_specific_preferences'], 'root')) {
form_set_error('upload', t('XML file failed validation'));
}
}
/**
......
......@@ -201,20 +201,7 @@ function boincwork_get_project_specific_config() {
// Convert XML to array for validation
$xml = load_configuration($raw_config_data);
// Validate against schema (as implemented here in PHP...)
if (!isset($xml['project_specific_preferences'])) {
drupal_set_message(t('prefs_xml::document is malformed',
array('@xml_path' => $xml_path)), 'warning');
}
elseif (!is_array($xml['project_specific_preferences'])) {
drupal_set_message(t('prefs_xml::document is empty',
array('@xml_path' => $xml_path)), 'warning');
}
elseif (boincwork_validate_prefs($xml['project_specific_preferences'], 'root')) {
return $xml;
}
return NULL;
return $xml;
}
/**
......@@ -316,11 +303,17 @@ function boincwork_format_project_specific_prefs_data($values, $xml = array()) {
case 'compound':
foreach ($elements as $element) {
$name = $element['@attributes']['name'];
$default[$name] = boincwork_format_project_specific_prefs_data($values, $element);
$default[$name]['@attributes'] = boincwork_format_project_specific_prefs_data($values, $element['attributes']);
}
$defaults += $default;
break;
case 'group':
foreach ($elements as $element) {
$defaults += boincwork_format_project_specific_prefs_data($values, $element);
}
break;
case 'text':
case 'radio':
case 'dropdown':
......@@ -354,368 +347,6 @@ function boincwork_format_project_specific_prefs_data($values, $xml = array()) {
return $defaults;
}
/**
* Validate project specific preferences XML.
* This implements the project specific preferences schema
*/
function boincwork_validate_prefs($config, $type = null, $xml_path = '') {
if ($type AND $xml_path) {
$xml_path .= "::{$type}";
}
switch ($type) {
case 'entitytype':
if (!is_string($config)) {
drupal_set_message(t('@xml_path entitytype must be a string',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
$datatypes = array(
'element',
'attribute'
);
if (!in_array($config, $datatypes)) {
drupal_set_message(t('@xml_path invalid entitytype',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
break;
case 'lang':
if (!is_string($config)) {
drupal_set_message(t('@xml_path language must be a string',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
$available_languages = array(
'en_US',
'de_DE'
);
if (!in_array($config, $available_languages)) {
drupal_set_message(t('@xml_path invalid language',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
break;
case 'datatype':
if (!is_string($config)) {
drupal_set_message(t('@xml_path datatype must be a string',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
$datatypes = array(
'text',
'integer',
'float'
);
if (!in_array($config, $datatypes)) {
drupal_set_message(t('@xml_path invalid datatype',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
break;
case 'title':
case 'description':
if (is_numeric(key($config))) {
drupal_set_message(t('@xml_path element has multiple @types',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
if (isset($config['@attributes'])) {
if (isset($config['@attributes']['lang']) AND
!boincwork_validate_prefs($config['@attributes']['lang'], 'lang',
$xml_path)) {
return false;
}
if (!is_string($config['@value'])) {
drupal_set_message(t('@xml_path @type should be a string',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
}
else {
if (!is_string($config)) {
drupal_set_message(t('@xml_path @type should be a string',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
}
break;
case 'item':
if (isset($config['@attributes'])) {
if (!is_string($config['@value'])) {
drupal_set_message(t('@xml_path value of @type should be a string',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
}
else {
if (!is_string($config)) {
drupal_set_message(t('@xml_path value of @type should be a string',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
}
break;
case 'app':
// Each app needs an ID
if (!isset($config['@attributes']) OR
!isset($config['@attributes']['id'])) {
drupal_set_message(t('@xml_path @type element must have an ID',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
if (!is_numeric($config['@attributes']['id']) OR
$config['@attributes']['id'] < 1) {
drupal_set_message(t('@xml_path @type ID must a positive integer',
array('@xml_path' => $xml_path, '@type' => $type)), 'warning');
return false;
}
break;
case 'items':
if (!isset($config['item'])) {
drupal_set_message(t('@xml_path element must contain an item',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (!is_numeric(key($config['item']))) {
$config['item'] = array($config['item']);
}
// Ensure only one item is selected
$items_selected = 0;
foreach ($config['item'] as $item) {
if (isset($item['@attributes']['selected']) AND
$item['@attributes']['selected'] = TRUE) {
$items_selected++;
}
}
if ($items_selected != 1) {
drupal_set_message(t('@xml_path exactly one item must be selected',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
break;
case 'component':
if (!isset($config['title'])) {
drupal_set_message(t('@xml_path title not set',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (!boincwork_validate_prefs($config['title'], 'title', $xml_path)) {
return false;
}
if (isset($config['description']) AND
!boincwork_validate_prefs($config['description'], 'description',
$xml_path)) {
return false;
}
if (!isset($config['@attributes']) OR
!isset($config['@attributes']['name'])) {
drupal_set_message(t('@xml_path no name is set',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (!is_string($config['@attributes']['name'])) {
drupal_set_message(t('@xml_path name must be a string',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
break;
case 'text':
if (!is_numeric(key($config))) {
$config = array($config);
}
foreach ($config as $text) {
// Check standard component validations
if (!boincwork_validate_prefs($text, 'component', $xml_path)) {
return false;
}
// Then check text specific attributes
if (!isset($text['@attributes']['datatype']) OR
!boincwork_validate_prefs($text['@attributes']['datatype'],
'datatype', $xml_path)) {
return false;
}
if (!isset($text['@attributes']['default']) OR
!is_string($text['@attributes']['default'])) {
drupal_set_message(t('@xml_path default value must be a string',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
// Optional attributes
if (isset($text['@attributes']['min']) AND
!is_numeric($text['@attributes']['min'])) {
drupal_set_message(t('@xml_path min must be an integer',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (isset($text['@attributes']['max']) AND
!is_numeric($text['@attributes']['max'])) {
drupal_set_message(t('@xml_path max must be an integer',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
}
break;
case 'boolean':
if (!is_numeric(key($config))) {
$config = array($config);
}
foreach ($config as $boolean) {
// Check standard component validation
if (!boincwork_validate_prefs($boolean, 'component', $xml_path)) {
return false;
}
}
break;
case 'radio':
if (!is_numeric(key($config))) {
$config = array($config);
}
foreach ($config as $radio) {
// Check standard component validation
if (!boincwork_validate_prefs($radio, 'component', $xml_path)) {
return false;
}
if (!isset($radio['items']) OR is_numeric(key($radio['items']))) {
drupal_set_message(t('@xml_path must have exactly one items list',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (!boincwork_validate_prefs($radio['items'], 'items', $xml_path)) {
return false;
}
}
break;
case 'dropdown':
if (!is_numeric(key($config))) {
$config = array($config);
}
foreach ($config as $dropdown) {
// Check standard component validation
if (!boincwork_validate_prefs($dropdown, 'component', $xml_path)) {
return false;
}
if (!isset($dropdown['items']) OR is_numeric(key($dropdown['items']))) {
drupal_set_message(t('@xml_path must have exactly one items list',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
if (!boincwork_validate_prefs($dropdown['items'], 'items', $xml_path)) {
return false;
}
}
break;
case 'apps':
// The apps element must have a title
if (!isset($config['title'])) {
drupal_set_message(t('@xml_path title is required',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
elseif (!boincwork_validate_prefs($config['title'], 'title')) {
return false;
}
// ...and at least one app
if (!isset($config['app'])) {
drupal_set_message(t('@xml_path must have at least one app',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
elseif (!is_numeric(key($config['app']))) {
$config['app'] = array($config['app']);
}
foreach ($config['app'] as $app) {
if (!boincwork_validate_prefs($app, 'app')) {
return false;
}
}
break;
case 'compound':
if (!is_numeric(key($config))) {
$config = array($config);
}
foreach ($config as $compound) {
// Each compound element must have a name...
if (!isset($compound['@attributes']) OR
!isset($compound['@attributes']['name']) OR
!is_string($compound['@attributes']['name']) OR
$compound['@attributes']['name'] == '') {
drupal_set_message(t('@xml_path has no name',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
// ...and a title...
if (!isset($compound['title'])) {
drupal_set_message(t('@xml_path @name element has no title',
array('@xml_path' => $xml_path, '@name' => $compound['@attributes']['name'])), 'warning');
return false;
}
elseif (is_numeric(key($compound['title']))) {
drupal_set_message(t('@xml_path @name element has multiple titles',
array('@xml_path' => $xml_path, '@name' => $compound['@attributes']['name'])), 'warning');
return false;
}
elseif (!boincwork_validate_prefs($compound['title'], 'title', $xml_path)) {
return false;
}
// ...and at least one of these elements as a child
if (!isset($compound['text']) AND
!isset($compound['boolean']) AND
!isset($compound['radio']) AND
!isset($compound['dropdown'])) {
drupal_set_message(t('@xml_path @name element is incomplete',
array('@xml_path' => $xml_path, '@name' => $compound['@attributes']['name'])), 'warning');
return false;
}
// Now recursively validate...
foreach ($compound as $type => $element) {
if (!boincwork_validate_prefs($element, $type, $xml_path)) {
return false;
}
}
}
break;
case 'root':
// Root must have at least one of these elements
if (!isset($config['text']) AND
!isset($config['boolean']) AND
!isset($config['radio']) AND
!isset($config['dropdown']) AND
!isset($config['apps']) AND
!isset($config['compound'])) {
drupal_set_message(t('prefs_xml::document content is required',
array('@xml_path' => $xml_path)), 'warning');
return false;
}
// Recursively validate the document...
foreach ($config as $type => $element) {
if (!boincwork_validate_prefs($element, $type, 'prefs_xml')) {
return false;
}
}
break;
default:
// Don't validate that which doesn't explicitly require validation, for
// recursion (i.e. '@attributes' keys and the like, which are handled
// by parent validation)
}
return true;
}
/**
* Add an element to the form based on its definition
*/
......@@ -815,6 +446,7 @@ function boincwork_generate_prefs_element(&$form, $type, $elements, $user_prefs
case 'radio':
case 'dropdown':
if (!is_numeric(key($elements))) {
$elements = array($elements);
}
......@@ -841,6 +473,7 @@ function boincwork_generate_prefs_element(&$form, $type, $elements, $user_prefs
if (isset($element['description'])) {
$description = is_array($element['description']) ? $element['description']['@value'] : $element['description'];
}
$user_pref = $user_prefs;
$entitytype = isset($element['@attributes']['entitytype']) ? $element['@attributes']['entitytype'] : 'element';
if ($entitytype == 'attribute') {
$user_pref = $user_prefs['@attributes'];
......@@ -904,6 +537,28 @@ function boincwork_generate_prefs_element(&$form, $type, $elements, $user_prefs
}
break;
case 'group':
if (!is_numeric(key($elements))) {
$elements = array($elements);
}
foreach ($elements as $key => $element) {
$title = is_array($element['title']) ? $element['title']['@value'] : $element['title'];
$name = str_replace(' ','_',strtolower($title));
$name = "group_{$name}";
$form[$name] = array(
'#title' => t($title),
'#type' => 'fieldset',
//'#description' => t('Notes about this group of fields'),
'#collapsible' => TRUE,
'#collapsed' => FALSE
);
// Recursively populate the compound element
foreach ($element as $child_type => $child) {
boincwork_generate_prefs_element($form[$name], $child_type, $child, $user_prefs);
}
}
break;
case 'compound':
if (!is_numeric(key($elements))) {
$elements = array($elements);
......@@ -919,8 +574,8 @@ function boincwork_generate_prefs_element(&$form, $type, $elements, $user_prefs
'#collapsed' => FALSE
);
// Recursively populate the compound element
foreach ($element as $child_type => $child) {
boincwork_generate_prefs_element($form[$name], $child_type, $child, $user_prefs[$name]);
foreach ($element['attributes'] as $child_type => $child) {
boincwork_generate_prefs_element($form[$name], $child_type, $child, $user_prefs[$name]['@attributes']);
}
}
break;
......
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://boinc.berkeley.edu"
xmlns="http://boinc.berkeley.edu"
targetNamespace="http://boinc.berkeley.edu/drupal-psp.xsd"
xmlns="http://boinc.berkeley.edu/drupal-psp.xsd"
xmlns:psp="http://boinc.berkeley.edu/drupal-psp.xsd"
elementFormDefault="qualified"
version="0.1">
version="0.3">
<!-- simple types -->
......@@ -70,12 +71,36 @@
<!-- collection types -->
<xs:complexType name="items">
<xs:sequence>
<xs:element name="item" type="item" minOccurs="1" maxOccurs="unbounded" />
<!-- TODO ensure only one item's "selected" attribute is set to "true" -->
</xs:sequence>
</xs:complexType>
<xs:element name="items">
<xs:complexType>
<xs:sequence>
<xs:element name="item" type="item" minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:unique name="unique-items-list-entry">
<xs:selector xpath="psp:item"/>
<xs:field xpath="."/>
</xs:unique>
<xs:unique name="only-one-item-can-be-selected">
<xs:selector xpath="psp:item"/>
<xs:field xpath="@selected"/>
</xs:unique>
</xs:element>
<xs:element name="attributes">
<xs:complexType>
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="text" type="text" />
<xs:element name="boolean" type="boolean" />
<xs:element name="radio" type="radio" />
<xs:element name="dropdown" type="dropdown" />
</xs:choice>
</xs:complexType>
<xs:unique name="unique-preference-attributes-per-tag">
<xs:selector xpath=".//*"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
<!-- complex base types -->
......@@ -84,7 +109,6 @@
<xs:element name="title" type="title" minOccurs="1" maxOccurs="1" />
<xs:element name="description" type="description" minOccurs="0" maxOccurs="1" />
</xs:sequence>
<xs:attribute name="entitytype" type="entitytype" default="element" />
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
......@@ -113,7 +137,7 @@
<xs:complexContent>
<xs:extension base="component">
<xs:sequence>
<xs:element name="items" type="items" minOccurs="1" maxOccurs="1" />
<xs:element ref="items" minOccurs="1" maxOccurs="1" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
......@@ -123,22 +147,42 @@
<xs:complexContent>
<xs:extension base="component">
<xs:sequence>
<xs:element name="items" type="items" minOccurs="1" maxOccurs="1" />
<xs:element ref="items" minOccurs="1" maxOccurs="1" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="apps">
<xs:sequence>
<xs:element name="title" type="title" minOccurs="1" maxOccurs="1" />
<xs:element name="app" type="app" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
<xs:complexType name="compound">
<xs:complexContent>
<xs:extension base="component">
<xs:sequence>
<xs:element ref="attributes" minOccurs="1" maxOccurs="1" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<!-- compound element type -->
<!-- grouping types -->
<xs:complexType name="compound">
<xs:element name="apps">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="title" minOccurs="1" maxOccurs="1" />
<xs:element name="app" type="app" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:unique name="unique-app-ids">