<?php
/*******************************************************************************
 * Copyright (C) 2007 Easter-eggs
 * https://ldapsaisie.org
 *
 * Author: See AUTHORS file in top-level directory.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

******************************************************************************/

// Error messages

LSerror :: defineError('PPOLICY_SUPPORT_01',
  ___("Password policy Support : The constant %{const} is not defined.")
);
LSerror :: defineError('PPOLICY_SUPPORT_02',
  ___("Password policy Support : The global array %{array} is not defined.")
);
LSerror :: defineError('PPOLICY_01',
  ___("Password policy: An error occured generating CSV outfile memory space.")
);
LSerror :: defineError('PPOLICY_02',
  ___("Password policy: An error occured executing the search.")
);
LSerror :: defineError('PPOLICY_03',
  ___("Password policy: An error occured writing CSV header.")
);
LSerror :: defineError('PPOLICY_04',
  ___("Password policy: An error occured writing a CSV row.")
);

/**
 * Check support of Ppolicy by LdapSaisie
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 *
 * @return boolean true if Ppolicy is totally supported, false in other case
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
function LSaddon_ppolicy_support() {
  $retval = true;

  $MUST_DEFINE_CONST = array(
    'LS_PPOLICY_DEFAULT_DN',
    'LS_PPOLICY_WARNING_EXPIRATION_THRESHOLD',
    'LS_PPOLICY_CRITICAL_EXPIRATION_THRESHOLD',
    'LS_PPOLICY_CSV_DELIMITER',
    'LS_PPOLICY_CSV_ENCLOSURE',
    'LS_PPOLICY_CSV_ESCAPE_CHAR',
  );

  foreach($MUST_DEFINE_CONST as $const) {
    if (!defined($const)) {
      LSerror :: addErrorCode('PPOLICY_SUPPORT_01', $const);
      $retval = false;
    }
  }

  $MUST_DEFINE_ARRAY= array(
    'LS_PPOLICY_API_GRANTED_PROFILES',
    'LS_PPOLICY_INFO_EXPORT_EXTRA_ATTRS',
  );
  foreach($MUST_DEFINE_ARRAY as $array) {
    if ( !isset($GLOBALS[$array]) || !is_array($GLOBALS[$array])) {
      LSerror :: addErrorCode('PPOLICY_SUPPORT_02', $array);
      $retval = false;
    }
  }

  // Init ppolicy objects cache
  $GLOBALS['PPOLICY_OBJECTS_CACHE'] = array();

  if ($retval) {
    LSurl :: add_handler(
      '#^api/1.0/exportPpolicyInfo/(?P<LSobject>[^/]+)/?$#',
      'handle/export_api_LSobject_exportPpolicyInfo',
      true, false, true);
  }

  return $retval;
}

/**
 * Retrieve ppolicy object (from cache if already loaded)
 *
 * @param string $dn The DN of the ppolicy object
 *
 * @return array|false Array of ppolicy object's attributes on success, false otherwise
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
function get_ppolicy_object($dn) {
  if (array_key_exists($dn, $GLOBALS['PPOLICY_OBJECTS_CACHE']))
    return $GLOBALS['PPOLICY_OBJECTS_CACHE'][$dn];
  $GLOBALS['PPOLICY_OBJECTS_CACHE'][$dn] = LSldap :: getAttrs($dn, '(objectClass=pwdPolicy)');
  return $GLOBALS['PPOLICY_OBJECTS_CACHE'][$dn];
}

/**
 * Retrieve ppolicy password max age
 *
 * @param string|null $ppolicy_dn Optional DN of the ppolicy object to use
 *
 * @return int|null|false The ppolicy password max age (in second) if defined, null if no password max age is defined or false in case of error
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
function get_ppolicy_password_max_age($ppolicy_dn=null) {
  if (!$ppolicy_dn)
    $ppolicy_dn = LS_PPOLICY_DEFAULT_DN;
  if (!$ppolicy_dn)
    return null;
  $ppolicy = get_ppolicy_object($ppolicy_dn);
  if (!$ppolicy)
    return false;
  if (isset($ppolicy['pwdMaxAge']))
    return intval($ppolicy['pwdMaxAge']);
  return null;
}

/**
 * Format and return HTML code of a badge
 *
 * @param string $text The text of the badge
 * @param string $bg_color The background color of the badge (optional, default: green)
 * @param string $color The text color of the badge (optional, default: white)
 *
 * @return string The HTML code of the badge
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
function _ppolicy_badge($text, $bg_color='green', $color='white') {
  // Disable HTML formating on PHP cli
  if (php_sapi_name() == 'cli') return $text;
  return sprintf(
    '<span style="
        background-color: %s; color: %s;
        padding: 0.2em; font-size: 0.8em; border-radius: 0.4em;"
    >%s</span>',
    $bg_color, $color, $text
  );
}

 /**
  * Retrieve Ppolicy extraDisplayedColumn password expiration
  *
  * @param LSsearchEntry $entry An LSsearchEntry object
  *
  * @return string Ppolicy extraDisplayedColumn password expiration
  *
  * @author Benjamin Renard <brenard@easter-eggs.com>
  */
function ppolicy_extraDisplayColumn_password_expiration($entry) {
  $change_time = $entry->pwdChangedTime;
  if (!$change_time)
    return _('Never');
  $change_time = ldapDate2Timestamp($change_time);
  $max_age = get_ppolicy_password_max_age($entry->pwdPolicySubentry);
  if ($max_age === false)
    return _ppolicy_badge(__('Unknown'), 'gray');
  if (!$max_age)
    return _('Never');
  $expiration_date = $change_time + $max_age;
  $now = time();
  if ($expiration_date <= $now)
    return _ppolicy_badge(
      sprintf(_('Expired (since %s)'), date('Y-m-d H:i', $expiration_date)),
      'black');
  $delta = $expiration_date - $now;
  if ($delta <= LS_PPOLICY_CRITICAL_EXPIRATION_THRESHOLD)
    $badge_color = 'red';
  elseif ($delta <= LS_PPOLICY_WARNING_EXPIRATION_THRESHOLD)
    $badge_color = 'orange';
  else
    $badge_color = 'green';
  return _ppolicy_badge(
    sprintf(_('Expire on %s'), date('Y-m-d H:i', $expiration_date)),
    $badge_color);
}

/**
 * Write LSsearch result as CSV and force download of it.
 *
 * @param LSsearch $LSsearch The LSsearch object
 * @param boolean $as_csv True for CSV export, False for JSON export
 * @param boolean $return Set to true to return data instead of trigger the download of it
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 *
 * @return boolean|array Data to export as array if $return is enabled, otherwise, true on
 *                       success and false on error.
 */
function ppolicy_export_search_info($LSsearch, $as_csv=true, $return=false) {
  $csv = null;
  if ($as_csv) {
    $csv = fopen('php://temp/maxmemory:'. (5*1024*1024), 'r+');

    if ($csv === false) {
      LSerror :: addErrorCode('PPOLICY_01');
      return false;
    }
  }

  $attrs = array_merge(
    array(
      'pwdPolicySubentry', 'pwdChangedTime', 'pwdGraceUseTime', 'pwdFailureTime',
      'pwdAccountLockedTime', 'pwdReset', 'pwdHistory'
    ),
    $GLOBALS['LS_PPOLICY_INFO_EXPORT_EXTRA_ATTRS']
  );
  $LSsearch -> setParam('attributes', $attrs);

  if (!$LSsearch -> run()) {
    LSerror :: addErrorCode('PPOLICY_02');
    return false;
  }

  $data = array();
  if ($as_csv) {
    $headers = array($LSsearch->label_objectName, 'DN');
    foreach($attrs as $attr) {
      $label = LSconfig::get("LSobjects.".$LSsearch->LSobject.".attrs.$attr.label", $attr, 'string');
      $headers[] = __($label);
    }

    if (!_ppolicy_write_row_in_csv($csv, $headers)) {
      LSerror :: addErrorCode('PPOLICY_03');
      return false;
    }
  }

  foreach ($LSsearch -> getSearchEntries() as $e) {
    $row = array(
      'name' => $e -> displayName,
      'dn' => $e -> dn,
    );

    foreach($attrs as $attr) {
      if ($as_csv) {
        $values = ensureIsArray($e -> get($attr));
        if ($values) {
          $row[] = implode('|', $values);
        }
        else {
          $no_value_label = LSconfig::get(
            "LSobjects.".$LSsearch->LSobject.".attrs.$attr.no_value_label",
            ___('Not set'), 'string');
          $row[] = __($no_value_label);
        }
      }
      else {
        $row[$attr] = ensureIsArray($e -> get($attr));
        if (!LSconfig::get("LSobjects.".$LSsearch->LSobject.".attrs.$attr.multiple")) {
          $row[$attr] = ($row[$attr]?$row[$attr][0]:null);
        }
      }
    }

    if ($as_csv) {
      if (!_ppolicy_write_row_in_csv($csv, $row)) {
        LSerror :: addErrorCode('PPOLICY_04');
        return false;
      }
    }
    else {
      $data[] = $row;
    }
  }

  if (!$as_csv) {
    if ($return)
      return $data;
    header("Content-disposition: attachment; filename=ppolicy-".$LSsearch->LSobject.".json");
    LSsession :: displayAjaxReturn($data);
    exit();
  }

  rewind($csv);
  if ($return) {
    $data = stream_get_contents($csv);
    @fclose($csv);
    return $data;
  }
  header("Content-disposition: attachment; filename=ppolicy-".$LSsearch->LSobject.".csv");
  header("Content-type: text/csv");
  print stream_get_contents($csv);
  @fclose($csv);
  exit();
}

/**
 * Write CSV row in file
 *
 * @param resource $csv The CSV file description reference
 * @param array<string> $row An array of a CSV row fields to write
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 *
 * @return boolean True if CSV row is successfully writed, false in other case
 */
function _ppolicy_write_row_in_csv(&$csv, &$row) {
  if (!defined('PHP_VERSION_ID') or PHP_VERSION_ID < 50504) {
    return (
      fputcsv($csv, $row, LS_PPOLICY_CSV_DELIMITER, LS_PPOLICY_CSV_ENCLOSURE) !== false
    );
  }
  return (
    fputcsv(
      $csv, $row, LS_PPOLICY_CSV_DELIMITER, LS_PPOLICY_CSV_ENCLOSURE,
      LS_PPOLICY_CSV_ESCAPE_CHAR) !== false
  );
}

/**
 * Handle exportPpolicyInfo API request
 *
 * @param LSurlRequest $request  The request
 *
 * @return void
 **/
function handle_api_LSobject_exportPpolicyInfo($request) {
  $object = get_LSobject_from_API_request($request);
  if (!$object)
    return;
  $container_dn = LSconfig::get(
    "LSobjects.".$object->LSobject.".container_dn",
    "", "string");
  $whoami = LSsession :: whoami(
    $container_dn?
    $container_dn.','.LSsession::getTopDn():
    LSsession::getTopDn()
  );
  if (!array_intersect($GLOBALS['LS_PPOLICY_API_GRANTED_PROFILES'], $whoami)) {
    LSerror :: addErrorCode('LSsession_11');
    LSsession :: displayAjaxReturn();
    return;
  }

  if (!LSsession :: loadLSclass('LSsearch')) {
    LSerror :: addErrorCode('LSsession_05', 'LSsearch');
    LSsession :: displayAjaxReturn();
    return;
  }

  $search = new LSsearch(
    $request->LSobject,
    'api'
  );
  $search -> setParam('onlyAccessible', True);

  $data = ppolicy_export_search_info($search, false, true);
  LSsession :: displayAjaxReturn($data);
}

if (php_sapi_name() != 'cli')
  return true;


/**
 * CLI command to export password policy info of a all objects of a specified object type
 * @param  array<string> $command_args Command arguments
 * @return bool
 */
function cli_export_ppolicy_info($command_args) {
  $objType = null;
  $output = false;
  $json = false;
  $pretty = false;
  for ($i=0; $i < count($command_args); $i++) {
    switch($command_args[$i]) {
      case '-o':
      case '--output':
        $i++;
        $output = $command_args[$i];
        break;

      case '-j':
      case '--json':
        $json = true;
        break;

      case '-p':
      case '--pretty':
        $pretty = true;
        break;

      default:
        if (is_null($objType))
          $objType = $command_args[$i];
        else
          LScli :: usage("Invalid ".$command_args[$i]." parameter.");
        break;
    }
  }

  if (is_null($objType))
    LScli :: usage('You must provide LSobject type.');

  if (!LSsession :: loadLSobject($objType))
    return false;

  if (!LSsession :: loadLSclass('LSsearch')) {
    LSerror :: addErrorCode('LSsession_05', 'LSsearch');
    LSsession :: displayAjaxReturn();
    return false;
  }

  $search = new LSsearch($objType, 'cli_export_ppolicy_info');
  $search -> setParam('onlyAccessible', True);

  $data = ppolicy_export_search_info($search, !$json, true);

  if ($json)
    $data = json_encode(
      $data,
      ($pretty?JSON_PRETTY_PRINT:0)
    );

  if (!$output) {
    print($data);
    exit();
  }
  $fd = fopen($output, 'w') or LStemplate::fatal_error("Fail to open output file '$output'");
  $result = fwrite($fd, $data) or LStemplate::fatal_error("Fail to write result in output file '$output'");
  @fclose($fd);
  return $result;
}

/**
 * Args autocompleter for CLI export_ppolicy_info command
 *
 * @param array<string> $command_args List of already typed words of the command
 * @param int $comp_word_num The command word number to autocomplete
 * @param string $comp_word The command word to autocomplete
 * @param array<string> $opts List of global available options
 *
 * @return array<string> List of available options for the word to autocomplete
 **/
function cli_export_ppolicy_info_args_autocompleter($command_args, $comp_word_num, $comp_word, $opts) {
  $opts = array_merge($opts, array ('-o', '--output', '-j', '--json', '-p', '--pretty'));

  // Handle positional args
  $objType = null;
  $objType_arg_num = null;
  for ($i=0; $i < count($command_args); $i++) {
    if (!in_array($command_args[$i], $opts)) {
      // If object type not defined
      if (is_null($objType)) {
        // Defined it
        $objType = $command_args[$i];
        LScli :: unquote_word($objType);
        $objType_arg_num = $i;

        // Check object type exists
        $objTypes = LScli :: autocomplete_LSobject_types($objType);

        // Load it if exist and not trying to complete it
        if (in_array($objType, $objTypes) && $i != $comp_word_num) {
          LSsession :: loadLSobject($objType, false);
        }
      }
    }
  }

  // If objType not already choiced (or currently autocomplete), add LSobject types to available options
  if (!$objType || $objType_arg_num == $comp_word_num)
    $opts = array_merge($opts, LScli :: autocomplete_LSobject_types($comp_word));

  return LScli :: autocomplete_opts($opts, $comp_word);
}

LScli :: add_command(
  'export_ppolicy_info',
  'cli_export_ppolicy_info',
  'Export password policy info of a all objects of a specified object type',
  '[object type] [-o|--output filepath] [-j|--json [-p|--pretty]]',
  false, // long desc
  true,
  'cli_export_ppolicy_info_args_autocompleter'
);
