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;
 }