public function test_check_nested_bookends()
 {
     $this->assertTrue(stack_utils::check_nested_bookends(''));
     $this->assertTrue(stack_utils::check_nested_bookends('x+1'));
     $this->assertTrue(stack_utils::check_nested_bookends('(sin(x)+1)'));
     $this->assertTrue(stack_utils::check_nested_bookends('[sin(x)+1]'));
     $this->assertTrue(stack_utils::check_nested_bookends('{}[]()'));
     $this->assertTrue(stack_utils::check_nested_bookends('{[()]}'));
     $this->assertTrue(stack_utils::check_nested_bookends('{[()(()[(){}((){})])]}'));
     $this->assertFalse(stack_utils::check_nested_bookends('('));
     $this->assertFalse(stack_utils::check_nested_bookends(')'));
     $this->assertFalse(stack_utils::check_nested_bookends('x+1)'));
     $this->assertFalse(stack_utils::check_nested_bookends('(sin(x+1)'));
     $this->assertFalse(stack_utils::check_nested_bookends('[sin(x]+1)'));
     $this->assertFalse(stack_utils::check_nested_bookends('{}[()'));
     $this->assertFalse(stack_utils::check_nested_bookends('{[()(()[(){}((){})]))]}'));
 }
 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;
 }