* @license BSD */ namespace Domframework; /** Manage the configurations of the module done by administrator in a config * file * It is based on the module configuration defaults * The DocType allow to define the configuration HTML page. It must contains * @param : the name of the parameter * @description : the textual description of the parameter * @type : the type of the parameter (string, integer, array) * @values : an array containing the allowed values to the parameter. Can be * a function like timezone_identifiers_list * @default : the default value (can be an array or a string or a number) * @group : Group all the parameters in a group * POC : * $config = new config(); * $config->default = array ("param"=>"default", * "param2"=>array (1,2,3), * "param3"=>null); * $var = $config->get ("param"); */ class config { /** All the parameters allowed with their default value */ public $default = array (); /** Use the .php to protect the information */ public $confFile = null; /** Select the configuration file */ public function selectConfFile () { if ($this->confFile !== null) $this->confFile = $this->confFile; elseif (defined("CONFIGFILE")) $this->confFile = CONFIGFILE; elseif (file_exists ("./datas/configuration.php")) $this->confFile = "./datas/configuration.php"; elseif (file_exists ("./data/configuration.php")) $this->confFile = "./data/configuration.php"; else $this->confFile = "./data/configuration.php"; } /** List all the parameters configurable in the software */ public function params () { $this->selectConfFile (); return $this->default; } /** Return the defined values in array format */ public function getAll () { $this->selectConfFile (); $conf = array (); $rc = include ($this->confFile); if ($rc !== 1) throw new Exception ("Error in configuration file", 500); return $conf; } /** Return the defined values in Slash format */ public function getAllInSlash () { $conf = $this->getAll (); $vals = array (); foreach ($this->docComment () as $key) { if (! key_exists ("param", $key)) continue; if (substr ($key["param"], 0, 1) !== "/") throw new \Exception ("Config param '".$key["param"]. "' don't start by /"); $exp = explode ("/", $key["param"]); unset ($exp[0]); if (count ($exp) === 1 && key_exists ($exp[1], $conf)) $vals[$key["param"]] = $conf[$exp[1]]; elseif (count ($exp) === 2 && key_exists ($exp[1], $conf) && key_exists ($exp[2], $conf[$exp[1]])) $vals[$key["param"]] = $conf[$exp[1]][$exp[2]]; elseif (count ($exp) === 3 && key_exists ($exp[1], $conf) && key_exists ($exp[2], $conf[$exp[1]]) && key_exists ($exp[3], $conf[$exp[1]][$exp[2]])) $vals[$key["param"]] = $conf[$exp[1]][$exp[2]][$exp[3]]; } return $vals; } /** Get the value of the provided parameter recorded in .php file * If it is not set in .php file, use the default value * @param string $param The option name to be returned */ public function get ($param) { $this->selectConfFile (); if (!array_key_exists ($param, $this->default)) throw new Exception ("Unknown configuration parameter '$param'", 500); if (!file_exists ($this->confFile)) { if (@file_put_contents ($this->confFile, "confFile), 500); } elseif (! is_readable ($this->confFile)) throw new Exception (sprintf ( dgettext ("domframework", "The configuration file '%s' is not readable"), $this->confFile)); $conf = array (); $rc = include ($this->confFile); if ($rc !== 1) throw new Exception ("Error in configuration file", 500); if (! array_key_exists ($param, $this->default)) throw new Exception (sprintf ("Configuration parameter '%s' not defined", $param), 500); // Create a conf where all the keys are defined. If the keys are already // define, use them, or use the default ones // Don't allow keys not defined in default ones if (! is_array ($this->default[$param])) { if (! array_key_exists ($param, $conf)) return $this->default[$param]; return $conf[$param]; } foreach ($this->default[$param] as $key=>$val) { if ($key === "not configured") { if (! array_key_exists ($param, $conf)) continue; foreach ($conf[$param] as $k=>$v) { $conf[$param][$k] = array_replace_recursive ($val, $conf[$param][$k]); } continue; } if (! isset ($conf[$param][$key])) $conf[$param][$key] = $val; elseif (is_array ($val)) $conf[$param][$key] = array_replace_recursive ($val, $conf[$param][$key]); } if (! array_key_exists ($param, $conf)) return $this->default[$param]; return $conf[$param]; } /** Define a value for the parameter in the config file. Add all the default * values if they are not defined * @param string $param The option name * @param mixed $value The option value * @return TRUE if the parameter is saved or an exception */ public function set ($param, $value) { $this->selectConfFile (); if (!array_key_exists ($param, $this->default)) throw new Exception ("Unknown parameter '$param'", 500); if (!file_exists ($this->confFile)) { if (@file_put_contents ($this->confFile, "confFile)); } elseif (! is_readable ($this->confFile)) throw new Exception (sprintf ( dgettext ("domframework", "The configuration file '%s' is not readable"), $this->confFile), 500); if (!is_writeable ($this->confFile)) throw new Exception (sprintf (dgettext ("domframework", "Configuration file '%s' is write protected"), $this->confFile), 500); $conf = array (); $rc = include ($this->confFile); if ($rc !== 1) throw new Exception (dgettext ("domframework", "Error in configuration file"), 500); $newconf = array_merge ($this->default, $conf, array ($param=>$value)); $txt = "writePHP ($newconf, $txt, 4); $txt .= ");\r\n"; if (@file_put_contents ($this->confFile, $txt, LOCK_EX) === FALSE) throw new Exception (sprintf (dgettext ("domframework", "Can't save configuration file '%s'"), $this->confFile), 500); return TRUE; } /** Display the $values in PHP format to be "require" easily * @param mixed $values Values to be recorded * @param string $phpcode Actual value of the php code * @param integer $indent Number of spaces in the indentation of config */ private function writePHP ($values, $phpcode, $indent) { foreach ($values as $key=>$val) { $phpcode .= str_pad (" ", $indent); $phpcode .= (is_numeric ($key)) ? $key : "\"$key\""; $phpcode .= " => "; if (is_bool ($val)) { if ($val === FALSE) $phpcode .= "FALSE,\r\n"; else $phpcode .= "TRUE,\r\n"; } elseif (is_null ($val)) $phpcode .= "NULL,\r\n"; elseif (is_int ($val) || is_float ($val)) $phpcode .= "$val,\r\n"; elseif (is_string ($val)) $phpcode .= "\"$val\",\r\n"; elseif (is_array ($val)) { $phpcode .= "array (\r\n"; $phpcode = $this->writePHP ($val, $phpcode, $indent+4); $phpcode .= str_pad (" ", $indent); $phpcode .= "),\r\n"; } else throw new Exception (dgettext ("domframework", "Config : missing type ").gettype ($val), 500); } return $phpcode; } /** Convert a string to the right PHP type, without using the eval function * @param string $values The string to convert */ private function strToType ($values) { $values = str_replace ("array (", "array(", $values); if (stripos ($values, "array(") !== false) { $values = substr ($values, 6, -1); $values = explode (",", $values); $new = array (); foreach ($values as $key=>$val) { $val = trim ($val); if (strpos ($val, "=>") !== false) { // Associated array unset ($values[$key]); list ($key1, $val1) = explode ("=>", $val); $key1 = trim ($key1); $val1 = trim ($val1); if ($val1[0] === "\"" || $val1[0] === "'") $val1 = substr($val1, 1, -1); elseif (strpos ($val1, ".")) $val1 = floatval ($val1); else $val1 = intval ($val1); $new[$key1] = $val1; } else { // Unique value (string or integer or float) if ($val[0] === "\"" || $val[0] === "'") $val = substr($val, 1, -1); elseif (strpos ($val, ".")) $val = floatval ($val); else $val = intval ($val); $new[$key] = $val; } } } else { if ($values[0] === "\"" || $values[0] === "'" ) $new = substr ($values, 1, -1); elseif (strpos ($values, ".")) $new = floatval ($values); elseif ($values === "null") $new = null; elseif ($values === "false") $new = "false"; elseif ($values === "true") $new = "true"; else $new = intval ($values); } return $new; } /** Update the configuration file with the provided parameters * @param array $paramsSlash The parameters to analyze for updating the file * The params are in an array like array ("/authentication/html" => "value") * All the parameters are not needed * To remove a parameter, set it to default value */ public function updateParamsSlash ($paramsSlash) { $debug = 0; if ($debug) echo "
";
    if ($debug) echo "########## BEFORE\n";
    if ($debug) echo "PARAMSLASH=";
    if ($debug) var_dump ($paramsSlash);
    // Check if the provided slash parameters are defined
    $definitions = $this->docComment ();
    // Actually stored values
    $stored = $this->getAllInSlash ();
    if ($debug) echo "stored =";
    if ($debug) var_dump ($stored);
    $new = array ();
    // Update the stored values with the provided ones
    foreach ($definitions as $key=>$unused)
    {
      if ($debug) echo "CHECK $key\n";
      if (! key_exists ($key, $paramsSlash) && key_exists ($key, $stored))
      {
        if ($debug) echo "COPY the OLD value fo '$key' to the new array\n";
        $new[$key] = $this->cast ($stored[$key]);
        continue;
      }
      if (! key_exists ($key, $paramsSlash))
      {
        if ($debug) echo "Param $key not provided : skipped\n";
        continue;
      }
      $val = $paramsSlash[$key];
      if ($val === "")
      {
        if ($debug) echo "MUST REMOVE $key => Empty val\n";
      }
      // Do NOT use the === as the $val is always a string and must be compared
      // with default integers
      elseif (key_exists ("default", $definitions[$key]) &&
              $this->cast ($val) ===
                $this->cast ($definitions[$key]["default"]))
      {
        if ($debug) echo "MUST REMOVE $key => default\n";
      }
      elseif (key_exists ($key, $paramsSlash) && key_exists ($key, $stored) &&
          $paramsSlash[$key] === $stored[$key])
      {
        if ($debug) echo "MUST COPY $key\n";
        $new[$key] = $this->cast ($val);
      }
      elseif (key_exists ($key, $stored) && $stored[$key] !== $val)
      {
        if ($debug) echo "MUST OVERWRITE $key WITH NEW VALUE\n";
        $new[$key] = $this->cast ($val);
      }
      elseif (! key_exists ($key, $stored))
      {
        if ($debug) echo "MUST ADD $key without DEFAULT\n";
        $new[$key] = $this->cast ($val);
      }
      else
      {
        if ($debug) echo "NOT CATCHED !!!\n";
        if ($debug) echo "   STORED=";var_dump ($stored);
        if ($debug) echo "   DEFS=";var_dump ($definitions[$key]);
        if ($debug) echo "   SLASH=";var_dump ($val);
      }
    }

    // Save the new stored values in the file
    $txt = "$val)
    {
      $exp = explode ("/", substr ($key, 1));
      $txt .= "\$conf['".implode ("']['", $exp)."'] = ".
        var_export ($val, true).";\n";
    }
    if (file_put_contents ($this->confFile, $txt, LOCK_EX) === FALSE)
      throw new \Exception (sprintf (dgettext ("domframework",
                                    "Can't save configuration file '%s'"),
                                    $this->confFile), 500);
    // The next command clear the include cache file and force the PHP to
    // read it again on next request.
    // If it is not used, include () return the old value
    opcache_invalidate ($this->confFile);
    return TRUE;
  }

  /** Cast the provided string into the associated value
    * @param string $val The val to cast
    */
  private function cast ($val)
  {
    if (isset ($val[0]) && ($val[0] === "\"" || $val[0] === "'" ))
      $new = substr ($val, 1, -1);
    elseif ($val === "null")
      $new = null;
    elseif ($val === "false")
      $new = false;
    elseif ($val === "true")
      $new = true;
    elseif ($val === "")
      $new = "";
    elseif (is_array ($val))
      $new = $val;
    elseif (strspn ($val, "0123456789") === strlen ($val))
      $new = intval ($val);
    elseif (strspn ($val, "0123456789.") === strlen ($val))
      $new = floatval ($val);
    else
      $new = $val;
    return $new;
  }

  /** Return an array containing the definitions read from the default config
    * file
    * The definition of param are in Slash format
    */
  public function docComment ()
  {
    $debug = 0;
    $reflector = new ReflectionClass (get_class ($this));
    $modelFile = $reflector->getFileName();
    if (! file_exists ($modelFile))
      throw new \Exception (dgettext ("domframework",
                           "The configuration model file is missing"), 500);
    if (! is_readable ($modelFile))
      throw new \Exception (dgettext ("domframework",
                           "The configuration model file is not readable"),
                           500);
    $filecontent = file_get_contents ($modelFile);
    $tokens = token_get_all ($filecontent);
    $foundDefault = "";
    $parenthesis = 0;
    $params = array ();
    $path = "";
    $group = dgettext ("domframework", "Default parameters");
    foreach ($tokens as $token)
    {
      if (is_array ($token))
      {
        list ($id, $text) = $token;
        if ($debug) echo "ARRAY : $id (".token_name ($id)."), $text\n";
        if ($foundDefault === "" && $text === "->")
          $foundDefault = $text;
        if ($id === T_DOC_COMMENT)
        {
          // Look at @param, @description, @type, @values, @default
          if ($debug) echo "DOC_COMMENT : $text\n";
          // Append the not completed lines
          $text = trim (substr ($text, 3, -2));
          $text = preg_replace ("/\n\s+\*?\s*/", " ", $text);
          $text = preg_replace (
                    "/(@(param|description|type|values|default|group|prefix))/",
                    "\n\${1}", $text);
          $text = ltrim ($text);
          // Look at each parameter and save them in a data array
          $data = array ();
          foreach (explode ("\n", $text) as $line)
          {
            $tmp = explode (" ", $line);
            $key = reset ($tmp);
            $key = substr ($key, 1);
            $data[$key] = trim (implode (" ", array_slice ($tmp, 1)));
          }
          if (isset ($data["group"]))
            $group = $data["group"];
          if (! isset ($data["param"]))
            continue;
          if (substr ($data["param"], 0, 1) !== "/")
            throw new Exception (sprintf (dgettext ("domframework",
              "Parameter '%s' doesn't start by slash"), $data["param"]), 500);
          $data["depth"] = $parenthesis;
          $data["group"] = $group;
          //if (isset ($data["values"]) && $data["values"][0] !== "\"")
          if (isset ($data["values"]))
          {
            // A function or an array or a number is provided
            if ( function_exists ($data["values"]))
              $data["values"] = call_user_func ($data["values"]);
            elseif (is_string ($data["values"]))
              $data["values"] = $this->strToType ($data["values"]);
          }
          if (isset ($data["default"]))
            $data["default"] = $this->strToType ($data["default"]);
          if ($debug) var_dump ($data);
          $params[$data["param"]] = $data;
        }
      }
      else
      {
        if ($debug) echo "TEXT  : $token\n";
        if ($foundDefault !== "" && $token === "(")
        {
          $parenthesis++;
        }
        if ($foundDefault !== "" && $token === ")")
        {
          $parenthesis--;
          if ($parenthesis === 0)
            break;
        }
      }
    }
    return $params;
  }
}