diff --git a/h5p.classes.php b/h5p.classes.php index 36dc3b3..716686c 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1071,11 +1071,325 @@ class H5PCore { * * @param array $library * With keys machineName, majorVersion and minorVersion + * @param boolean $folderName + * Use hyphen instead of space in returned string. * @return string * On the form {machineName} {majorVersion}.{minorVersion} */ public function libraryToString($library, $folderName = FALSE) { return $library['machineName'] . ($folderName ? '-' : ' ') . $library['majorVersion'] . '.' . $library['minorVersion']; } + + /** + * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion} + * + * @param string $libraryString + * On the form {machineName} {majorVersion}.{minorVersion} + * @return array|FALSE + * With keys machineName, majorVersion and minorVersion. + * Returns FALSE only if string is not parsable in the normal library + * string formats "Lib.Name-x.y" or "Lib.Name x.y" + */ + public function libraryFromString($libraryString) { + $re = '/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i'; + $matches = array(); + $res = preg_match($re, $libraryString, $matches); + if ($res) { + return array( + 'machineName' => $matches[1], + 'majorVersion' => $matches[2], + 'minorVersion' => $matches[3] + ); + } + return FALSE; + } +} + +/** + * Functions for validating basic types from H5P library semantics. + */ +class H5PContentValidator { + public $h5pF; + public $h5pC; + private $typeMap; + private $semanticsCache; + + /** + * Constructor for the H5PContentValidator + * + * @param object $H5PFramework + * The frameworks implementation of the H5PFrameworkInterface + * @param object $H5PCore + * The main H5PCore instance + */ + public function __construct($H5PFramework, $H5PCore) { + $this->h5pF = $H5PFramework; + $this->h5pC = $H5PCore; + $this->typeMap = array( + 'text' => 'validateText', + 'number' => 'validateNumber', + 'boolean' => 'validateBoolean', + 'list' => 'validateList', + 'group' => 'validateGroup', + 'image' => 'validateImage', + 'video' => 'validateVideo', + 'audio' => 'validateAudio', + 'select' => 'validateSelect', + 'library' => 'validateLibrary', + ); + // Cache for semantics used within this validation to avoid unneccessary + // json_decodes if a library is used multiple times. + $this->semanticsCache = array(); + } + + /** + * Validate the given value from content with the matching semantics + * object from semantics + * + * Function will recurse via external functions for container objects like + * 'list', 'group' and 'library'. + * + * @param object $value + * Object to be verified. May be a string or an array. (normal or keyed) + * @param object $semantics + * Semantics object from semantics.json for main library. Further + * semantics will be loaded from H5PFramework if any libraries are + * found within the value data. + */ + public function validateBySemantics(&$value, $semantics) { + $fakebaseobject = (object) array( + 'type' => 'group', + 'fields' => $semantics, + ); + $this->validateGroup($value, $fakebaseobject, FALSE); + } + + /** + * Validate given text value against text semantics. + */ + public function validateText(&$text, $semantics) { + if ($semantics->widget && $semantics->widget == 'html') { + // Build allowed tag list, based in $semantics->tags and known defaults. + // These four are always allowed. + $tags = array('div', 'span', 'p', 'br'); + if (isset($semantics->tags)) { + $tags = array_merge($tags, $semantics->tags); + // Add related tags for table etc. + if (in_array('table', $semantics->tags)) { + $tags = array_merge($tags, array('tr', 'td', 'th', 'colgroup', 'thead', 'tbody', 'tfoot')); + } + if (in_array('b', $semantics->tags)) { + $tags[] = 'strong'; + } + if (in_array('i', $semantics->tags)) { + $tags[] = 'em'; + } + if (in_array('ul', $semantics->tags) || in_array('ol', $semantics->tags)) { + $tags[] = 'li'; + } + } + $allowedtags = implode('', array_map(array($this, 'bracketTags'), $tags)); + + // Strip invalid HTML tags. + $text = strip_tags($text, $allowedtags); + } + else { + // Filter text to plain text. + $text = htmlspecialchars($text); + } + // Check if string is within allowed length + if (isset($semantics->maxLength)) { + $text = mb_substr($text, 0, $semantics->maxLength); + } + // Check if string is according to optional regexp in semantics + if (isset($semantics->regexp)) { + $pattern = $semantics->regexp->pattern; + $pattern .= isset($semantics->regexp->modifiers) ? $semantics->regexp->modifiers : ''; + if (preg_match($pattern, $text) === 0) { + // Note: explicitly ignore return value FALSE, to avoid removing text + // if regexp is invalid... + $this->h5pF->setErrorMessage($this->h5pF->t('Provided string is not valid according to regexp in semantics.')); + $text = ''; + } + } + } + private function bracketTags($tag) { + return '<'.$tag.'>'; + } + + /** + * Validate given value against number semantics + */ + public function validateNumber(&$number, $semantics) { + // Validate that $number is indeed a number + if (!is_numeric($number)) { + $number = 0; + } + // Check if number is within valid bounds. Move within bounds if not. + if (isset($semantics->min) && $number < $semantics->min) { + $number = $semantics->min; + } + if (isset($semantics->max) && $number > $semantics->max) { + $number = $semantics->max; + } + // Check if number is within allowed bounds even if step value is set. + if (isset($semantics->step)) { + $testnumber = $number - (isset($semantics->min) ? $semantics->min : 0); + $rest = $testnumber % $semantics->step; + if ($rest !== 0) { + $number -= $rest; + } + } + // Check if number has proper number of decimals. + if (isset($semantics->decimals)) { + $number = round($number, $semantics->decimals); + } + } + + /** + * Validate given value against boolean semantics + */ + public function validateBoolean(&$bool, $semantics) { + if (!is_bool($bool)) { + $bool = FALSE; + } + } + + /** + * Validate select values + */ + public function validateSelect(&$select, $semantics) { + // Special case for dynamicCheckboxes (valid options are generated live) + if ($semantics->widget == 'dynamicCheckboxes') { + // No practical way to guess valid parameters. Just make sure we don't + // have special chars here. Also, dynamicCheckboxes will insert an + // array, so iterate it. + foreach ($select as $key => $value) { + $select[$key] = htmlspecialchars($value); + } + } + else if (!in_array($select, array_map(array($this, 'map_object_value'), $semantics->options))) { + $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in select.')); + $select = $semantics->options[0]->value; + } + } + private function map_object_value($o) { + return $o->value; + } + + /** + * Validate given list value agains list semantics. + * Will recurse into validating each item in the list according to the type. + */ + public function validateList(&$list, $semantics) { + $field = $semantics->field; + $function = $this->typeMap[$field->type]; + + // Check that list is not longer than allowed length. We do this before + // iterating to avoid unneccessary work. + if (isset($semantics->max)) { + array_splice($list, $semantics->max); + } + + // Validate each element in list. + foreach ($list as $key => $value) { + $this->$function($value, $field); + } + } + + /** + * Validate given image data + */ + public function validateImage(&$image, $semantics) { + $image->path = htmlspecialchars($image->path); + if ($image->mime && substr($image->mime, 0, 5) !== 'image') { + unset($image->mime); + } + } + + /** + * Validate given video data + */ + public function validateVideo(&$video, $semantics) { + foreach ($video as $variant) { + $variant->path = htmlspecialchars($variant->path); + if ($variant->mime && substr($variant->mime, 0, 5) !== 'video') { + unset($variant->mime); + } + } + } + + /** + * Validate given audio data + */ + public function validateAudio(&$audio, $semantics) { + foreach ($audio as $variant) { + $variant->path = htmlspecialchars($variant->path); + if ($variant->mime && substr($variant->mime, 0, 5) !== 'audio') { + unset($variant->mime); + } + } + } + + /** + * Validate given group value against group semantics. + * Will recurse into validating each group member. + */ + public function validateGroup(&$group, $semantics, $flatten = TRUE) { + // Groups with just one field are compressed in the editor to only output + // the child content. (Exemption for fake groups created by + // "validateBySemantics" above) + if (count($semantics->fields) == 1 && $flatten) { + $field = $semantics->fields[0]; + $function = $this->typeMap[$field->type]; + $this->$function($group, $field); + } + else { + foreach ($group as $key => &$value) { + // Find semantics for name=$key + $found = FALSE; + foreach ($semantics->fields as $field) { + if ($field->name == $key) { + $function = $this->typeMap[$field->type]; + $found = TRUE; + break; + } + } + if ($found) { + $this->$function($value, $field); + } + else { + // If validator is not found, something exists in content that does + // not have a corresponding semantics field. Remove it. + $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for ' . $key)); + unset($group->$key); + } + } + } + } + + /** + * Validate given library value against library semantics. + * + * Will recurse into validating the library's semantics too. + */ + public function validateLibrary(&$value, $semantics) { + // Check if provided library is within allowed options + if (in_array($value->library, $semantics->options)) { + if (isset($semanticsCache[$value->library])) { + $librarySemantics = $semanticsCache[$value->library]; + } + else { + $libspec = $this->h5pC->libraryFromString($value->library); + $library = $this->h5pF->loadLibrary($libspec['machineName'], $libspec['majorVersion'], $libspec['minorVersion']); + $librarySemantics = json_decode($library['semantics']); + $semanticsCache[$value->library] = $librarySemantics; + } + $this->validateBySemantics($value->params, $librarySemantics); + } + else { + $this->h5pF->setErrorMessage($this->h5pF->t('Library used in content is not a valid library according to semantics')); + } + } } ?> \ No newline at end of file