<?php
/**

setup_lib.php - Library of the web configurator (setup.php).
-------------

see header of setup.php for copyright, license and contact information

**/

require_once('xpath/XPath.class.php');
require_once('xmlForm.inc.php');
require_once('DiGIR_utils.php');

//////// Functions //////// 

/** Strip slashes from strings and array elements
 *  
 * @param string or array reference
 */
function stripMagicSlashes(&$var)
{
  if (is_string($var))
    {
      $var = stripslashes($var);
    }
  elseif (is_array($var))
    {
      foreach($var as $key => $value)
	{
	  stripMagicSlashes($var[$key]);
	}
    }
}

/** Simple function that returns a hash (value => value) from a simple array (index => value)
 *  
 * @param array elements
 *
 * @return hash
 */
function getHash($elements)
{
  $retArray = array();
  
  foreach ($elements as $value)
    {
      $retArray[$value] = $value;
    }

  return $retArray;
}

/** Case insensitive version of "in_array"
 *  
 * @param string searchVal Value to be searched
 * @param array targetArray Array to search for value
 *
 * @return boolean true or false
 */
function flexInArray($searchVal, $targetArray)
{
  foreach ($targetArray as $key => $value)
    {
      if (strcasecmp($searchVal, $value) == 0) return true;
    }

  return false;
}

/** Case insensitive version of "str_replace" compatible with versions prior to PHP5
 *  
 * @param string/array find String to be replaced (can be array of strings)
 * @param string/array replace New replacement string (can be array of strings)
 * @param string String subject to changes
 *
 * @return string Updated string
 */
function striReplace($find, $replace, $string)
{
  if(!is_array($find)) $find = array($find);

  if(!is_array($replace))
    {
      if(!is_array($find)) $replace = array($replace);
      else
	{
	  // this will duplicate the string into an array the size of $find
	  $c = count($find);
	  $rString = $replace;
	  unset($replace);

	  for ($i = 0; $i < $c; $i++)
	    {
	      $replace[$i] = $rString;
	    }
	}
    }

  foreach($find as $fKey => $fItem)
    {
      $between = explode(strtolower($fItem),strtolower($string));
      $pos = 0;

      foreach($between as $bKey => $bItem)
	{
	  $between[$bKey] = substr($string,$pos,strlen($bItem));
	  $pos += strlen($bItem) + strlen($fItem);
	}

      $string = implode($replace[$fKey],$between);
    }

  return($string);
}


/** Generic function to return an HTML combo from an array of values 
 *
 * @param string name Name of the combo
 * @param hash items Items to build the combo (label=>key)
 * @param string value Value to be used
 * @param boolean multiple [optional] Indicates if combo accepts multiple selection
 * @param string size [optional] Number of lines to show
 *
 * @return string Html representing the combo widget
 */
function getCombo($name, $items, $value, $multiple=false, $size=false, $onChange='')
{
  if (count($items) == 0)
    {
      return '';
    }

  $strSize = (gettype($size) == 'integer') ? " size=\"".$size."\"" : '' ;
  
  if ($onChange)
    {
      $onChange = sprintf(' onchange="%s"', $onChange);
    }

  if ($multiple)
    {
      $combo = sprintf("\n<select multiple name=\"%s[]\"%s%s>", $name, $strSize, $onChange) ;
    }
  else
    {
      $combo = sprintf("\n<select name=\"%s\"%s%s>", $name, $strSize, $onChange) ;
    }
  
  foreach ($items as $key => $label)
    {
      if ($multiple)
	{
	  $selected = (in_array($key, $value)) ? ' selected' : '';
	}
      else
	{
	  $selected = (!strcasecmp($value, $key)) ? ' selected' : '';
	}
    
      $combo .= sprintf("\n<option value=\"%s\"%s>%s</option>", $key, $selected, $label);
    }
  
  $combo .= sprintf("\n</select>");
  
  return $combo ;
}

/** Generic function to return an HTML text input 
 *
 * @param string name Name of the input
 * @param string value Value to be used
 * @param string size [optional] Size (width)
 * @param boolean isPassword [optional] Indicates if input is of type "password"
 *
 * @return string Html representing the text widget
 */
function getTextInput($name, $value, $size='60', $isPassword=false)
{
  $type = ($isPassword) ? 'password' : 'text';

  $input = sprintf("<input type=\"%s\" name=\"%s\" value=\"%s\" size=\"%s\">",
		   $type, $name, $value, $size);

  return $input;
}

/** Get html representation of table joins inside the root table tag of a resource config file
 *  
 * @param string script Script name (only used to create the "remove" link)
 * @param hash passVars [reference] Variables to be passed as parameters (only used to create the "remove" link)
 * @param object xp [reference] PHP.XPath object reference
 * @param string parentTable Name of the parent table ("name" attribute from the upper table tag)
 * @param string path [optional] Xpath to search for table children tags
 *
 * @return string Html representing the text widget
 */
function getJoins($script, &$passVars, &$xp, $parentTable, $path='/table/table')
{
  $out = '';

  $level = substr_count($path, 'table') -1;

  $joins = $xp->match($path);

  foreach ($joins as $pathToJoin)
    {
      $indent = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;', $level);

      $table = $xp->getAttributes($pathToJoin, 'name');
      $key   = $xp->getAttributes($pathToJoin, 'key');
      $join  = $xp->getAttributes($pathToJoin, 'join');

      # Build url parameters
      $vars = '';

      foreach ($passVars as $name => $value)
	{
	  $vars .= "$name=$value&";
	}

      $removeLink = sprintf('<a href="%s?%sremove=%s" class="error">remove</a>', 
			    $script, $vars, urlencode($pathToJoin));

      $treeSymbol = '-';

      $out .= "<br><span class=\"label\">$indent <span class=\"text\">$treeSymbol</span> join with </span>".
	"<span class=\"text\">$table</span><span class=\"label\"> when </span>".
	"<span class=\"text\">$parentTable.$join</span><span class=\"label\"> equals </span>".
	"<span class=\"text\">$table.$key</span><span class=\"error\">&nbsp;&nbsp;[$removeLink]</span>";

      $out .= getJoins($script, $passVars, $xp, $table, $pathToJoin . '/table');
    }

  return $out;
}

/** Get html representation of a concept
 *  
 * @param hash schemaData Hash with three lines: 
 *                        [xp] => PHP.XPath object with conceptual schema loaded
 *                        [alias] => Namespace alias being used
 *                        [required] => Array with required element names
 * @param string lastNamespaceAliasInputName Since user may change the namespace alias, we need to keep track of the last alias name (remember that in the form all values are bound to the alias: $alias:$concept_something). So besides all alias text inputs, we have a hidden field to store the original value presented in the form. This parameter is that hidden field name. 
 * @param object xpt [reference] PHP.XPath object reference with resource configuration
 * @param object cn [reference] ADOdb database connection object reference
 * @param hash tables Hash with all tables inside database
 * @param hash fields Hash containing all fields from each table (tableName => array(fields))
 * @param string path Xpath to search for concept inside the XML schema (using $xps)
 * @param boolean enableSmartMapping Indicates if fields with the same name of concepts should be automatically selected 
 *
 * @return string Html representing the concept
 */
function getConcept($schemaData, $lastNamespaceAliasInputName, &$xpt, &$cn, 
		    $tables, $fields, $path, $enableSmartMapping)
{
  $out = '';

  $xps =& $schemaData['xp'];
  $namespaceAlias = $schemaData['alias'];
  $requiredElements = $schemaData['required'];

  # Standard hashes to be used by combos
  $possibleTypes = array('text'=>'text', 'numeric'=>'numeric', 'datetime'=>'datetime', 
			 'bbounding'=>'bounding box', 'julian'=>'julian day');

  # Get concept name
  $name = $xps->getAttributes($path, 'name');

  $isRequired = in_array($name, $requiredElements);

  # Define widget names
  $searchableInputName = "$namespaceAlias:$name".'_searchable';
  $returnableInputName = "$namespaceAlias:$name".'_returnable';
  $typeInputName = "$namespaceAlias:$name".'_type';
  $tableInputName = "$namespaceAlias:$name".'_table';
  $fieldInputName = "$namespaceAlias:$name".'_field';
  $clearFieldsButtonName = "$namespaceAlias:$name".'_clear';
  $addFieldComboName = "$namespaceAlias:$name".'_newfield';
  $addFieldButtonName = "$namespaceAlias:$name".'_add';

  # Get values
  if (getVal('clicked'))
    {
      # Replicate everything since user may change namespace alias!
      $lastNamespaceAlias = getVal($lastNamespaceAliasInputName);
      
      $lastSearchableInputName = "$lastNamespaceAlias:$name".'_searchable';
      $lastReturnableInputName = "$lastNamespaceAlias:$name".'_returnable';
      $lastTypeInputName = "$lastNamespaceAlias:$name".'_type';
      $lastTableInputName = "$lastNamespaceAlias:$name".'_table';
      $lastFieldInputName = "$lastNamespaceAlias:$name".'_field';
      $lastClearFieldsButtonName = "$lastNamespaceAlias:$name".'_clear';
      $lastAddFieldComboName = "$lastNamespaceAlias:$name".'_newfield';
      $lastAddFieldButtonName = "$lastNamespaceAlias:$name".'_add';

      $searchableVal = getVal($lastSearchableInputName);
      $returnableVal = getVal($lastReturnableInputName);
      $typeVal = getVal($lastTypeInputName);
      $tableVal = getVal($lastTableInputName);

      if (!$tableVal and getVal('refresh'))
	{
	  # If table did not have a value but user has just selected another table from
	  # other concept, try it as a default value (although guessing what the user wants
	  # is always complicated...)
	  # Change this line to: $tableVal = '0'; to disable automatic selection of tables.
	  $tableVal = getVal(getVal('refresh'), '0');

	  if ($tableVal == '0')
	    {
	      $searchableVal = $returnableVal = 0;
	    }

	  $fieldVal = '';
	}
      else
	{
	  # Possible field selected in the combo
	  $newField = getVal($lastAddFieldComboName);

	  if (getVal($lastClearFieldsButtonName))
	    {
	      # Remove field content if user clicked on "clear" button
	      $fieldVal = '';
	    }
	  else
	    {
	      # Get last field value
	      $fieldVal = getVal($lastFieldInputName);

	      if (getVal($lastAddFieldButtonName))
		{
		  $addedFields = split(',', $fieldVal);

		  if ($newField and !flexInArray(strtolower($newField), $addedFields))
		    {
		      # If there was something selected in the field combo, 
		      # different from the previously added fields, add it to the field value
		      $sep = ($fieldVal) ? ',': '';
		      
		      $fieldVal .= $sep . $newField;
		      
		      unset($_REQUEST[$addFieldComboName]);
		    }
		}
	    }
	}

      # If selected table has field with same name as concept, try it as default value
      if ($tableVal)
	{
	  if ($fieldVal)
	    {
	      $relatedFields = explode(',', $fieldVal);

	      foreach ($relatedFields as $relatedField)
		{
		  if (!flexInArray(strtolower($relatedField), $fields[strtolower($tableVal)]))
		    {
		      # If field value does not exist among selected table fields, clear it!
		      unset($relatedFields[$relatedField]);
		    }
		  
		  if ($newField and strcasecmp($relatedField, $newField) == 0)
		    {
		      # Reset combo if field has already been selected
		      unset($_REQUEST[$addFieldComboName]);
		    }
		}

	      $fieldVal = implode(',', $relatedFields);
	    }
	  elseif ($enableSmartMapping)
	    {
	      # If selected table has field with same name as concept, try it as default value
	      # when there is no previous field selected
	      if (flexInArray(strtolower($name), $fields[strtolower($tableVal)]))
		{
		  $_REQUEST[$addFieldComboName] = $name;
		}
	    }
	}
    }
  elseif (!getVal('new') and $pathInResource = $xpt->match("//concept[@name='$namespaceAlias:$name']"))
    {
      $values = $xpt->getAttributes($pathInResource[0]);

      $searchableVal = $values['searchable'];
      $returnableVal = $values['returnable'];
      $typeVal = $values['type'];
      $tableVal = $values['table'];
      $fieldVal = $values['field'];
    }
  else
    {
      $substitutionGroup = $xps->getAttributes($path, 'substitutionGroup');

      $searchableVal = (strpos(strtolower($substitutionGroup), 'searchable') === false) ? '0':'1';
      $returnableVal = (strpos(strtolower($substitutionGroup), 'returnable') === false) ? '0':'1';

      $type = $xps->getAttributes($path, 'type');

      if ($type == 'xsd:dateTime')
	{
	  $typeVal = 'datetime';
	}
      elseif ($type == 'xsd:decimal' or $type == 'nonNegativeInteger')
	{
	  $typeVal = 'numeric';
	}
      elseif ($name == 'JulianDay')
	{
	  $typeVal = 'julian';
	}
      elseif ($name == 'BoundingBox')
	{
	  $typeVal = 'bbounding';
	}
      else
	{
	  $typeVal = 'text';
	}

      $tableVal = $tables[0];
      $fieldVal = '';
    }

  # Prepare "add field" combo items
  if ($tableVal)
    {
      $fields = $fields[strtolower($tableVal)];

      $fields = getHash($fields);
 
      # Exclude selected fields from field combo
      if ($fieldVal)
	{
	  $selectedFields = explode(',', $fieldVal);
	  
	  foreach ($selectedFields as $field)
	    {
	      unset($fields[strtolower($field)]);
	    }
	}
    }
  else
    {
      $fields = array();
    }

  # Only show "field combo" and "add button" if there are available fields
  $addFieldCombo = $addButton = '&nbsp;';

  if (count($fields))
    {
      array_unshift($fields, '-- add field --');
     
      $addFieldCombo = getCombo($addFieldComboName, $fields, getVal($addFieldComboName, '0'));
      $addButton = '<input type="submit" name="'.$addFieldButtonName.'" value="add" onClick="window.saveScroll();">';
    }

  #  Only show "clear" button if there is a field
  $clearFieldsButton = '&nbsp;';

  if ($fieldVal)
    {
      $clearFieldsButton = '<input type="submit" name="'.$clearFieldsButtonName.'" value="clear" onClick="window.saveScroll();">';
    }

  $searchableCheck = ($searchableVal) ? 'checked' : '';
  $returnableCheck = ($returnableVal) ? 'checked' : '';

  $cssClass = ($isRequired) ? 'text_required' : 'text';

  # Gather everything
  $out .= '<tr bgcolor="#ffffee" nowrap="1">'.
            '<td class="'.$cssClass.'">&nbsp;'.$name.'</td>'.
            '<td align="center">'.
              '<input type="checkbox" class="checkbox" name="'.$searchableInputName.'" value="1" '.$searchableCheck.'>'.
            '</td>'.
            '<td align="center">'.
              '<input type="checkbox" class="checkbox" name="'.$returnableInputName.'" value="1" '.$returnableCheck.'>'.
            '</td>'.
            '<td align="center">'.getCombo($tableInputName, $tables, $tableVal, false, false, "document.forms[1].refresh.value='$tableInputName';window.saveScroll();document.forms[1].submit();").'</td>'.
            '<td align="left" class="text">'.
              '<input type="hidden" name="'.$fieldInputName.'" value="'.$fieldVal.'">'.
              '<table border="0" width="100%">'.
                '<tr nowrap="1">'.
                  '<td align="left" class="label">&nbsp;'.strtr($fieldVal, array(',' => '<br>')).
                  '<td align="right">'.$clearFieldsButton.
                '</tr>'.
                '<tr nowrap="1">'.
                  '<td align="left">'.$addFieldCombo.
                  '<td align="right">'.$addButton.
                '</tr>'.
              '</table>'.
            '</td>'.
            '<td align="center">'.getCombo($typeInputName, $possibleTypes, $typeVal).'</td>'.
          '</tr>';

  return $out;
}

/** This function check if the involved schemas exist and import them to the local config directory if they are not already available there. NOTE: I'm not happy with this function!
 *  
 * @param object source PHP.Xpath object or xml string to be loaded
 * @param string configDir Directory where configuration files are kept
 *
 * @return string Error message in case of error
 */
function handleSchemas($source, $configDir)
{
  $errStr = '';

  if (is_string($source))
    {
      # add root tag
      $source = '<root>'.$source.'</root>';

      $xp = new XPath();
      $xp->setVerbose(0);
      $xp->setXmlOption(XML_OPTION_CASE_FOLDING,false);
      $xp->setXmlOption(XML_OPTION_SKIP_WHITE,true);
      $xp->importFromString($source);
    }
  else
    {
      $xp = $source;
    }

  $schemas = $xp->match('//conceptualSchema');

  foreach ($schemas as $pathToSchema)
    {
      $schemaLocation  = $xp->getAttributes($pathToSchema, 'schemaLocation');
      
      if (!isURL($schemaLocation) or !strpos($schemaLocation, '.xsd'))
	{
	  $errStr = "Schema location '$schemaLocation' must be an available URL pointing to an xsd file!";
	  break;
	}
      
      $schemaName = substr($schemaLocation, strrpos($schemaLocation, '/')+1);
      
      $schemaFile = $configDir.'/'.$schemaName;
     
      if (!file_exists($schemaFile))
	{
	  # Create local copy
	  $xps = new XPath();
	  $xps->setVerbose(0);
	  $xps->setXmlOption(XML_OPTION_CASE_FOLDING,false);
	  $xps->setXmlOption(XML_OPTION_SKIP_WHITE,true);

	  if (!$xps->importFromFile($schemaLocation) or
	      !$xps->exportToFile($schemaFile))
	    {
	      $errStr = "Could not make local copy of '$schemaFile'!";
	      break;
	    }
	}
    }

  return $errStr;
}


/** Get html representation of filter elements in a resource config file
 *  
 * @param string script Script name (only used to create the "remove" link)
 * @param object xp [reference] PHP.XPath object reference
 * @param string path [optional] Xpath of an op (operator) element inside a filter
 *
 * @return string Html representing the text widget
 */
function getFilterElements($script, &$xp, $path='/filter/*')
{
  global $copsList, $lopsList, $columnsList, $typesList;

  $out = '';

  $level = substr_count($path, '/') -1;

  $els = $xp->match($path);

  if (count($els))
    {
      foreach ($els as $pathToEl)
	{
	  $el = $xp->getNode($pathToEl);
	  
	  $elName = strtolower(trim($el['attributes']['name']));

	  $removeButton = '&nbsp;<input type="submit" name="remove" value="remove" onClick="document.forms[1].refresh.value=\''.$pathToEl.'\';document.forms[1].submit();">';
	  
	  if ($el['attributes']['type'] == 'lop')
	    {
	      $lopId = urlencode($pathToEl);
	      
	      $cssClass = (fmod($level, 2) <> 0) ? 'box1' : 'box2';
	      
	      $out .= sprintf('<div class="%s" nowrap="1">', $cssClass) . "\n";
	      
	      $out .= getFilterElements($script, $xp, $pathToEl.'/*[1]');
	      
	      $out .= '<br><br>' . 
		getCombo($lopId, $lopsList, getVal($lopId, $elName)) . '&nbsp;' . $removeButton .
		"<br><br>\n";
	      
	      $out .= getFilterElements($script, $xp, $pathToEl.'/*[2]');
	      
	      $out .= '</div>'. "\n";
	    }
	  else
	    {
	      $copId = urlencode($pathToEl);

	      $pathToTerm = $pathToEl.'/term[1]';

	      $columnId = urlencode($pathToTerm .'[@col]');
	      $typeId = urlencode($pathToTerm .'[@type]');
	      $valueId = urlencode($pathToTerm .'[@val]');
	      
	      $term = $xp->getNode($pathToTerm);

	      $column = $term['attributes']['table'] .'.'.$term['attributes']['field'];
	      
	      $type  = $term['attributes']['type'];

	      $value = implode('', $term['textParts']);
	      
	      $out .= getCombo($columnId, $columnsList, getVal($columnId, $column)) . '&nbsp;' .
		getCombo($copId, $copsList, getVal($copId, $elName)) . '&nbsp;'.
		getCombo($typeId, $typesList, getVal($typeId, $type)) .'&nbsp;'.
		getTextInput($valueId, getVal($valueId, $value), '10'). "\n" .
		'&nbsp;' . $removeButton;
	    }
	}
    }
  else
    {
      # Button names
      $newCop = $path.'_newcop';
      $newLop = $path.'_newlop';

      $addCopButton = '<input type="submit" name="add" value="add simple comparison" onClick="document.forms[1].refresh.value=\''.$newCop.'\';document.forms[1].submit();">';
      $addLopButton = '<input type="submit" name="add" value="add logical expression" onClick="document.forms[1].refresh.value=\''.$newLop.'\';document.forms[1].submit();">';

      $out = $addCopButton .'&nbsp;'.$addLopButton;
    }

  return $out;
}


/** Replace "op" tag names with the corresponding DiGIR tags (equals/and/like, etc)
 *  by looking at the "type" attribute, convert NULL values to xsi:nill attributes,
 *  and split comma separated list of "IN" operations into a list of "term" elements.
 *  
 * @param object xp [reference] PHP.XPath object reference
 * @param string path [optional] Xpath of an generic element
 *
 * @return string Returns xml version of the filter compatible with DiGIR parser
 */
function exportToDigir(&$xp, $path='/filter[1]/op[1]')
{
  $node = $xp->getNode($path);
 
  if ($node['name'] == 'op')
    {
      $xml = '<'.$node['attributes']['name'].'>';
      
      $children = $xp->match($path.'/*');
      
      foreach ($children as $pathToChild)
	{
	  $xml .= exportToDigir($xp, $pathToChild);
	}
      
      $xml .= '</'.$node['attributes']['name'].'>';
    }
  else
    {
      # Change "in" term element to list
      if (strcasecmp($node['parentNode']['attributes']['name'], 'in') == 0)
	{
	  $xml = '<list>';
	  
	  $values = explode(',', implode('', $node['textParts']));

	  foreach ($values as $val)
	    {
	      $xml .= sprintf('<term table="%s" field="%s" type="%s">%s</term>',
			      $node['attributes']['table'], $node['attributes']['field'], 
			      $node['attributes']['type'], trim($val));
	    }
	  
	  $xml .= '</list>';
	}
      else
	{
	  # Null regular term
	  if (strcasecmp(trim(implode('', $node['textParts'])), 'null') == 0)
	    {
	      $xml = sprintf('<term table="%s" field="%s" type="%s" xsi:nill="true"/>',
			     $node['attributes']['table'], $node['attributes']['field'], 
			     $node['attributes']['type']);
	    }
	  else
	    {
	      # Not null regular term
	      $xml = $xp->exportAsXml($path);
	    }
	}
    }

  return $xml;
}

?>