is built by htmlSelect(), whose first argument is the field name. // Reading only the literal attributes would miss those and the test would // claim a field is not submitted when it is. function keys_offered($source) { preg_match_all('/name="newJob\[([A-Za-z]+)\]"/', $source, $m); preg_match_all("/htmlSelect\\('newJob\\[([A-Za-z]+)\\]'/", $source, $m2); $keys = array_unique(array_merge($m[1], $m2[1])); sort($keys); return $keys; } // Likewise for an id: htmlSelect() takes it in its behaviours array. function has_id($source, $id) { return (bool)(preg_match('/id="'.$id.'"/', $source) or preg_match("/'id'\\s*=>\\s*'".$id."'/", $source)); } echo "the modal sends every field the action reads\n"; $read = keys_read($action); $offered = keys_offered($modal); check('the action reads some fields', count($read) > 0, true); check('the modal offers some fields', count($offered) > 0, true); check('and the two sets are the same', $offered, $read); echo "the modal is a complete, self-submitting form\n"; // A form built over ajax is outside the page's own
, so everything it // needs has to be in the html that comes back. check('it posts to the cron view', (bool)preg_match('/action="\?view=cron"/', $modal), true); check('it posts rather than gets', (bool)preg_match('/method="post"/', $modal), true); // index.php dispatches on view, and the modal is not rendered inside the page // that would otherwise carry it. check('it carries the view', (bool)preg_match('/name="view" value="cron"/', $modal), true); // Without this the save is rejected as a forgery. getCSRFinputHTML() is put in // by hand because nothing else wraps an ajax-built form. check('it carries a csrf key', (bool)preg_match('/getCSRFinputHTML\(\)/', $modal), true); check('its submit button names the save action', (bool)preg_match('/name="action"[^>]*value="save"/', $modal), true); // Editing an existing task has to say which one, and adding has to say it is // not editing: the action treats an empty id as "insert". check('it carries the task id', (bool)preg_match('/name="id"/', $modal), true); echo "the schedule the server stores is the one that gets submitted\n"; // Three controls, one stored value. The pickers write into the cron field, // and it is the only one the server reads: the frequency select does post its // own value, but under its own name, which the action ignores. A picker that // gained a newJob[...] name would quietly become a second, competing answer // to the same question, and which one won would depend on field order. check('the cron field is submitted', (bool)preg_match('/id="scheduleSpec"[^>]*name="newJob\[Schedule\]"/s', $modal), true); check('and it is the only newJob field describing when', array_values(array_filter(keys_offered($modal), function($k) { return in_array($k, array('Schedule', 'Frequency', 'Hour', 'Minute', 'Time'), true); })), array('Schedule')); check('the time picker is not submitted at all', (bool)preg_match('/id="scheduleTime"[^>]*name=/s', $modal), false); // A schedule cron can express but the pickers cannot must survive an edit, so // the field is always present and never disabled. check('the cron field is required', (bool)preg_match('/id="scheduleSpec"[^>]*required/s', $modal), true); echo "the three picker ids the javascript looks for are the ones the modal uses\n"; // cron.js reads them by id after the modal html is inserted. Renaming one in // the modal would not error; the pickers would just stop filling the field in. preg_match_all("/getElementById\('(schedule[A-Za-z]+)'\)/", $js, $m); $wanted = array_unique($m[1]); sort($wanted); $missing = array(); foreach ($wanted as $id) { if (!has_id($modal, $id)) $missing[] = $id; } check('the javascript looks for three ids', count($wanted), 3); check('and the modal provides all of them', $missing, array()); echo "the list opens the modal and deletes\n"; // The name cell is the way in to editing, and the id has to travel with it. check('rows carry a task id for the modal', (bool)preg_match('/data-jid="/', $view), true); check('on the class the javascript binds', (bool)preg_match('/class="cronjobCol"/', $view), true); check('the javascript binds that class', (bool)preg_match("/\\\$j\('\.cronjobCol'\)/", $js), true); check('and asks for this modal by name', (bool)preg_match('/modal=cronjob/', $js), true); // The list's own form is the delete, and it must not also claim to save. check('the list form deletes', (bool)preg_match('/name="action" value="delete"/', $view), true); check('and nothing in the list claims to save', (bool)preg_match('/value="save"/', $view), false); check('marked rows post as markIds', (bool)preg_match('/name="markIds\[\]"/', $view), true); check('the action deletes those', (bool)preg_match("/\\\$_REQUEST\['markIds'\]/", $action), true); echo "the when row says one thing at a time\n"; // The three states of the schedule controls, checked by rendering the modal // rather than by reading its source: which picker is selected and whether the // cron field is open are decisions the php makes, and the source says only // that a decision happens. // // Rendering needs a handful of the web environment. It is stubbed rather than // loaded because the real thing wants a database and a readable zm.conf, and // none of what is being checked here depends on either. $SLANG = array(); require_once($root.'/web/lang/en_gb.php'); function translate($n) { global $SLANG; return isset($SLANG[$n]) ? $SLANG[$n] : $n; } function canEdit($p) { return true; } function validCardinal($i) { return (int)preg_replace('/\D/', '', (string)$i); } function validInt($i) { return (int)$i; } function validHtmlStr($i) { return htmlspecialchars((string)$i, ENT_QUOTES); } function getCSRFinputHTML() { return ''; } function htmlSelect($name, $contents, $values, $behaviours=false) { $attrs = ''; if (is_array($behaviours)) { foreach ($behaviours as $event=>$action) $attrs .= ' '.$event.'="'.$action.'"'; } $html = ''; } // One row, as the modal reads it. class StubJob { private $d; function __construct($d) { $this->d = $d; } function Id() { return $this->d['Id']; } function Name() { return $this->d['Name']; } function Command() { return $this->d['Command']; } function Schedule() { return $this->d['Schedule']; } function ServerId() { return $this->d['ServerId']; } function Enabled() { return $this->d['Enabled']; } } class StubServer { function Id() { return 2; } function Name() { return 'kitchen'; } } // The modal requires its dependencies by the relative paths the web root // provides, so they are satisfied from a throwaway include path. CronTab is // the real one -- cronToParts() is being exercised here, not replaced. $stub_dir = sys_get_temp_dir().'/zm_cron_modal_test_'.getmypid().'/includes'; if (!is_dir($stub_dir)) mkdir($stub_dir, 0700, true); file_put_contents($stub_dir.'/CronJob.php', '$id); ob_start(); include(__DIR__.'/../../web/ajax/modals/cronjob.php'); return ob_get_clean(); } // Which