http://www.mediawiki.org/wiki/Special:Code/MediaWiki/92633
Revision: 92633 Author: salvatoreingala Date: 2011-07-20 14:47:04 +0000 (Wed, 20 Jul 2011) Log Message: ----------- - Refactored GadgetPrefs to have a more structured and more flexible language for describing preference specifications. - Decoupled field-description from preference-descriptions, so that it is possible to design fields that doesn't encode for preferences or fields that possibly encode for more than one preference (e.g.: containers). Refactored jquery.formBuilder for the same purpose. - Changed the return values of the 'getgadgetprefs' api, separating "description" and "values" in two separate objects, for consistency. Modified Paths: -------------- branches/salvatoreingala/Gadgets/api/ApiGetGadgetPrefs.php branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js Modified: branches/salvatoreingala/Gadgets/api/ApiGetGadgetPrefs.php =================================================================== --- branches/salvatoreingala/Gadgets/api/ApiGetGadgetPrefs.php 2011-07-20 12:59:11 UTC (rev 92632) +++ branches/salvatoreingala/Gadgets/api/ApiGetGadgetPrefs.php 2011-07-20 14:47:04 UTC (rev 92633) @@ -51,14 +51,9 @@ if ( $userPrefs === null ) { throw new MWException( __METHOD__ . ': $userPrefs should not be null.' ); } - - //Add user preferences to preference description - foreach ( $prefsDescription['fields'] as $prefIdx => $prefDescription ) { - $prefName = $prefDescription['name']; - $prefsDescription['fields'][$prefIdx]['value'] = $userPrefs[$prefName]; - } - - $this->getResult()->addValue( null, $this->getModuleName(), $prefsDescription ); + + $this->getResult()->addValue( null, 'description', $prefsDescription ); + $this->getResult()->addValue( null, 'values', $userPrefs ); } public function getAllowedParams() { Modified: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php =================================================================== --- branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php 2011-07-20 12:59:11 UTC (rev 92632) +++ branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php 2011-07-20 14:47:04 UTC (rev 92633) @@ -10,145 +10,200 @@ class GadgetPrefs { - //Syntax specifications of preference description language. - //Each element describes a type, and it has a 'description' and may have a 'checker'. - // - 'description' is an array that describes the fields of that option. - // - 'checker' is an optional function that does validation of the entire preference description, - // when more complex semantics are needed. + /* + * Syntax specifications of preference description language. + * Each element describes a field; a "simple" field encodes exactly one gadget preference, but some fields + * may encode for 0 or multiple gadget preferences. + * Each field has a 'description' and may have a 'validator', a 'flattener', and a 'checker'. + * - 'description' is an array that describes all the members of that fields. Each member description has this shape: + * - 'isMandatory' is a boolean that specifies if that member is mandatory for the field; + * - 'validator', if specified, is the name of a function that validates that member + * - 'validator' is an optional function that does validation of the entire field description, + * when member validators does not suffice since more complex semantics are needed. + * - 'flattener' is an optional function that takes a valid field description and returns an array of specification of + * gadget preferences, with preference names as keys and corresponding "simple" field descriptions as values. + * If omitted (for "simple fields"), the default flattener is used. + * - 'checker', only for "simple" fields, is the name of a function that takes a preference description and + * a preference value, and returns true if that value passes validation, false otherwise. + * - 'getMessages', if specified, is the name of a function that takes a valid description of a field and returns + * a list of messages referred to by it. If omitted, only the "label" field is returned (if it is a message). + */ private static $prefsDescriptionSpecifications = array( 'boolean' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true, - 'checker' => 'is_bool' + 'validator' => 'is_bool' ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ) - ) + ), + 'checker' => 'GadgetPrefs::checkBooleanPref' ), 'string' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ), 'required' => array( 'isMandatory' => false, - 'checker' => 'is_bool' + 'validator' => 'is_bool' ), 'minlength' => array( 'isMandatory' => false, - 'checker' => 'is_integer' + 'validator' => 'is_integer' ), 'maxlength' => array( 'isMandatory' => false, - 'checker' => 'is_integer' + 'validator' => 'is_integer' ) ), - 'checker' => 'GadgetPrefs::checkStringOptionDefinition' + 'validator' => 'GadgetPrefs::validateStringOptionDefinition', + 'checker' => 'GadgetPrefs::checkStringPref' ), 'number' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true, - 'checker' => 'GadgetPrefs::isFloatOrIntOrNull' + 'validator' => 'GadgetPrefs::isFloatOrIntOrNull' ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ), 'required' => array( 'isMandatory' => false, - 'checker' => 'is_bool' + 'validator' => 'is_bool' ), 'integer' => array( 'isMandatory' => false, - 'checker' => 'is_bool' + 'validator' => 'is_bool' ), 'min' => array( 'isMandatory' => false, - 'checker' => 'GadgetPrefs::isFloatOrInt' + 'validator' => 'GadgetPrefs::isFloatOrInt' ), 'max' => array( 'isMandatory' => false, - 'checker' => 'GadgetPrefs::isFloatOrInt' + 'validator' => 'GadgetPrefs::isFloatOrInt' ) ), - 'checker' => 'GadgetPrefs::checkNumberOptionDefinition' + 'validator' => 'GadgetPrefs::validateNumberOptionDefinition', + 'checker' => 'GadgetPrefs::checkNumberPref' ), 'select' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ), 'options' => array( 'isMandatory' => true, - 'checker' => 'is_array' + 'validator' => 'is_array' ) ), - 'checker' => 'GadgetPrefs::checkSelectOptionDefinition' + 'validator' => 'GadgetPrefs::validateSelectOptionDefinition', + 'checker' => 'GadgetPrefs::checkSelectPref', + 'getMessages' => 'GadgetPrefs::getSelectMessages' ), 'range' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true, - 'checker' => 'GadgetPrefs::isFloatOrIntOrNull' + 'validator' => 'GadgetPrefs::isFloatOrIntOrNull' ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ), 'min' => array( 'isMandatory' => true, - 'checker' => 'GadgetPrefs::isFloatOrInt' + 'validator' => 'GadgetPrefs::isFloatOrInt' ), 'max' => array( 'isMandatory' => true, - 'checker' => 'GadgetPrefs::isFloatOrInt' + 'validator' => 'GadgetPrefs::isFloatOrInt' ), 'step' => array( 'isMandatory' => false, - 'checker' => 'GadgetPrefs::isFloatOrInt' + 'validator' => 'GadgetPrefs::isFloatOrInt' ) ), - 'checker' => 'GadgetPrefs::checkRangeOptionDefinition' + 'validator' => 'GadgetPrefs::validateRangeOptionDefinition', + 'checker' => 'GadgetPrefs::checkRangePref' ), 'date' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ) - ) + ), + 'checker' => 'GadgetPrefs::checkDatePref' ), 'color' => array( 'description' => array( + 'name' => array( + 'isMandatory' => true, + 'validator' => 'GadgetPrefs::isValidPreferenceName' + ), 'default' => array( 'isMandatory' => true ), 'label' => array( 'isMandatory' => true, - 'checker' => 'is_string' + 'validator' => 'is_string' ) - ) + ), + 'checker' => 'GadgetPrefs::checkColorPref' ) ); + private static function isValidPreferenceName( $name ) { + return strlen( $name ) <= 40 + && preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name ); + } + + //Further checks for 'string' options - private static function checkStringOptionDefinition( $option ) { + private static function validateStringOptionDefinition( $option ) { if ( isset( $option['minlength'] ) && $option['minlength'] < 0 ) { return false; } @@ -174,8 +229,13 @@ return is_float( $param ) || is_int( $param ) || $param === null; } + //default flattener for simple fields that encode for a single preference + private static function flattenSimpleField( $fieldDescription ) { + return array( $fieldDescription['name'] => $fieldDescription ); + } + //Further checks for 'number' options - private static function checkNumberOptionDefinition( $option ) { + private static function validateNumberOptionDefinition( $option ) { if ( isset( $option['integer'] ) && $option['integer'] === true ) { //Check if 'min', 'max' and 'default' are integers (if given) if ( intval( $option['default'] ) != $option['default'] ) { @@ -192,7 +252,7 @@ return true; } - private static function checkSelectOptionDefinition( $option ) { + private static function validateSelectOptionDefinition( $option ) { $options = $option['options']; foreach ( $options as $opt => $optVal ) { @@ -212,7 +272,7 @@ return true; } - private static function checkRangeOptionDefinition( $option ) { + private static function validateRangeOptionDefinition( $option ) { $step = isset( $option['step'] ) ? $option['step'] : 1; if ( $step <= 0 ) { @@ -233,42 +293,71 @@ return true; } - - //Checks if the given description of the preferences is valid - public static function isPrefsDescriptionValid( $prefsDescription ) { - if ( !is_array( $prefsDescription ) - || !isset( $prefsDescription['fields'] ) - || !is_array( $prefsDescription['fields'] ) ) + + //Flattens a simple field, by calling its field-specific flattener if there is any, + //or the default flattener otherwise. + private static function flattenFieldDescription( $fieldDescription ) { + $typeSpec = self::$prefsDescriptionSpecifications[$fieldDescription['type']]; + $typeDescription = $typeSpec['description']; + if ( isset( $typeSpec['flattener'] ) ) { + $flattener = $typeSpec['flattener']; + } else { + $flattener = 'GadgetPrefs::flattenSimpleField'; + } + return call_user_func( $flattener, $fieldDescription ); + } + + //Returns a map keyed at preference names, and with their corresponding + //"simple" field descriptions as values. + //It is assumed that $prefsDescription is valid. + private static function flattenPrefsDescription( $prefsDescription ) { + $flattenedPrefsDescription = array(); + foreach ( $prefsDescription['fields'] as $fieldDescription ) { + $flt = self::flattenFieldDescription( $fieldDescription ); + $flattenedPrefsDescription = array_merge( $flattenedPrefsDescription, $flt ); + } + return $flattenedPrefsDescription; + } + + //Validate the description of a 'section' of preferences + private static function validateSectionDefinition( $sectionDescription ) { + static $mandatoryCount = array(), $initialized = false; + + if ( !is_array( $sectionDescription ) + || !isset( $sectionDescription['fields'] ) + || !is_array( $sectionDescription['fields'] ) ) { return false; } + if ( !$initialized ) { + //Count of mandatory members for each type + foreach ( self::$prefsDescriptionSpecifications as $type => $typeSpec ) { + $mandatoryCount[$type] = 0; + foreach ( $typeSpec['description'] as $fieldName => $fieldSpec ) { + if ( $fieldSpec['isMandatory'] === true ) { + ++$mandatoryCount[$type]; + } + } + } + $initialized = true; + } + //Check if 'fields' is a regular (not-associative) array, and that it is not empty - $count = count( $prefsDescription['fields'] ); - if ( $count == 0 || array_keys( $prefsDescription['fields'] ) !== range( 0, $count - 1 ) ) { + $count = count( $sectionDescription['fields'] ); + if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) { return false; } - //Count of mandatory members for each type - $mandatoryCount = array(); - foreach ( self::$prefsDescriptionSpecifications as $type => $typeSpec ) { - $mandatoryCount[$type] = 0; - foreach ( $typeSpec['description'] as $fieldName => $fieldSpec ) { - if ( $fieldSpec['isMandatory'] === true ) { - ++$mandatoryCount[$type]; - } - } - } - //TODO: validation of members other than $prefs['fields'] - //Map of encountered names - $names = array(); + //Flattened preferences + $flattenedPrefs = array(); - foreach ( $prefsDescription['fields'] as $optionDefinition ) { + foreach ( $sectionDescription['fields'] as $optionDefinition ) { - //Check if 'name' and 'type' are set - if ( !isset( $optionDefinition['type'] ) || !isset( $optionDefinition['name'] ) ) { + //Check if 'type' is set + if ( !isset( $optionDefinition['type'] ) ) { return false; } @@ -279,30 +368,14 @@ return false; } - $option = $optionDefinition['name']; - - //check that it's different from previous names - if ( isset( $names[$option] ) ) { - return false; - } - - $names[$option] = true; - - //check option name compliance - if ( strlen( $option ) > 40 - || !preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $option ) ) - { - return false; - } - //Check if all fields satisfy specification $typeSpec = self::$prefsDescriptionSpecifications[$type]; $typeDescription = $typeSpec['description']; $count = 0; //count of present mandatory members foreach ( $optionDefinition as $fieldName => $fieldValue ) { - if ( $fieldName == 'type' || $fieldName == 'name' ) { - continue; //'type' and 'name' must not be checked + if ( $fieldName == 'type' ) { + continue; //'type' must not be checked } if ( !isset( $typeDescription[$fieldName] ) ) { @@ -313,9 +386,9 @@ ++$count; } - if ( isset( $typeDescription[$fieldName]['checker'] ) ) { - $checker = $typeDescription[$fieldName]['checker']; - if ( !call_user_func( $checker, $fieldValue ) ) { + if ( isset( $typeDescription[$fieldName]['validator'] ) ) { + $validator = $typeDescription[$fieldName]['validator']; + if ( !call_user_func( $validator, $fieldValue ) ) { return false; } } @@ -325,30 +398,48 @@ return false; //not all mandatory members are given } - if ( isset( $typeSpec['checker'] ) ) { + if ( isset( $typeSpec['validator'] ) ) { //Call type-specific checker for finer validation - if ( !call_user_func( $typeSpec['checker'], $optionDefinition ) ) { + if ( !call_user_func( $typeSpec['validator'], $optionDefinition ) ) { return false; } } + + //flatten preferences described by this field + $flt = self::flattenFieldDescription( $optionDefinition ); - //Finally, check that the 'default' fields exists and is valid - if ( !array_key_exists( 'default', $optionDefinition ) ) { - return false; + foreach ( $flt as $prefName => $prefDescription ) { + //Finally, check that the 'default' fields exists and is valid + //for all preferences encoded by this field + if ( !array_key_exists( 'default', $prefDescription ) ) { + return false; + } + + $prefs = array( 'dummy' => $optionDefinition['default'] ); + if ( !self::checkSinglePref( $optionDefinition, $prefs, 'dummy' ) ) { + return false; + } } - $prefs = array( 'dummy' => $optionDefinition['default'] ); - if ( !self::checkSinglePref( $optionDefinition, $prefs, 'dummy' ) ) { + //If there are preferences with the same name of a previously encountered preference, fail + if ( array_intersect( array_keys( $flt ), array_keys( $flattenedPrefs ) ) ) { return false; } + $flattenedPrefs = array_merge( $flattenedPrefs, $flt ); } return true; } - //Check if a preference is valid, according to description + //Checks if the given description of the preferences is valid + public static function isPrefsDescriptionValid( $prefsDescription ) { + return self::validateSectionDefinition( $prefsDescription ); + } + + //Check if a preference is valid, according to description. + //$prefDescription must be the description of a "simple" field (that is, with 'checker') //NOTE: we pass both $prefs and $prefName (instead of just $prefs[$prefName]) - // to allow checking for null. + // to allow checking for undefined values. private static function checkSinglePref( $prefDescription, $prefs, $prefName ) { //isset( $prefs[$prefName] ) would return false for null values @@ -356,129 +447,158 @@ return false; } - $pref = $prefs[$prefName]; - - switch ( $prefDescription['type'] ) { - case 'boolean': - return is_bool( $pref ); - case 'string': - if ( !is_string( $pref ) ) { - return false; - } - - $len = strlen( $pref ); - - //Checks the "required" option, if present - $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true; - if ( $required === true && $len == 0 ) { - return false; - } elseif ( $required === false && $len == 0 ) { - return true; //overriding 'minlength' - } - - //Checks the "minlength" option, if present - $minlength = isset( $prefDescription['minlength'] ) ? $prefDescription['minlength'] : 0; - if ( $len < $minlength ){ - return false; - } + $value = $prefs[$prefName]; + $type = $prefDescription['type']; + + if ( !isset( self::$prefsDescriptionSpecifications[$type] ) + || !isset( self::$prefsDescriptionSpecifications[$type]['checker'] ) ) + { + return false; + } + + $checker = self::$prefsDescriptionSpecifications[$type]['checker']; + return call_user_func( $checker, $prefDescription, $value ); + } - //Checks the "maxlength" option, if present - $maxlength = isset( $prefDescription['maxlength'] ) ? $prefDescription['maxlength'] : 1024; //TODO: what big integer here? - if ( $len > $maxlength ){ - return false; - } - - return true; - case 'number': - if ( !is_float( $pref ) && !is_int( $pref ) && $pref !== null ) { - return false; - } + //Checker for 'boolean' preferences + private static function checkBooleanPref( $prefDescription, $value ) { + return is_bool( $value ); + } - $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true; - if ( $required === false && $pref === null ) { - return true; - } - - if ( $pref === null ) { - return false; //$required === true, so null is not acceptable - } + //Checker for 'string' preferences + private static function checkStringPref( $prefDescription, $value ) { + if ( !is_string( $value ) ) { + return false; + } + + $len = strlen( $value ); + + //Checks the "required" option, if present + $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true; + if ( $required === true && $len == 0 ) { + return false; + } elseif ( $required === false && $len == 0 ) { + return true; //overriding 'minlength' + } + + //Checks the "minlength" option, if present + $minlength = isset( $prefDescription['minlength'] ) ? $prefDescription['minlength'] : 0; + if ( $len < $minlength ){ + return false; + } - $integer = isset( $prefDescription['integer'] ) ? $prefDescription['integer'] : false; - - if ( $integer === true && intval( $pref ) != $pref ) { - return false; //not integer - } - - if ( isset( $prefDescription['min'] ) ) { - $min = $prefDescription['min']; - if ( $pref < $min ) { - return false; //value below minimum - } - } + //Checks the "maxlength" option, if present + $maxlength = isset( $prefDescription['maxlength'] ) ? $prefDescription['maxlength'] : 1024; //TODO: what big integer here? + if ( $len > $maxlength ){ + return false; + } + + return true; + } - if ( isset( $prefDescription['max'] ) ) { - $max = $prefDescription['max']; - if ( $pref > $max ) { - return false; //value above maximum - } - } + //Checker for 'number' preferences + private static function checkNumberPref( $prefDescription, $value ) { + if ( !is_float( $value ) && !is_int( $value ) && $value !== null ) { + return false; + } - return true; - case 'select': - $values = array_values( $prefDescription['options'] ); - return in_array( $pref, $values, true ); - case 'range': - if ( !is_float( $pref ) && !is_int( $pref ) ) { - return false; - } - - $min = $prefDescription['min']; - $max = $prefDescription['max']; - - if ( $pref < $min || $pref > $max ) { - return false; - } - - $step = isset( $prefDescription['step'] ) ? $prefDescription['step'] : 1; - - if ( $step <= 0 ) { - return false; - } - - //Valid values are min, min + step, min + 2*step, ... - //Then ( $pref - $min ) / $step must be close enough to an integer - $eps = 1.0e-6; //tolerance - $tmp = ( $pref - $min ) / $step; - if ( abs( $tmp - floor( $tmp ) ) > $eps ) { - return false; - } - - return true; - case 'date': - if ( $pref === null ) { - return true; - } - - //Basic syntactic checks - if ( !is_string( $pref ) || - !preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $pref ) ) - { - return false; - } - - //Full parsing - return date_create( $pref ) !== false; - case 'color': - //Check if it's a string representing a color - //(with 6 hexadecimal lowercase characters). - return is_string( $pref ) && preg_match( '/^#[0-9a-f]{6}$/', $pref ); - default: - return false; //unexisting type + $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true; + if ( $required === false && $value === null ) { + return true; } + + if ( $value === null ) { + return false; //$required === true, so null is not acceptable + } + + $integer = isset( $prefDescription['integer'] ) ? $prefDescription['integer'] : false; + + if ( $integer === true && intval( $value ) != $value ) { + return false; //not integer + } + + if ( isset( $prefDescription['min'] ) ) { + $min = $prefDescription['min']; + if ( $value < $min ) { + return false; //value below minimum + } + } + + if ( isset( $prefDescription['max'] ) ) { + $max = $prefDescription['max']; + if ( $value > $max ) { + return false; //value above maximum + } + } + + return true; } + //Checker for 'select' preferences + private static function checkSelectPref( $prefDescription, $value ) { + $values = array_values( $prefDescription['options'] ); + return in_array( $value, $values, true ); + } + + //Checker for 'range' preferences + private static function checkRangePref( $prefDescription, $value ) { + if ( !is_float( $value ) && !is_int( $value ) ) { + return false; + } + + $min = $prefDescription['min']; + $max = $prefDescription['max']; + + if ( $value < $min || $value > $max ) { + return false; + } + + $step = isset( $prefDescription['step'] ) ? $prefDescription['step'] : 1; + + if ( $step <= 0 ) { + return false; + } + + //Valid values are min, min + step, min + 2*step, ... + //Then ( $value - $min ) / $step must be close enough to an integer + $eps = 1.0e-6; //tolerance + $tmp = ( $value - $min ) / $step; + if ( abs( $tmp - floor( $tmp ) ) > $eps ) { + return false; + } + + return true; + } + + //Checker for 'date' preferences + private static function checkDatePref( $prefDescription, $value ) { + if ( $value === null ) { + return true; + } + + //Basic syntactic checks + if ( !is_string( $value ) || + !preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $value ) ) + { + return false; + } + + //Full parsing + return date_create( $value ) !== false; + } + + //Checker for 'color' preferences + private static function checkColorPref( $prefDescription, $value ) { + //Check if it's a string representing a color + //(with 6 hexadecimal lowercase characters). + return is_string( $value ) && preg_match( '/^#[0-9a-f]{6}$/', $value ); + } + + + /** - * Checks if $prefs is an array of preferences that passes validation + * Checks if $prefs is an array of preferences that passes validation. + * It is assumed that $prefsDescription is a valid description of preferences. * * @param $prefsDescription Array: the preferences description to use. * @param $prefs Array: reference of the array of preferences to check. @@ -486,9 +606,10 @@ * @return boolean true if $prefs passes validation against $prefsDescription, false otherwise. */ public static function checkPrefsAgainstDescription( $prefsDescription, $prefs ) { + $flattenedPrefs = self::flattenPrefsDescription( $prefsDescription ); $validPrefs = array(); //Check that all the given preferences pass validation - foreach ( $prefsDescription['fields'] as $prefDescription ) { + foreach ( $flattenedPrefs as $prefDescription ) { $prefName = $prefDescription['name']; if ( !self::checkSinglePref( $prefDescription, $prefs, $prefName ) ) { return false; @@ -509,15 +630,17 @@ /** * Fixes $prefs so that it matches the description given by $prefsDescription. * All values of $prefs that fail validation are replaced with default values. + * It is assumed that $prefsDescription is a valid description of preferences. * * @param $prefsDescription Array: the preferences description to use. * @param &$prefs Array: reference of the array of preferences to match. */ public static function matchPrefsWithDescription( $prefsDescription, &$prefs ) { + $flattenedPrefs = self::flattenPrefsDescription( $prefsDescription ); $validPrefs = array(); //Fix preferences that fail validation, by replacing their value with default - foreach ( $prefsDescription['fields'] as $prefDescription ) { + foreach ( $flattenedPrefs as $prefDescription ) { $prefName = $prefDescription['name']; if ( !self::checkSinglePref( $prefDescription, $prefs, $prefName ) ) { $prefs[$prefName] = $prefDescription['default']; @@ -568,28 +691,36 @@ * @return Array: the messages needed by $prefsDescription. */ public static function getMessages( $prefsDescription ) { - $maybeMsgs = array(); + $msgs = array(); - if ( isset( $prefsDescription['intro'] ) ) { - $maybeMsgs[] = $prefsDescription['intro']; + if ( isset( $prefsDescription['intro'] ) && self::isMessage( $prefsDescription['intro'] ) ) { + $msgs[] = substr( $prefsDescription['intro'], 1 ); } - foreach ( $prefsDescription['fields'] as $prefName => $prefDesc ) { - $maybeMsgs[] = $prefDesc['label']; - - if ( $prefDesc['type'] == 'select' ) { - foreach ( $prefDesc['options'] as $optName => $value ) { - $maybeMsgs[] = $optName; + foreach ( $prefsDescription['fields'] as $prefDesc ) { + $type = $prefDesc['type']; + $prefSpec = self::$prefsDescriptionSpecifications[$type]; + if ( isset( $prefSpec['getMessages'] ) ) { + $getMessages = $prefSpec['getMessages']; + $msgs = array_merge( $msgs, call_user_func( $getMessages, $prefDesc ) ); + } else { + if ( isset( $prefDesc['label'] ) && self::isMessage( $prefDesc['label'] ) ) { + $msgs[] = substr( $prefDesc['label'], 1 ); } } } + return array_unique( $msgs ); + } + + //Returns the messages for a 'select' field description + private static function getSelectMessages( $prefDescription ) { $msgs = array(); - foreach ( $maybeMsgs as $msg ) { - if ( self::isMessage( $msg ) ) { - $msgs[] = substr( $msg, 1 ); + foreach ( $prefDescription['options'] as $optName => $value ) { + if ( self::isMessage( $optName ) ) { + $msgs[] = substr( $optName, 1 ); } } - return array_unique( $msgs ); + return $msgs; } } Modified: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js =================================================================== --- branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js 2011-07-20 12:59:11 UTC (rev 92632) +++ branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js 2011-07-20 14:47:04 UTC (rev 92633) @@ -57,17 +57,21 @@ dataType: "json", // response type success: function( response ) { - if ( typeof response.getgadgetprefs != 'object' ) { + if ( typeof response.description != 'object' + || typeof response.values != 'object') + { alert( mw.msg( 'gadgets-unexpected-error' ) ) return; } //Create and show dialog - var prefs = response.getgadgetprefs; + var prefsDescription = response.description; + var values = response.values; - var dialogBody = $( prefs ).formBuilder( { - gadget: gadget + var dialogBody = $( prefsDescription ).formBuilder( { + gadget: gadget, + values: values } ); $( dialogBody ).submit( function() { Modified: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js =================================================================== --- branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js 2011-07-20 12:59:11 UTC (rev 92632) +++ branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js 2011-07-20 14:47:04 UTC (rev 92633) @@ -87,9 +87,9 @@ } //A field with no content - function EmptyField( $form, name, desc ) { + function EmptyField( $form, desc, values ) { //Check existence of compulsory fields - if ( typeof name == 'undefined' || !desc.type || !desc.label ) { + if ( !desc.type || !desc.label ) { $.error( "Missing arguments" ); } @@ -97,22 +97,16 @@ this.$p = $( '<p/>' ); - this.name = name; this.desc = desc; } - EmptyField.prototype.getName = function() { - return this.name; - }; - EmptyField.prototype.getDesc = function() { return this.desc; }; - //Override expected - EmptyField.prototype.getValue = function() { - return null; + EmptyField.prototype.getValues = function() { + return {}; }; EmptyField.prototype.getElement = function() { @@ -129,12 +123,12 @@ //A field with just a label LabelField.prototype = object( EmptyField.prototype ); LabelField.prototype.constructor = LabelField; - function LabelField( $form, name, desc ) { - EmptyField.call( this, $form, name, desc ); + function LabelField( $form, desc, values ) { + EmptyField.call( this, $form, desc, values ); var $label = $( '<label/>' ) .text( preproc( this.$form, this.desc.label ) ) - .attr('for', idPrefix + this.name ); + .attr('for', idPrefix + this.desc.name ); this.$p.append( $label ); } @@ -142,53 +136,59 @@ //A field with a label and a checkbox BooleanField.prototype = object( LabelField.prototype ); BooleanField.prototype.constructor = BooleanField; - function BooleanField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function BooleanField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( typeof desc.value != 'boolean' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( typeof value != 'boolean' ) { + $.error( "value is invalid" ); } this.$c = $( '<input/>' ) .attr( 'type', 'checkbox' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ) - .attr( 'checked', this.desc.value ); + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ) + .attr( 'checked', value ); this.$p.append( this.$c ); } - BooleanField.prototype.getValue = function() { - return this.$c.is( ':checked' ); + BooleanField.prototype.getValues = function() { + var res = {}; + res[this.desc.name] = this.$c.is( ':checked' ); + return res; }; //A field with a textbox accepting string values StringField.prototype = object( LabelField.prototype ); StringField.prototype.constructor = StringField; - function StringField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function StringField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( typeof desc.value != 'string' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( typeof value != 'string' ) { + $.error( "value is invalid" ); } this.$text = $( '<input/>' ) .attr( 'type', 'text' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ) - .val( desc.value ); + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ) + .val( value ); this.$p.append( this.$text ); } - StringField.prototype.getValue = function() { - return this.$text.val(); + StringField.prototype.getValues = function() { + var res = {}; + res[this.desc.name] = this.$text.val(); + return res; }; StringField.prototype.getValidationSettings = function() { var settings = LabelField.prototype.getValidationSettings.call( this ), - fieldId = idPrefix + this.name; + fieldId = idPrefix + this.desc.name; settings.rules[fieldId] = {}; var fieldRules = settings.rules[fieldId], @@ -218,30 +218,33 @@ //A field with a textbox accepting numeric values NumberField.prototype = object( LabelField.prototype ); NumberField.prototype.constructor = NumberField; - function NumberField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function NumberField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( desc.value !== null && typeof desc.value != 'number' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( value !== null && typeof value != 'number' ) { + $.error( "value is invalid" ); } this.$text = $( '<input/>' ) .attr( 'type', 'text' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ) - .val( desc.value ); + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ) + .val( value ); this.$p.append( this.$text ); } - NumberField.prototype.getValue = function() { - var val = parseFloat( this.$text.val() ); - return isNaN( val ) ? null : val; + NumberField.prototype.getValues = function() { + var val = parseFloat( this.$text.val() ), + res = {}; + res[this.desc.name] = isNaN( val ) ? null : val; + return res; }; NumberField.prototype.getValidationSettings = function() { var settings = LabelField.prototype.getValidationSettings.call( this ), - fieldId = idPrefix + this.name; + fieldId = idPrefix + this.desc.name; settings.rules[fieldId] = {}; var fieldRules = settings.rules[fieldId], @@ -277,50 +280,54 @@ //A field with a drop-down list SelectField.prototype = object( LabelField.prototype ); SelectField.prototype.constructor = SelectField; - function SelectField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function SelectField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); var $select = this.$select = $( '<select/>' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ); + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ); - var values = []; + var validValues = []; var self = this; $.each( desc.options, function( optName, optVal ) { - var i = values.length; + var i = validValues.length; $( '<option/>' ) .text( preproc( self.$form, optName ) ) .val( i ) .appendTo( $select ); - values.push( optVal ); + validValues.push( optVal ); } ); - this.values = values; + this.validValues = validValues; - if ( $.inArray( desc.value, values ) == -1 ) { - $.error( "desc.value is not in the list of possible values" ); + var value = values[this.desc.name]; + if ( $.inArray( value, validValues ) == -1 ) { + $.error( "value is not in the list of possible values" ); } - var i = $.inArray( desc.value, values ); + var i = $.inArray( value, validValues ); $select.val( i ).attr( 'selected', 'selected' ); this.$p.append( $select ); } - SelectField.prototype.getValue = function() { - var i = parseInt( this.$select.val(), 10 ); - return this.values[i]; + SelectField.prototype.getValues = function() { + var i = parseInt( this.$select.val(), 10 ), + res = {}; + res[this.desc.name] = this.validValues[i]; + return res; }; //A field with a slider, representing ranges of numbers RangeField.prototype = object( LabelField.prototype ); RangeField.prototype.constructor = RangeField; - function RangeField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function RangeField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( typeof desc.value != 'number' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( typeof value != 'number' ) { + $.error( "value is invalid" ); } if ( typeof desc.min != 'number' ) { @@ -335,17 +342,17 @@ $.error( "desc.step is invalid" ); } - if ( desc.value < desc.min || desc.value > desc.max ) { - $.error( "desc.value is out of range" ); + if ( value < desc.min || value > desc.max ) { + $.error( "value is out of range" ); } var $slider = this.$slider = $( '<div/>' ) - .attr( 'id', idPrefix + this.name ); + .attr( 'id', idPrefix + this.desc.name ); var options = { min: desc.min, max: desc.max, - value: desc.value + value: value }; if ( typeof desc.step != 'undefined' ) { @@ -357,34 +364,37 @@ this.$p.append( $slider ); } - RangeField.prototype.getValue = function() { - return this.$slider.slider( 'value' ); + RangeField.prototype.getValues = function() { + var res = {}; + res[this.desc.name] = this.$slider.slider( 'value' ); + return res; }; //A field with a textbox with a datepicker DateField.prototype = object( LabelField.prototype ); DateField.prototype.constructor = DateField; - function DateField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function DateField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( typeof desc.value == 'undefined' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( typeof value == 'undefined' ) { + $.error( "value is invalid" ); } var date; - if ( desc.value !== null ) { - date = new Date( desc.value ); + if ( value !== null ) { + date = new Date( value ); if ( !isFinite( date ) ) { - $.error( "desc.value is invalid" ); + $.error( "value is invalid" ); } } this.$text = $( '<input/>' ) .attr( 'type', 'text' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ) + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ) .datepicker( { onSelect: function() { //Force validation, so that a previous 'invalid' state is removed @@ -392,7 +402,7 @@ } } ); - if ( desc.value !== null ) { + if ( value !== null ) { this.$text.datepicker( 'setDate', date ); } @@ -400,25 +410,28 @@ this.$p.append( this.$text ); } - DateField.prototype.getValue = function() { - var d = this.$text.datepicker( 'getDate' ); + DateField.prototype.getValues = function() { + var d = this.$text.datepicker( 'getDate' ), + res = {}; if ( d === null ) { return null; } //UTC date in ISO 8601 format [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z - return '' + pad( d.getUTCFullYear(), 4 ) + '-' + + res[this.desc.name] = '' + + pad( d.getUTCFullYear(), 4 ) + '-' + pad( d.getUTCMonth() + 1, 2 ) + '-' + pad( d.getUTCDate(), 2 ) + 'T' + pad( d.getUTCHours(), 2 ) + ':' + pad( d.getUTCMinutes(), 2 ) + ':' + pad( d.getUTCSeconds(), 2 ) + 'Z'; + return res; }; DateField.prototype.getValidationSettings = function() { var settings = LabelField.prototype.getValidationSettings.call( this ), - fieldId = idPrefix + this.name; + fieldId = idPrefix + this.desc.name; settings.rules[fieldId] = { "datePicker": true @@ -436,20 +449,21 @@ ColorField.prototype = object( LabelField.prototype ); ColorField.prototype.constructor = ColorField; - function ColorField( $form, name, desc ){ - LabelField.call( this, $form, name, desc ); + function ColorField( $form, desc, values ){ + LabelField.call( this, $form, desc, values ); - if ( typeof desc.value == 'undefined' ) { - $.error( "desc.value is invalid" ); + var value = values[this.desc.name]; + if ( typeof value == 'undefined' ) { + $.error( "value is invalid" ); } this.$text = $( '<input/>' ) .attr( 'type', 'text' ) - .attr( 'id', idPrefix + this.name ) - .attr( 'name', idPrefix + this.name ) + .attr( 'id', idPrefix + this.desc.name ) + .attr( 'name', idPrefix + this.desc.name ) .addClass( 'colorpicker-input' ) - .val( desc.value ) - .css( 'background-color', desc.value ) + .val( value ) + .css( 'background-color', value ) .focus( function() { $( '<div/>' ) .attr( 'id', 'colorpicker' ) @@ -484,7 +498,7 @@ ColorField.prototype.getValidationSettings = function() { var settings = LabelField.prototype.getValidationSettings.call( this ), - fieldId = idPrefix + this.name; + fieldId = idPrefix + this.desc.name; settings.rules[fieldId] = { "color": true @@ -492,10 +506,12 @@ return settings; } - ColorField.prototype.getValue = function() { - var color = $.colorUtil.getRGB( this.$text.val() ); - return '#' + pad( color[0].toString( 16 ), 2 ) + + ColorField.prototype.getValues = function() { + var color = $.colorUtil.getRGB( this.$text.val() ), + res = {} + res[this.desc.name] = '#' + pad( color[0].toString( 16 ), 2 ) + pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ); + return res; }; //If a click happens outside the colorpicker while it is showed, remove it @@ -559,7 +575,6 @@ for ( var i = 0; i < description.fields.length; i++ ) { //TODO: validate fieldName var field = description.fields[i], - fieldName = field.name, FieldConstructor = validFieldTypes[field.type]; if ( typeof FieldConstructor != 'function' ) { @@ -569,7 +584,7 @@ var f; try { - f = new FieldConstructor( $form, fieldName, field ); + f = new FieldConstructor( $form, field, options.values ); } catch ( e ) { mw.log( e ); return null; //constructor failed, wrong syntax in field description @@ -610,7 +625,7 @@ for ( var i = 0; i < data.fields.length; i++ ) { var f = data.fields[i]; - result[f.getName()] = f.getValue(); + $.extend( result, f.getValues() ); } return result; _______________________________________________ MediaWiki-CVS mailing list MediaWiki-CVS@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs