public function test_check_bookends() { $this->assertSame('left', stack_utils::check_bookends('x+1)^2', '(', ')')); $this->assertSame('right', stack_utils::check_bookends('(x+1', '(', ')')); $this->assertSame('left', stack_utils::check_bookends('(y^2+1))', '(', ')')); $this->assertSame('left', stack_utils::check_bookends('[sin(x)+1)', '(', ')')); $this->assertSame('right', stack_utils::check_bookends('[sin(x)+1)', '[', ']')); $this->assertSame(true, stack_utils::check_bookends('x+1', '(', ')')); $this->assertSame(true, stack_utils::check_bookends('x+1', '[', ']')); $this->assertSame(true, stack_utils::check_bookends('x+1', '{', '}')); $this->assertSame(true, stack_utils::check_bookends('(sin(x)+1)', '[', ']')); $this->assertSame(true, stack_utils::check_bookends('(sin(x)+1)', '(', ')')); $this->assertSame(true, stack_utils::check_bookends('[sin(x)+1)', '{', '}')); }
private function validate($security = 's', $syntax = true, $insertstars = 0, $allowwords = '') { if (!('s' === $security || 't' === $security)) { throw new stack_exception('stack_cas_casstring: security level, must be "s" or "t" only.'); } if (!is_bool($syntax)) { throw new stack_exception('stack_cas_casstring: syntax, must be Boolean.'); } if (!is_int($insertstars)) { throw new stack_exception('stack_cas_casstring: insertstars, must be an integer.'); } $this->valid = true; $this->casstring = $this->rawcasstring; $cmd = $this->rawcasstring; // CAS strings must be non-empty. if (trim($this->casstring) == '') { $this->answernote[] = 'empty'; $this->valid = false; return false; } // CAS strings may not contain @ or $. if (strpos($cmd, '@') !== false || strpos($cmd, '$') !== false) { $this->add_error(stack_string('illegalcaschars')); $this->answernote[] = 'illegalcaschars'; $this->valid = false; return false; } // Check for matching string delimiters. $cmdsafe = str_replace('\\"', '', $cmd); if (stack_utils::check_matching_pairs($cmdsafe, '"') == false) { $this->errors .= stack_string('stackCas_MissingString'); $this->answernote[] = 'MissingString'; $this->valid = false; } // Now remove any strings from the $cmd. list($cmd, $strings) = $this->strings_remove($cmd); // Search for HTML fragments. This is hard to do because < is an infix operator! // We cannot search for arbitrary closing tags, e.g. for the pattern '</' because // we pass back strings with HTML in when we have already evaluated plots! $htmlfragments = array('<span', '</span>', '<p>', '</p>'); foreach ($htmlfragments as $frag) { if (strpos($cmd, $frag) !== false) { $this->add_error(stack_string('htmlfragment') . ' <pre>' . $this->strings_replace($cmd, $strings) . '</pre>'); $this->answernote[] = 'htmlfragment'; $this->valid = false; return false; } } // If student, check for spaces between letters or numbers in expressions. if ($security != 't') { $pat = "|([A-Za-z0-9\\(\\)]+) ([A-Za-z0-9\\(\\)]+)|"; // Special case - allow students to type in expressions such as "x>1 and x<4". $cmdmod = str_replace(' or ', '', $cmd); $cmdmod = str_replace(' and ', '', $cmdmod); $cmdmod = str_replace('not ', '', $cmdmod); if (preg_match($pat, $cmdmod)) { $cmds = str_replace(' ', '<font color="red">_</font>', $this->strings_replace($cmd, $strings)); $this->add_error(stack_string('stackCas_spaces', array('expr' => stack_maxima_format_casstring($cmds)))); $this->answernote[] = 'spaces'; $this->valid = false; } } // Check for % signs, allow %pi %e, %i, %gamma, %phi but nothing else. if (strstr($cmd, '%') !== false) { $cmdl = strtolower($cmd); preg_match_all("(\\%.*)", $cmdl, $found); foreach ($found[0] as $match) { if (!(strpos($match, '%e') !== false || strpos($match, '%pi') !== false || strpos($match, '%i') !== false || strpos($match, '%j') !== false || strpos($match, '%gamma') !== false || strpos($match, '%phi') !== false)) { // Constants %e and %pi are allowed. Any other percentages dissallowed. $this->add_error(stack_string('stackCas_percent', array('expr' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); $this->answernote[] = 'percent'; $this->valid = false; } } } $inline = stack_utils::check_bookends($cmd, '(', ')'); if ($inline !== true) { // The method check_bookends does not return false. $this->valid = false; if ($inline == 'left') { $this->answernote[] = 'missingLeftBracket'; $this->add_error(stack_string('stackCas_missingLeftBracket', array('bracket' => '(', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } else { $this->answernote[] = 'missingRightBracket'; $this->add_error(stack_string('stackCas_missingRightBracket', array('bracket' => ')', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } } $inline = stack_utils::check_bookends($cmd, '{', '}'); if ($inline !== true) { // The method check_bookends does not return false. $this->valid = false; if ($inline == 'left') { $this->answernote[] = 'missingLeftBracket'; $this->add_error(stack_string('stackCas_missingLeftBracket', array('bracket' => '{', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } else { $this->answernote[] = 'missingRightBracket'; $this->add_error(stack_string('stackCas_missingRightBracket', array('bracket' => '}', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } } $inline = stack_utils::check_bookends($cmd, '[', ']'); if ($inline !== true) { // The method check_bookends does not return false. $this->valid = false; if ($inline == 'left') { $this->answernote[] = 'missingLeftBracket'; $this->add_error(stack_string('stackCas_missingLeftBracket', array('bracket' => '[', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } else { $this->answernote[] = 'missingRightBracket'; $this->add_error(stack_string('stackCas_missingRightBracket', array('bracket' => ']', 'cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } } if (!stack_utils::check_nested_bookends($cmd)) { $this->valid = false; $this->add_error(stack_string('stackCas_bracketsdontmatch', array('cmd' => stack_maxima_format_casstring($this->strings_replace($cmd, $strings))))); } if ($security == 's') { // Check for apostrophes if a student. if (strpos($cmd, "'") !== false) { $this->add_error(stack_string('stackCas_apostrophe')); $this->answernote[] = 'apostrophe'; $this->valid = false; } // Check new lines. if (strpos($cmd, "\n") !== false) { $this->add_error(stack_string('stackCas_newline')); $this->answernote[] = 'newline'; $this->valid = false; } } if ($security == 's') { // Check for bad looking trig functions, e.g. sin^2(x) or tan*2*x // asin etc, will be included automatically, so we don't need them explicitly. $triglist = array('sin', 'cos', 'tan', 'sinh', 'cosh', 'tanh', 'sec', 'cosec', 'cot', 'csc', 'coth', 'csch', 'sech'); $funlist = array('log', 'ln', 'lg', 'exp', 'abs', 'sqrt'); foreach (array_merge($triglist, $funlist) as $fun) { if (strpos($cmd, $fun . '^') !== false) { $this->add_error(stack_string('stackCas_trigexp', array('forbid' => stack_maxima_format_casstring($fun . '^')))); $this->answernote[] = 'trigexp'; $this->valid = false; break; } if (strpos($cmd, $fun . '[') !== false) { $this->add_error(stack_string('stackCas_trigparens', array('forbid' => stack_maxima_format_casstring($fun . '(x)')))); $this->answernote[] = 'trigparens'; $this->valid = false; break; } $opslist = array('*', '+', '-', '/'); foreach ($opslist as $op) { if (strpos($cmd, $fun . $op) !== false) { $this->add_error(stack_string('stackCas_trigop', array('trig' => stack_maxima_format_casstring($fun), 'forbid' => stack_maxima_format_casstring($fun . $op)))); $this->answernote[] = 'trigop'; $this->valid = false; break; } } } foreach ($triglist as $fun) { if (strpos($cmd, 'arc' . $fun) !== false) { $this->add_error(stack_string('stackCas_triginv', array('badinv' => stack_maxima_format_casstring('arc' . $fun), 'goodinv' => stack_maxima_format_casstring('a' . $fun)))); $this->answernote[] = 'triginv'; $this->valid = false; break; } } } // Only permit the following characters to be sent to the CAS. $cmd = trim($cmd); $allowedcharsregex = '~[^' . preg_quote(self::$allowedchars, '~') . ']~u'; // Check for permitted characters. if (preg_match_all($allowedcharsregex, $cmd, $matches)) { $invalidchars = array(); foreach ($matches as $match) { $badchar = $match[0]; if (!array_key_exists($badchar, $invalidchars)) { $invalidchars[$badchar] = $badchar; } } $this->add_error(stack_string('stackCas_forbiddenChar', array('char' => implode(", ", array_unique($invalidchars))))); $this->answernote[] = 'forbiddenChar'; $this->valid = false; } // Check for disallowed final characters, / * + - ^ £ # = & ~ |, ? : ;. $disallowedfinalcharsregex = '~[' . preg_quote(self::$disallowedfinalchars, '~') . ']$~u'; if (preg_match($disallowedfinalcharsregex, $cmd, $match)) { $this->valid = false; $a = array(); $a['char'] = $match[0]; $a['cmd'] = stack_maxima_format_casstring($this->strings_replace($cmd, $strings)); $this->add_error(stack_string('stackCas_finalChar', $a)); $this->answernote[] = 'finalChar'; } // Check for empty parentheses `()`. if (strpos($cmd, '()') !== false) { $this->valid = false; $this->add_error(stack_string('stackCas_forbiddenWord', array('forbid' => stack_maxima_format_casstring('()')))); $this->answernote[] = 'forbiddenWord'; } // Check for spurious operators. $spuriousops = array('<>', '||', '&', '..', ',,', '/*', '*/'); foreach ($spuriousops as $op) { if (substr_count($cmd, $op) > 0) { $this->valid = false; $a = array(); $a['cmd'] = stack_maxima_format_casstring($op); $this->add_error(stack_string('stackCas_spuriousop', $a)); $this->answernote[] = 'spuriousop'; } } // CAS strings may not contain // * reversed inequalities, i.e =< is not permitted in place of <=. // * chained inequalities 1<x<=3. if (strpos($cmd, '=<') !== false || strpos($cmd, '=>') !== false) { if (strpos($cmd, '=<') !== false) { $a['cmd'] = stack_maxima_format_casstring('=<'); } else { $a['cmd'] = stack_maxima_format_casstring('=>'); } $this->add_error(stack_string('stackCas_backward_inequalities', $a)); $this->answernote[] = 'backward_inequalities'; $this->valid = false; } else { if (!$this->check_chained_inequalities($cmd)) { $this->add_error(stack_string('stackCas_chained_inequalities')); $this->answernote[] = 'chained_inequalities'; $this->valid = false; } } // Commas not inside brackets either should be, or indicate a decimal number not // using the decimal point. In either case this is problematic. // For now, we just look for expressions with a comma, but without brackets. // [TODO]: improve this test to really look for unencapsulated commas. if (!(false === strpos($cmd, ',')) && !(!(false === strpos($cmd, '(')) || !(false === strpos($cmd, '[')) || !(false === strpos($cmd, '{')))) { $this->add_error(stack_string('stackCas_unencpsulated_comma')); $this->answernote[] = 'unencpsulated_comma'; $this->valid = false; } $this->check_stars($security, $syntax, $insertstars); $this->check_security($security, $allowwords); $this->key_val_split(); return $this->valid; }
/** * Checks the castext syntax is valid, no missing @'s, $'s etc * * @access public * @return bool */ private function validate() { if (strlen(trim($this->rawcastext)) > 64000) { // Limit to just less than 64kb. Maximum practical size of a post. (about 14pages). $this->errors = stack_string("stackCas_tooLong"); $this->valid = false; return false; } // Remove any comments from the castext. $this->trimmedcastext = stack_utils::remove_comments(str_replace("\n", ' ', $this->rawcastext)); if (trim($this->trimmedcastext) === '') { $this->valid = true; return true; } // Find reasons to invalidate the text... $this->valid = true; // Check @'s match. $amps = stack_utils::check_matching_pairs($this->trimmedcastext, '@'); if ($amps == false) { $this->errors .= stack_string('stackCas_MissingAt'); $this->valid = false; } // Dollars can be protected for use with currency. $protected = str_replace('\\$', '', $this->trimmedcastext); $dollar = stack_utils::check_matching_pairs($protected, '$'); if ($dollar == false) { $this->errors .= stack_string('stackCas_MissingDollar'); $this->valid = false; } $html = stack_utils::check_bookends($this->trimmedcastext, '<html>', '</html>'); if ($html !== true) { // The method check_bookends does not return false. $this->valid = false; if ($html == 'left') { $this->errors .= stack_string('stackCas_MissingOpenHTML'); } else { $this->errors .= stack_string('stackCas_MissingCloseHTML'); } } $inline = stack_utils::check_bookends($this->trimmedcastext, '\\[', '\\]'); if ($inline !== true) { // The method check_bookends does not return false. $this->valid = false; if ($inline == 'left') { $this->errors .= stack_string('stackCas_MissingOpenDisplay'); } else { $this->errors .= stack_string('stackCas_MissingCloseDisplay'); } } $inline = stack_utils::check_bookends($this->trimmedcastext, '\\(', '\\)'); if ($inline !== true) { // The method check_bookends does not return false. $this->valid = false; if ($inline == 'left') { $this->errors .= stack_string('stackCas_MissingOpenInline'); } else { $this->errors .= stack_string('stackCas_MissingCloseInline'); } } // Perform validation on the existing session. if (null != $this->session) { if (!$this->session->get_valid()) { $this->valid = false; $this->errors .= $this->session->get_errors(); } } // Now extract and perform validation on the CAS variables. // This does alot more than strictly "validate" the castext, but is makes sense to do all these things at once... $this->extract_cas_commands(); if (false === $this->valid) { $this->errors = '<span class="error">' . stack_string("stackCas_failedValidation") . '</span>' . $this->errors; } return $this->valid; }