$t[1], 'line' => $t[2], 'id' => $t[0]); } else { $out[] = array('text' => $t, 'line' => null, 'id' => null); } } // carry the last known line onto punctuation $line = 0; foreach ($out as $i => $t) { if ($t['line'] !== null) $line = $t['line']; else $out[$i]['line'] = $line; } return $out; } // Every call to $name, with its arguments split at top level. function zm_calls($tokens, $name) { $calls = array(); for ($i = 0; $i < count($tokens); $i++) { if ($tokens[$i]['id'] !== T_STRING || $tokens[$i]['text'] !== $name) continue; if (!isset($tokens[$i + 1]) || $tokens[$i + 1]['text'] !== '(') continue; // a method call is not what we are looking for if ($i > 0 && in_array($tokens[$i - 1]['text'], array('->', '::'), true)) continue; $depth = 0; $args = array(); $cur = ''; for ($j = $i + 1; $j < count($tokens); $j++) { $tx = $tokens[$j]['text']; if ($tx === '(') { $depth++; if ($depth === 1) continue; } elseif ($tx === ')') { $depth--; if ($depth === 0) { $args[] = trim($cur); break; } } elseif ($tx === ',' && $depth === 1) { $args[] = trim($cur); $cur = ''; continue; } $cur .= $tx; } $calls[] = array('line' => $tokens[$i]['line'], 'args' => $args); } return $calls; } // Line range of the block guarded by if ($exportStructure != 'flat'). function zm_non_flat_block($tokens) { for ($i = 0; $i < count($tokens); $i++) { if ($tokens[$i]['id'] !== T_IF) continue; // scan the condition $depth = 0; $sawVar = false; $sawNe = false; $sawFlat = false; $j = $i + 1; for (; $j < count($tokens); $j++) { $tx = $tokens[$j]['text']; if ($tx === '(') { $depth++; continue; } if ($tx === ')') { $depth--; if ($depth === 0) { $j++; break; } continue; } if ($tokens[$j]['id'] === T_VARIABLE && $tx === '$exportStructure') $sawVar = true; if ($tokens[$j]['id'] === T_IS_NOT_EQUAL || $tokens[$j]['id'] === T_IS_NOT_IDENTICAL) $sawNe = true; if ($tokens[$j]['id'] === T_CONSTANT_ENCAPSED_STRING && trim($tx, "'\"") === 'flat') $sawFlat = true; } if (!($sawVar && $sawNe && $sawFlat)) continue; if (!isset($tokens[$j]) || $tokens[$j]['text'] !== '{') continue; $start = $tokens[$j]['line']; $bd = 0; for ($k = $j; $k < count($tokens); $k++) { if ($tokens[$k]['text'] === '{') $bd++; elseif ($tokens[$k]['text'] === '}') { $bd--; if ($bd === 0) return array($start, $tokens[$k]['line']); } } } return null; } // ---- what a bare mkdir actually leaves behind --------------------------- $base = sys_get_temp_dir().'/zm_export_perm_'.getmypid(); @mkdir($base); $old_umask = umask(0022); $bare = $base.'/bare'; @mkdir($bare); $bare_mode = fileperms($bare) & 0777; check('a bare mkdir under the usual umask is world readable', $bare_mode === 0755, sprintf('got mode %04o, want 0755', $bare_mode)); // This half exercises PHP's own chmod, not ZoneMinder: exportEvents() needs a // database, config constants and exec(), so it cannot be called from here. It // documents the intended mode, it is not behavioural coverage of the export. $fixed = $base.'/fixed'; @mkdir($fixed); chmod($fixed, 0700); $fixed_mode = fileperms($fixed) & 0777; check('an explicit chmod keeps it to the owner', $fixed_mode === 0700, sprintf('got mode %04o, want 0700', $fixed_mode)); umask($old_umask); @rmdir($bare); @rmdir($fixed); @rmdir($base); // ---- source guards ------------------------------------------------------ $path = $root.'/web/skins/classic/includes/export_functions.php'; $src = file_get_contents($path); $tokens = zm_tokens($src); $mkdirs = zm_calls($tokens, 'mkdir'); $chmods = zm_calls($tokens, 'chmod'); check('the export path still creates directories', count($mkdirs) > 0); // Pair by the actual first argument, whatever form the mkdir takes. An // unreadable argument counts as unpaired rather than being skipped. $unpaired = array(); $wrong_mode = array(); foreach ($mkdirs as $mk) { $target = isset($mk['args'][0]) ? $mk['args'][0] : ''; if ($target === '') { $unpaired[] = $mk['line'].': (unreadable argument)'; continue; } $paired = null; foreach ($chmods as $ch) { if (isset($ch['args'][0]) && $ch['args'][0] === $target) { $paired = $ch; break; } } if ($paired === null) { $unpaired[] = $mk['line'].': '.$target; continue; } $mode = isset($paired['args'][1]) ? $paired['args'][1] : ''; if ($mode !== '0700') $wrong_mode[] = $paired['line'].': '.$target.' set to '.($mode === '' ? '(none)' : $mode); } check('every mkdir in the export path chmods that same directory', count($unpaired) === 0, 'unpaired: '.implode('; ', $unpaired)); check('and sets it to 0700, not something the world can read', count($wrong_mode) === 0, implode('; ', $wrong_mode)); // The per-event directory belongs to the non-flat structure. A flat export // copies into $export_dir directly and never touches it, so creating it there, // and skipping the event when that fails, would drop events that export fine. $block = zm_non_flat_block($tokens); check('the export still branches on a non-flat structure', $block !== null); if ($block !== null) { $inside = false; foreach ($mkdirs as $mk) { if (isset($mk['args'][0]) && $mk['args'][0] === '$event_dir' && $mk['line'] > $block[0] && $mk['line'] < $block[1]) { $inside = true; } } check('the per-event directory is only created inside that branch', $inside, sprintf('non-flat block spans lines %d-%d; mkdir($event_dir) at %s', $block[0], $block[1], implode(',', array_map(function($m) { return $m['line']; }, array_filter($mkdirs, function($m) { return ($m['args'][0] ?? '') === '$event_dir'; }))))); } print $failures ? "\n$failures failed\n" : "\nall passed\n"; exit($failures ? 1 : 0);