(.+)<\/h2>/", $content, $match); if (isset($match[1])) { // this removes any php from the title $title = preg_replace("//", "", $match[1]); // this removes any html from the title $title = preg_replace("/<.+?>/", "", $title); } // if we didn't get a title, use the moddir name if (!$title) { $title = $moddir; } # pull the version from the file $version = "v0.0"; # XXX this regex should stay in sync with update_version.pl on dev... preg_match("//", $content, $match); if (isset($match[1])) { $version = $match[1]; } # save info about this module $fsmods{ $moddir } = array( 'dir' => "$relModPath/$moddir", 'moddir' => $moddir, 'title' => $title, 'position' => 0, 'hidden' => false, 'fragment' => $fragment, 'version' => $version, ); } else { # save info about this incomplete module $fsmods{ $moddir } = array( 'dir' => "$relModPath/$moddir", 'moddir' => $moddir, 'title' => $moddir, 'position' => 0, 'hidden' => true, 'fragment' => false, 'version' => "", ); } } } closedir($handle); return $fsmods; } #------------------------------------------- # returns an associative array of modules from the # database - does not check the filesystem at all #------------------------------------------- function getmods_db() { $db = getdb(); $dbmods = array(); # opening the DB worked if ($db) { # get that db module list $rv = $db->query("SELECT * FROM modules"); while ($row = $rv->fetchArray(SQLITE3_ASSOC)) { $dbmods[$row['moddir']] = $row; } } return $dbmods; } #------------------------------------------- # get a database handle - also init the table if needed #------------------------------------------- function getdb() { # we need to keep a copy so we can close it in a callback later global $_db; # and also because caching per-request is smart if (isset($_db)) { return $_db; } # not already connected? connect. try { # this has to work from both cgi & random cli, # however it doesn't work if we're faking being # a rachelplus or rachelpi (search for "fake-rachel" below) $dbfile = getAbsAdminPath() . "/admin.sqlite"; $_db = new SQLite3($dbfile); # File could get created by webserver or cli script # and we want both to be able to use it. # Also the @ suppresses the error if we're not # the owner. @chmod($dbfile, 0666); } catch (Exception $ex) { error_log($ex->getMessage()); error_log("DB File: $dbfile"); return null; } # allow blocking for 10 seconds while other # processes are writing to the db $_db->busyTimeout(10000); # It seems a bit wasteful to do this every time we grab a db connection. # However it's probably better than trying to detect if each table # exists, in the case of a new install or an upgrade. $_db->exec("BEGIN"); $_db->exec(" CREATE TABLE IF NOT EXISTS modules ( module_id INTEGER PRIMARY KEY, moddir VARCHAR(255), title VARCHAR(255), position INTEGER, hidden INTEGER ) "); $_db->exec(" CREATE TABLE IF NOT EXISTS tasks ( task_id INTEGER PRIMARY KEY, moddir VARCHAR(255), command VARCHAR(255), pid INTEGER, stdout_tail TEXT, stderr_tail TEXT, started INTEGER, -- timestamp last_update INTEGER, -- timestamp completed INTEGER, -- timestamp dismissed INTEGER, -- timestamp retval INTEGER, files_done INTEGER, data_done INTEGER, data_rate VARCHAR(255) ) "); $_db->exec(" CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, username VARCHAR(255), password VARCHAR(255), CONSTRAINT username UNIQUE (username) ) "); $admin = $_db->querySingle("SELECT 1 FROM users WHERE username = 'admin'"); if (!$admin) { # insert default user/pass $_db->exec(" INSERT INTO users (username, password) VALUES ('admin', 'd54f4a435aca0ed313c2a7a0b9914d78') "); } $_db->exec(" CREATE TABLE IF NOT EXISTS prefs ( pref VARCHAR(255), value VARCHAR(255), CONSTRAINT pref UNIQUE (pref) ) "); $_db->exec("COMMIT"); return $_db; } #------------------------------------------- # If we don't do this, dangling filehandles build up # and after a while we can't open any more... yikes. #------------------------------------------- function cleanup() { global $_db; if (isset($_db)) { $_db->close(); unset($_db); } } register_shutdown_function('cleanup'); #------------------------------------------- # sort by db position, then alphabetically by moddir, # if there's no db position put alphabetically at top #------------------------------------------- function bypos($a, $b) { if (!isset($a['position'])) { $a['position'] = 0; } if (!isset($b['position'])) { $b['position'] = 0; } if ($a['position'] == $b['position']) { return strcmp(strtolower($a['moddir']), strtolower($b['moddir'])); } else { return $a['position'] - $b['position']; } } function available_langs() { $basedir = "lang"; $default_lang = "en"; # if there's no options, don't bother trying # -- this actually means we'll render with blanks # for all translated text -- fatal error? if (!is_dir($basedir)) { return array( $default_lang ); } # first we get a list of all languages available (from the lang directory) $available_langs = array(); $handle = opendir($basedir); while ($moddir = readdir($handle)) { if (preg_match("/^lang\.(..)\.php$/", $moddir, $matches)) { array_push($available_langs, $matches[1]); } } closedir($handle); # if there's one option, return it if (sizeof($available_langs) == 1) { return array( $available_langs[0] ); } return $available_langs; } # This function returns the language preferred by the # browser - out of the available languages. If the browser # wants a language we don't have, we just return "en". function browser_lang() { # we want the language codes as keys, not values $available_langs = array_flip(available_langs()); $browser_langs; # now we pull the languages from header if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { preg_match_all('~([\w-]+)(?:[^,\d]+([\d.]+))?~', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER); } else { # bad robot (probably) -- fake it to avoid errors $matches = array(array('en','en')); } #print("

" . $_SERVER['HTTP_ACCEPT_LANGUAGE'] . "

"); # this logic gets the qvalue for each language foreach($matches as $match) { list($a, $b) = explode('-', $match[1]) + array('', ''); $value = isset($match[2]) ? (float) $match[2] : 1.0; if(isset($available_langs[$match[1]])) { $browser_langs[$match[1]] = $value; continue; } # dialects (e.g. en-US) - don't overwrite if we've already got # a match on base language if(!isset($browser_langs[$a]) && isset($available_langs[$a])) { $browser_langs[$a] = $value - 0.1; } } # default to english if there's no match if (!is_array($browser_langs)) { return "en"; } # order them by q weight arsort($browser_langs); # return the first return key($browser_langs); } #------------------------------------------- # there's several things that can affect which langauge # gets displayed - this function figures all that out # (actually there's not any more, so this is very simple) #------------------------------------------- function getlang() { # if there was no user or admin setting use the browser's request return browser_lang(); } function authorized() { global $lang; # if they're running php from the command prompt, # that's considered authorized if (php_sapi_name() == "cli") { return true; } # if we've got a good cookie, return true if (isset($_COOKIE['rachel-auth']) && $_COOKIE['rachel-auth'] == "admin") { return true; # if we've got good user/pass, issue cookie } else if (isset($_POST['user']) && isset($_POST['pass'])) { $db = getdb(); $db_user = $db->escapeString(strtolower($_POST['user'])); $db_pass = $db->escapeString(md5($_POST['pass'])); $validuser = $db->querySingle( "SELECT * FROM users WHERE username = '$db_user' AND password = '$db_pass'" ); if ($validuser) { # we used to let the path be current directory, but then # we had cookies getting set that were difficult to unset # (if you don't know what directory they were set in, even # unsetting at "/" doesn't work) -- so now we set and # unset everything at the root setcookie("rachel-auth", "admin", 0, "/"); header( "Location: //$_SERVER[HTTP_HOST]" . strtok($_SERVER["REQUEST_URI"],'?') ); return true; } } # if we made it here it means they're not authorized # -- so give them a chance to log in $indexurl = getAbsBaseUrl(); print << Login
EOT; } #------------------------------------------- # what kind of RACHEL are we? # we have a couple /tmp/ files you can plop down # to get the admin interface to temporarily show you # what would come up for different devices # ...sadly this breaks db access, but we'll have # to figure that out later #------------------------------------------- define("RACHELPI_MODPATH", "/var/www/modules"); function is_rachelpi() { return is_dir(RACHELPI_MODPATH) || file_exists("/tmp/fake-rachelpi"); } define("RACHELPLUS_MODPATH", "/media/RACHEL/rachel/modules"); function is_rachelplus() { return is_dir(RACHELPLUS_MODPATH) || file_exists("/tmp/fake-rachelplus"); } function is_rachelplusv3() { return is_dir("/.data/RACHEL") || file_exists("/tmp/fake-rachelplusv3"); } #------------------------------------------- # gets the absolute module path on any machine # this should work from any directory so install # scripts can call it and find the right place #------------------------------------------- function getAbsModPath() { if (is_rachelplus()) { return RACHELPLUS_MODPATH; } if (is_rachelpi()) { return RACHELPI_MODPATH; } # other system (from webroot) if (file_exists("./modules")) { return realpath("./modules"); } # other (from admin dir) if (file_exists("../modules")) { return realpath("../modules"); } # unknown return false; } function getAbsAdminPath() { return preg_replace("/modules/", "admin", getAbsModPath()); } #------------------------------------------- # Sometimes we want the module directory # relative from the current directory instead. # This should work through HTTP or command line #------------------------------------------- function getRelModPath() { if (isset($_SERVER['REQUEST_URI'])) { $me = $_SERVER['REQUEST_URI']; } else { $me = __FILE__; } if (basename(dirname($me)) == "admin") { return "../modules"; } else { return "modules"; } } function getRelAdminPath() { return preg_replace("/modules/", "admin", getRelModPath()); } #------------------------------------------- # Get the absolute URL from the current location # to the root RACHEL directory, presumably where # index.php is residing #------------------------------------------- function getAbsBaseUrl() { # we avoid just using "/" here because in development # our code is sometimes in a subdirectory of the server $baseurl = dirname($_SERVER['REQUEST_URI']); $baseurl = preg_replace("/\/admin.*/", "/", $baseurl); $baseurl = preg_replace("/\/modules.*/", "/", $baseurl); // safe? return $baseurl; } #------------------------------------------- # this function updates the database to match the modules that # are in the filesystem #------------------------------------------- function syncmods_fs2db() { # get info on the modules in the filesystem $fsmods = getmods_fs(); # get info on the modules in the database $dbmods = getmods_db(); $db = getdb(); if ($db) { $db->exec("BEGIN"); # insert anything we found in the fs that wasn't in the db foreach (array_keys($fsmods) as $moddir) { if (!isset($dbmods[$moddir])) { $db_moddir = $db->escapeString($moddir); $db_title = $db->escapeString($fsmods[$moddir]['title']); $db_position = $db->escapeString($fsmods[$moddir]['position']); $db->exec( "INSERT into modules (moddir, title, position, hidden) " . "VALUES ('$db_moddir', '$db_title', '$db_position', '0')" ); #error_log("INSERT into modules (moddir, title, position, hidden) " . # "VALUES ('$db_moddir', '$db_title', '$db_position', '0')"); } } # delete anything from the db that wasn't in the fs foreach (array_keys($dbmods) as $moddir) { if (!isset($fsmods[$moddir])) { $db_moddir = $db->escapeString($moddir); $db->exec("DELETE FROM modules WHERE moddir = '$db_moddir'"); #error_log("DELETE FROM modules WHERE moddir = '$db_moddir'"); } } $db->exec("COMMIT"); } } #------------------------------------------- # Read in a .modules file and return a sorted arrray # of the modules that should be installed and an # associative array of modules that should be hidden. # So use it like this: # # list($sorted, $hidden) = parseModulesFile($file); # # The file format is just a list of module names, # one per line, in the order you want them to appear. # Blank lines and lines starting with a "#" are ignored; # lines starting with a "." are installed but hidden. # # We just die on any error: can't read file, malformed file #------------------------------------------- function parseModulesFile($file) { $fh = fopen($file, "r"); if (!$fh) { error_log("$file could not be opened"); exit(1); } $hidden = array(); $sorted = array(); while (($line = fgets($fh)) !== false) { # remove all whitespace $line = preg_replace("/\s+/", "", $line); # skip comments and blank lines if (preg_match("/^#/", $line) || !preg_match("/\S/", $line)) { continue; } # detect screwy files and bail # (module names can only be letters, numbers, underscore, hyphen, and dot) if (preg_match("/[^\w\.\-]/", $line)) { error_log("$file does not look like a valid .modules file"); exit(1); } # flag hidden items in an associative array if (preg_match("/^\./", $line)) { $line = preg_replace("/^\./", "", $line); $hidden[$line] = 1; } # put all items in an ordered array, even # hidden items, because we will still want to # install and sort them array_push($sorted, $line); } return array($sorted, $hidden); } #------------------------------------------- # Installs sorts, and sets visibility based on a # .modules file -- must work from the command line # or the html admin interface #------------------------------------------- function installmods($file, $install_server) { list($sorted, $hidden) = parseModulesFile($file); if (!$install_server) { error_log("Missing install_server argument to installmods() in common.php"); exit(1); } # where are we putting the installed modules? # (should we use getRelModDir instead? should we # replace the code there with this? testing needed # under different dirs, http, command line, etc.) $destdir = dirname(dirname(__FILE__)) . "/modules/"; # use rsync -z for remote hosts, not for LAN # (the CPU overhead of zip actually slows it down on a fast network) $zip = "z"; if (preg_match("/^[\d\.]+$/", $install_server)) { $zip = ""; } try { $db = getdb(); if (!$db) { throw new Exception($db->lastErrorMsg); } # this is a performance enhancing command, more than for safety $db->exec("BEGIN"); foreach ($sorted as $moddir) { $cmd = "rsync -Pav$zip rsync://$install_server/rachelmods/$moddir $destdir"; # insert a task into the DB $db_cmd = $db->escapeString($cmd); $db_moddir = $db->escapeString($moddir); $db->exec(" INSERT INTO tasks (moddir, command) VALUES ('$db_moddir', '$db_cmd') "); } # after all the modules are in the task queue to be installed, # we add a call to the sort script -- since this may not run # for a while we copy the file to a tmp file for later... # get unique name $mfile = uniqid("/tmp/sortmods-", true); # copy the .modules file to that unique name copy($file, $mfile); $sortscript = dirname(getAbsModPath()) . "/sortmods.php"; $db_cmd = $db->escapeString("php $sortscript $mfile"); # insert a sort task into the DB $db->exec(" INSERT INTO tasks (moddir, command) VALUES ('.modules sort', '$db_cmd') "); } catch (Exception $ex) { $db->exec("ROLLBACK"); error_log($ex); exit(1); } $db->exec("COMMIT"); # finally, we fire off our clever database updating rsync process $script = dirname(__FILE__) . "/do_tasks.php"; exec("php $script > /dev/null 2>&1 &"); } #------------------------------------------- # Call this to sort modules based on a .modules file #------------------------------------------- function sortmods($file) { list($sorted, $hidden) = parseModulesFile($file); _sortmods($sorted, $hidden); } #------------------------------------------- # Internal use - actually does the sorting/hiding work. # The instructions come from a .modules file # as parsed by parseModulesFile() -- modules that aren't # seen at all are hidden and sorted last, but not removed. #------------------------------------------- function _sortmods($sorted, $hidden) { # before we sort anything, let's make sure all the modules # are actually recorded in the DB syncmods_fs2db(); try { $db = getdb(); if (!$db) { throw new Exception($db->lastErrorMsg); } $db->exec("BEGIN"); # boink everything to the bottom and hide it $res = $db->exec("UPDATE modules SET position = '9999', hidden = '1'"); if (!$res) { throw new Exception($db->lastErrorMsg()); } # set the new order and new hidden state $db_position = 1; foreach ($sorted as $moddir) { $db_moddir = $db->escapeString($moddir); if (isset($hidden[$moddir])) { $db_is_hidden = 1; } else { $db_is_hidden = 0; } $rv = $db->exec(" UPDATE modules SET position = '$db_position', hidden = '$db_is_hidden' WHERE moddir = '$db_moddir' "); if (!$rv) { throw new Exception($db->lastErrorMsg()); } ++$db_position; } } catch (Exception $ex) { $db->exec("ROLLBACK"); error_log($ex); exit(1); } $db->exec("COMMIT"); } function showip () { global $lang; #------------------------------------------- # this is done as a function to enforce scope on $output #------------------------------------------- # some notes to prevent future regression: # the PHP suggested gethostbyname(gethostname()) # brings back the unhelpful 127.0.0.1 on RPi systems, # as well as slowing down some Windows installations # with a DNS lookup. $_SERVER["SERVER_ADDR"] will just # display what's in the user's address bar, so also # not useful - using ifconfig/ipconfig is the way to go, # but requires system-specific tweaking #------------------------------------------- if(is_rachelpi()){ exec("/sbin/ifconfig", $output); preg_match("/(?:eth0|enp2s0).+?inet (.+?) /", join("", $output), $match); if (isset($match[1])) { echo "LAN: $match[1]\n"; } preg_match("/(?:wlan0|br\-lan).+?inet (.+?) /", join("", $output), $match); if (isset($match[1])) { echo "
WIFI: $match[1]\n"; } return; } if (preg_match("/^win/i", PHP_OS)) { # under windows it's ipconfig # (though we're making windows static-only now) $output = shell_exec("ipconfig"); preg_match("/IPv4 Address.+?: (.+)/", $output, $match); if (isset($match[1])) { echo "$lang[server_address]: $match[1]\n"; } } else if (preg_match("/^darwin/i", PHP_OS)) { # OSX is unix, but it's a little different exec("/sbin/ifconfig", $output); preg_match("/en0.+?inet (.+?) /", join("", $output), $match); if (isset($match[1])) { echo "$lang[server_address]: $match[1]\n"; } } else { # most likely linux based - so ifconfig should work # eth0/wlan for Rpi, CAP1&2, enp2s0/br-lan for CAP3 exec("/sbin/ifconfig", $output); preg_match("/(?:eth0|enp2s0).+?inet addr:(.+?) /", join("", $output), $match); if (isset($match[1])) { echo "LAN: $match[1]\n"; } preg_match("/(?:wlan0|br\-lan).+?inet addr:(.+?) /", join("", $output), $match); if (isset($match[1])) { echo "
WIFI: $match[1]\n"; } } } # restart kiwix so it sees what modules are visible/hidden function kiwix_restart() { if (is_rachelplus()){ exec("sudo bash /root/rachel-scripts/rachelKiwixStart.sh"); } else if (is_rachelpi()){ #exec("sudo /usr/bin/perl /var/kiwix/bin/rachel-kiwix-start.pl"); } } function show_local_content_link() { $db = getdb(); $rv = $db->querySingle("SELECT 1 FROM prefs WHERE pref = 'show_local_content_link' AND value = '1'"); return $rv; } function run_rsyncd() { $db = getdb(); $rv = $db->querySingle("SELECT 1 FROM prefs WHERE pref = 'run_rsyncd' AND value = '1'"); return $rv; } ?>