view mgallery.inc.php @ 324:d598b2320878

Improvements and fixes to configuration handling.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 09 Apr 2020 13:09:01 +0300
parents 2f4e3e458714
children 782c1520984e
line wrap: on
line source

<?php
//
// Yet Another Image Gallery
// -- Common functions and data include
// Programmed and designed by Matti 'ccr' Hamalainen <ccr@tnsp.org>
// (C) Copyright 2015-2020 Tecnic Software productions (TNSP)
//

$mgProgVersion = "v0.9.12";
$mgProgInfo = "Programmed by Matti 'ccr' Hamalainen";
$mgProgEmail = "<ccr@tnsp.org>";
$mgProgCopyright = "2015-2020 Tecnic Software productions (TNSP)";

$mgProgConfigFile = "mgallery.cfg";

$mgalDebug = FALSE;


//
// Navigation control defines
//
define("MGF_JAVASCRIPT"      , 0x01);
define("MGF_BREADCRUMBS"     , 0x10);
define("MGF_CAPTIONS"        , 0x20);

//
// Constants for different value types
//
define("MG_STR"              , 1);
define("MG_STR_LC"           , 2);
define("MG_STR_ARRAY"        , 3);
define("MG_INT"              , 4);
define("MG_DVA"              , 5);
define("MG_BOOL"             , 6);
define("MG_FLAGS"            , 7);
define("MG_DATE"             , 8);


define("yes"                 , 1);
define("no"                  , 0);


$mgGFlags = [
  "javascript"               => MGF_JAVASCRIPT,
  "breadcrumbs"              => MGF_BREADCRUMBS,
  "captions"                 => MGF_CAPTIONS,
];


function mgPathName($path)
{
  $tmp = mgCleanPathArray(TRUE, 0, func_num_args(), func_get_args());
  if (count($tmp) > 0)
    return implode("/", array_splice($tmp, 0, -1))."/";
  else
    return $path;
}


//
// Configuration settings and their default values
//
$mgDefaults = [
  "timezone"         => [MG_STR, NULL],

  "clean_urls"       => [MG_BOOL, FALSE],
  "base_path"        => [MG_STR, mgPathName(mgRealPath($_SERVER["SCRIPT_FILENAME"]))],
  "base_url"         => [MG_STR, mgPathName($_SERVER["PHP_SELF"])],
  "image_url"        => [MG_STR, mgPathName($_SERVER["PHP_SELF"])],
  "mgallery_php"     => [MG_STR, ""],
  "format_exts"      => [MG_STR, "\.jpg|\.png|\.gif|\.jpeg|\.webp"],
  "captions_file"    => [MG_STR, "captions.txt"],
  "header_file"      => [MG_STR, "header.txt"],
  "info_file"        => [MG_STR, "mgallery.info"],
  "cache_file"       => [MG_STR, ".mgallery.cache"],

  "cover_images"     => [MG_BOOL, TRUE],
  "album_icon"       => [MG_STR, NULL],

  "title_prefix"     => [MG_STR, ""],
  "title_sep"        => [MG_STR, " - "],
  "page_info"        => [MG_STR, "Powered by <b>MGallery ".$mgProgVersion."</b> &copy; Copyright ".$mgProgCopyright],

  "css"              => [MG_STR_ARRAY, NULL],
  "js_file"          => [MG_STR_ARRAY, NULL],
  "urchin_file"      => [MG_STR_ARRAY, FALSE],

  "global_flags"     => [MG_FLAGS, MGF_JAVASCRIPT | MGF_BREADCRUMBS | MGF_CAPTIONS, &$mgGFlags],
  "image_flags"      => [MG_FLAGS, MGF_JAVASCRIPT | MGF_BREADCRUMBS, &$mgGFlags],
  "album_flags"      => [MG_FLAGS, MGF_JAVASCRIPT | MGF_BREADCRUMBS, &$mgGFlags],

  "tn_path"          => [MG_STR, "tn/"],
  "med_path"         => [MG_STR, "med/"],

  "tn_format"        => [MG_STR_LC, "jpeg"],
  "tn_width"         => [MG_INT, 140],  // In pixels, applies as bounding box for w/h
  "tn_height"        => [MG_INT, 100],
  "tn_quality"       => [MG_INT, 90],   // JPEG/WEBP quality percent

  "med_format"       => [MG_STR_LC, "jpeg"],
  "med_width"        => [MG_INT, 1200],
  "med_height"       => [MG_INT, 900],
  "med_quality"      => [MG_INT, 90],

  "backend"          => [MG_STR_LC, "php"],
  "sql_db"           => [MG_STR, FALSE],
  "sql_username"     => [MG_STR, ""],
  "sql_password"     => [MG_STR, ""],
  "sql_options"      => [MG_STR_ARRAY, []],
];


function mgDebug($msg)
{
  global $mgalDebug;
  if ($mgalDebug)
  {
    echo "<p>MGAL[debug]: ".htmlspecialchars($msg)."</p>";
    error_log("MGAL[debug]: ".$msg);
  }
}


function mgFatal($msg)
{
  die("MGAL[fatal]: ".$msg);
}


function mgError($msg)
{
  error_log("MGAL[error]: ".$msg);
  return FALSE;
}


function mgCArg($index, $clip = FALSE)
{
  global $argc, $argv;
  if ($index < $argc)
  {
    $str = $argv[$index];
    return ($clip !== FALSE) ? substr($str, 0, $clip) : $str;
  }
  else
    return FALSE;
}


function mgCArgLC($index, $clip = FALSE)
{
  global $argc, $argv;
  if ($index < $argc)
  {
    $str = strtolower($argv[$index]);
    return ($clip !== FALSE) ? substr($str, 0, $clip) : $str;
  }
  else
    return FALSE;
}


function mgGetSetting($key, $default = NULL)
{
  global $mgSettings, $mgDefaults;

  if (!array_key_exists($key, $mgDefaults))
    mgFatal("Setting '".$key."' does not exist.\n");

  if (array_key_exists($key, $mgSettings))
    $val = $mgSettings[$key];
  else
    $val = $mgDefaults[$key][1];

  if (!isset($val) || $val === NULL)
  {
    if ($default !== NULL)
      $val = $default;
    else
      mgFatal("Setting '".$key."' is not set, but is required to be configured.\n");
  }

  switch ($mgDefaults[$key][0])
  {
    case MG_STR_LC:
      $val = strtolower($val);
      break;

    case MG_FLAGS:
      if (is_string($val))
      {
        $flags = $mgDefaults[$key][2];
        $cval = preg_split("/\s*[,|]\s*/", strtolower($val), -1, PREG_SPLIT_NO_EMPTY);
        $nval = 0;
        foreach ($cval as $qval)
        {
          if (array_key_exists($qval, $flags))
            $nval |= $flags[$qval];
          else
            mgFatal("Invalid flag value for '".$key."': '".$qval."'.");
        }
      }
      break;
  }

  return $val;
}


function mgGetAlbumSetting(&$data, $key, $default = NULL)
{
  if (array_key_exists($key, $data))
    $val = $data[$key];
  else
    $val = mgGetSetting($key, $default);

  // XXX This is a rather silly place for this check, but since any album can
  // set their own formats, we can't do this check globally without scanning
  // all the sub-albums etc .. maybe we'll do that some day.
  if (($key == "tn_format" || $key == "med_format") &&
      $val == "webp" && version_compare(PHP_VERSION, "7.1.0") < 0)
  {
    mgFatal("WebP image format support requires PHP version 7.1 or later.\n");
  }

  return $val;
}


function mgGetPath($path, $key)
{
  $val = mgGetSetting($key);
  return ($val !== FALSE) ? $path."/".$val : FALSE;
}


function mgReadOneConfig(&$searchPaths, $pathList)
{
  global $mgSettings, $mgProgConfigFile;
  $found = FALSE;

  foreach (array_unique($pathList) as $path)
  {
    $filename = $path.$mgProgConfigFile;
    $searchPaths[] = $filename;

    if (!$found)
    {
      mgDebug("Checking '".$filename."' for configuration ..\n");

      if (file_exists($filename) &&
        ($data = parse_ini_file($filename, FALSE)) !== FALSE)
      {
        mgDebug("Found '".$filename."' config.\n");
        foreach ($data as $dkey => &$dval)
          $mgSettings[$dkey] = $dval;

        $found = TRUE;
      }
    }
  }

  return $found;
}


function mgReadSettings(&$searchPaths)
{
  global $mgSettings;
  $mgSettings = [];
  $searchPaths = [];

  // System-wide
  $ok = mgReadOneConfig($searchPaths, ["/etc/", "/usr/local/etc/"]);

  // User-specific
  $spaths = [];
  if (($tmp = getenv("HOME")) !== FALSE && strlen($tmp) > 0)
  {
    $spaths[] = $tmp."/.config/mgallery/";
    $spaths[] = $tmp."/.";
  }
  else
  {
    $data = posix_getpwuid(posix_getuid());
    if ($data !== FALSE && isset($data["dir"]))
    {
      $tmp = $data["dir"];
      $spaths[] = $tmp."/.config/mgallery/";
      $spaths[] = $tmp."/.";
    }
  }
  $ok |= mgReadOneConfig($searchPaths, $spaths);

  // Album/directory-local
  $ok |= mgReadOneConfig($searchPaths, [ dirname(__FILE__)."/", getcwd()."/" ]);

  $searchPaths = array_unique($searchPaths);
  return $ok;
}


function mgRealPath($path)
{
  return realpath($path);
}


function mgCleanPathStr($path)
{
  return str_replace("//", "/", $path);
}


function mgCleanPathArray($refs, $start, $argc, $argv)
{
  $path = [];
  $first = TRUE;
  for ($n = $start; $n < $argc; $n++)
  {
    foreach (explode("/", $argv[$n]) as $piece)
    {
      switch ($piece)
      {
        case ".":
        case "":
          if ($first)
            $path[] = $piece;
          break;

        case "..":
          if ($refs && count($path) > 0)
            array_pop($path);
          break;

        default:
          $path[] = $piece;
          break;
      }
      $first = FALSE;
    }
  }
  return mgCleanPathStr($path);
}


function mgCleanPath($refs)
{
  return implode("/", mgCleanPathArray($refs, 1, func_num_args(), func_get_args()));
}


function mgGetArr($data, $skeys, $sfmt1 = "%1", $sfmt2 = "", $func = NULL)
{
  global $pageLang;

  if (!is_array($skeys))
    $skeys = array($skeys);

  foreach ($skeys as $skey)
  if (!array_key_exists($skey, $data))
    return $sfmt2;

  $str = $sfmt1;
  for ($i = 1; $i <= sizeof($skeys); $i++)
  {
    $val = $data[$skeys[$i - 1]];
    if (is_array($val))
      $vtmp = array_key_exists($pageLang, $val) ? $val[$pageLang] : reset($val);
    else
      $vtmp = $val;

    if (is_callable($func))
      $vtmp = call_user_func($func, $vtmp);

    $str = str_replace("%".$i, $vtmp, $str);
  }

  return $str;
}


function mgLogSQLError($dbh, $sql)
{
  return mgError("SQL error '".implode("; ", $dbh->errorInfo())."' in statement: \"".$sql."\"");
}


function mgDBGetSQLParam($dbh, $type, $value)
{
  switch ($type)
  {
    case "d": return intval($value);
    case "s": return $dbh->quote($value);
    case "b": return intval($value) ? 1 : 0;
    case "D":
      if ($value instanceOf DateTime)
      {
        switch ($dbh->getAttribute(PDO::ATTR_DRIVER_NAME))
        {
          case "pgsql"   : $fmt = "Y-m-d H:i:sP"; break;
          case "sqlite"  : $fmt = DATE_RFC3339; break;
          case "mysql"   : $fmt = "Y-m-d H:i:s"; break;
          default        : $fmt = DATE_RFC3339;
        }
        return $dbh->quote($value->format($fmt));
      }
      else
        return intval($value);
  }
}


function mgSQLToDateTime($dbh, $stamp)
{
  switch ($dbh->getAttribute(PDO::ATTR_DRIVER_NAME))
  {
    case "pgsql":
      // PostgreSQL 'timestamptz' format
      $tmp =  DateTime::createFromFormat("Y-m-d H:i:sP", $stamp);
      break;

    case "sqlite":
      // SQLite can use RFC3339 format
      $tmp = DateTime::createFromFormat(DATE_RFC3339, $stamp);
      break;

    case "mysql":
      // MySQL uses UTC internally, no way to specify TZ
      $tmp = DateTime::createFromFormat("Y-m-d H:i:s", $stamp);
      break;

    default:
      $tmp = NULL;
  }

  //  echo "<p>".$stamp." :: ".var_export(($tmp instanceOf DateTime) ? $tmp : NULL, TRUE)."</p>";
  return ($tmp instanceOf DateTime) ? $tmp : NULL;
}


function mgConnectSQLDB()
{
  global $db;
  try {
    $db = new PDO(mgGetSetting("sql_db"),
      mgGetSetting("sql_username", NULL),
      mgGetSetting("sql_password", NULL),
      mgGetSetting("sql_options", array()));
  }
  catch (PDOException $e) {
    mgError("Could not connect to SQL database: ".$e->getMessage().".");
    return FALSE;
  }
  return ($db !== false);
}


function mgDBPrepareSQLUpdate($dbh, $table, $cond, $pairs, $values = NULL)
{
  $sql = [];
  foreach ($pairs as $name => $attr)
  {
    $sql[] = $name."=".mgDBGetSQLParam($dbh,
      $attr, $values !== NULL ? $values[$name] : $name);
  }
  return
    "UPDATE ".$table." SET ".implode(",", $sql).
    ($cond != "" ? " ".$cond : "");
}


function mgDBPrepareSQL_V($dbh, $fmt, $argv)
{
  $len = strlen($fmt);
  $sql = "";
  $argn = 0;
  $argc = count($argv);

  for ($pos = 0; $pos < $len; $pos++)
  {
    if ($fmt[$pos] == "%")
    {
      if ($argn < $argc)
        $sql .= mgDBGetSQLParam($dbh, $fmt[++$pos], $argv[$argn++]);
      else
      {
        mgError("Invalid SQL statement format string '".$fmt.
          "', not enough parameters specified (".$argn." of ".$argc.")");
        return FALSE;
      }
    }
    else
      $sql .= $fmt[$pos];
  }

  return $sql;
}


function mgDBPrepareSQL($dbh)
{
  $argv = func_get_args();
  return mgDBPrepareSQL_V($dbh, $argv[1], array_splice($argv, 2));
}


function mgPrepareSQL()
{
  global $db;
  $argv = func_get_args();
  return mgDBPrepareSQL_V($db, $argv[0], array_splice($argv, 1));
}


function mgDBExecSQLInsert($dbh, $sql)
{
  switch ($dbh->getAttribute(PDO::ATTR_DRIVER_NAME))
  {
    case "pgsql":
      if (($res = mgDBFetchSQLColumn($dbh, $sql." RETURNING id")) !== false)
        return $res;
      else
        return FALSE;

    default:
      if (mgDBExecSQL($dbh, $sql) !== false)
        return $dbh->lastInsertId();
      else
        return FALSE;
  }
}


function mgDBExecSQL($dbh, $sql)
{
  if (($res = $dbh->query($sql)) !== FALSE)
    return $res;
  else
  {
    mgLogSQLError($dbh, $sql);
    return FALSE;
  }
}


function mgDBFetchSQL($dbh, $sql)
{
  if (($res = $dbh->query($sql)) !== FALSE)
    return $res->fetch();
  else
  {
    mgLogSQLError($dbh, $sql);
    return FALSE;
  }
}


function mgDBFetchSQLColumn($dbh, $sql, $column = 0)
{
  if (($res = $dbh->query($sql)) !== FALSE)
    return $res->fetchColumn($column);
  else
  {
    mgLogSQLError($dbh, $sql);
    return FALSE;
  }
}


function mgPrepareSQLUpdate($table, $cond, $pairs)
{
  global $db;
  return mgDBPrepareSQLUpdate($db, $table, $cond, $pairs);
}


function mgExecSQLInsert($sql)
{
  global $db;
  return mgDBExecSQLInsert($db, $sql);
}


function mgExecSQL($sql)
{
  global $db;
  return mgDBExecSQL($db, $sql);
}


function mgFetchSQL($sql)
{
  global $db;
  return mgDBFetchSQL($db, $sql);
}


function mgFetchSQLColumn($sql, $column = 0)
{
  global $db;
  return mgDBFetchSQLColumn($db, $sql, $column);
}


function mgDBBeginTransaction($dbh = FALSE)
{
  global $db;
  return mgDBExecSQL(($dbh !== FALSE) ? $dbh : $db, "BEGIN TRANSACTION");
}


function mgDBCommitTransaction($dbh = FALSE)
{
  global $db;
  return mgDBExecSQL(($dbh !== FALSE) ? $dbh : $db, "COMMIT");
}


function mgDBGetTableSchema($dbh, $schema)
{
  $res = [];
  $driver = $dbh->getAttribute(PDO::ATTR_DRIVER_NAME);

  // Go through the table schema, definition by definition
  foreach ($schema as $scol)
  {
    $tmp = [];

    // And each element of the one definition
    // (like 'foo INTEGER AUTOINCREMENT')
    foreach ($scol as $elem)
    switch ($driver)
    {
      case "pgsql":
        switch ($elem)
        {
          case "AUTOINCREMENT":
            // For Postgres, use SERIAL for autoincrement and
            // "cleverly" replace the 2nd element with SERIAL
            // assuming that it is INTEGER or such.
            $tmp[1] = "SERIAL";
            break;

          case "DATETIME":
            $tmp[] = "TIMESTAMPTZ";
            break;

          default:
            $tmp[] = $elem;
            break;
        }
        break;

      case "mysql":
        switch ($elem)
        {
          case "AUTOINCREMENT":
            $tmp[] = "AUTO_INCREMENT";
            break;

          case "DATETIME":
            $tmp[] = "TIMESTAMP";
            break;

          default:
            $tmp[] = $elem;
            break;
        }
        break;

      case "sqlite":
        $tmp[] = $elem;
        break;

      default:
        die("Don't know how to handle PDO driver '".$driver."' yet.\n");
    }

    $res[] = implode(" ", $tmp);
  }

  return implode(", ", $res);
}


function mgDBCreateOneTable($dbh, $name, $schema)
{
  return (mgDBExecSQL($dbh, "CREATE TABLE IF NOT EXISTS ".$name." (".$schema.")") !== FALSE) ? TRUE : FALSE;
}

?>