/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com)  |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
*/
#include "hphp/util/cronolog.h"

#include <boost/filesystem/path.hpp>

#ifndef _MSC_VER
#include <pwd.h>
#endif

#include <folly/portability/Fcntl.h>
#include <folly/portability/SysStat.h>

/* Default permissions for files and directories that are created */

#ifndef FILE_MODE
#define FILE_MODE       ( S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH )
#endif

namespace HPHP {

///////////////////////////////////////////////////////////////////////////////

using std::string;
namespace fs = boost::filesystem;

/* Open a new log file: determine the start of the current
 * period, generate the log file name from the fileTemplate,
 * determine the end of the period and open the new log file.
 *
 * Returns the file descriptor of the new log file and also sets the
 * name of the file and the start time of the next period via pointers
 * supplied.
 */
static FILE *new_log_file(const char *fileTemplate, const char *linkname,
                          mode_t linktype, const char *prevlinkname,
                          PERIODICITY periodicity, int period_multiple,
                          int period_delay, char *pfilename,
                          size_t pfilename_len, time_t time_now,
                          time_t *pnext_period) {
  time_t start_of_period;
  struct tm   *tm;
  int log_fd;

  start_of_period = start_of_this_period(time_now, periodicity,
                                         period_multiple);
  tm = localtime(&start_of_period);
  strftime(pfilename, pfilename_len, fileTemplate, tm);
  *pnext_period = start_of_next_period(start_of_period, periodicity,
                                       period_multiple) + period_delay;

  CRONO_DEBUG(("%s (%d): using log file \"%s\" from %s (%d) until %s (%d) "
        "(for %d secs)\n",
        timestamp(time_now), time_now, pfilename,
        timestamp(start_of_period), start_of_period,
        timestamp(*pnext_period), *pnext_period,
        *pnext_period - time_now));

  log_fd = open(pfilename, O_WRONLY|O_CREAT|O_APPEND, FILE_MODE);

#ifndef DONT_CREATE_SUBDIRS
  if ((log_fd < 0) && (errno == ENOENT)) {
    create_subdirs(pfilename);
    log_fd = open(pfilename, O_WRONLY|O_CREAT|O_APPEND, FILE_MODE);
  }
#endif

  if (log_fd < 0) {
    perror(pfilename);
    return nullptr;
  }

#ifndef _MSC_VER
  if (linkname) {
    struct stat stat_buf;
    struct stat stat_buf2;
    if (fstat(log_fd, &stat_buf) ||
        stat(linkname, &stat_buf2) ||
        stat_buf.st_ino != stat_buf2.st_ino ||
        stat_buf.st_dev != stat_buf2.st_dev) {

      /* Create a relative symlink to logs under linkname's directory */
      std::string dir = fs::path(linkname).parent_path().native();
      if (dir != "/") {
        dir.append("/");
      }
      std::string filename;
      if (!strncmp(pfilename, dir.c_str(), dir.length())) {
        filename = pfilename + dir.length();
      } else {
        filename = pfilename;
      }

      create_link(filename.c_str(), linkname, linktype, prevlinkname);
    }
  }
#endif
  return fdopen(log_fd, "a");
}

void Cronolog::setPeriodicity() {
  if (m_periodicity == UNKNOWN) {
    m_periodicity = determine_periodicity((char *)m_template.c_str());
  }
}

FILE *Cronolog::getOutputFile() {
  if (m_template.empty()) return m_file;

  time_t time_now = time(nullptr) + m_timeOffset;
  /* If the current period has not finished and there is a log file, use it */
  if ((time_now < m_nextPeriod) && (m_file)) return m_file;

  /* We need to open a new file under a mutex. */
  {
    std::lock_guard<std::mutex> lock(m_mutex);
    if ((time_now >= m_nextPeriod)) {
      /* the current period has finished */

      /* We cannot close m_file because there may be other threads still
       * writing to it. We save m_file in m_prevFile and leave it open for
       * an entire period. We simply assume that by the end of the delay
       * no threads should be still referencing m_prevFile and we can safely
       * close it.
       */
      if (m_prevFile) fclose(m_prevFile);
      m_prevFile = m_file;
      m_file = nullptr;
    }

    /* If there is no log file open then open a new one. */
    if (m_file == nullptr) {
      const char *linkname = m_linkName.empty() ? nullptr : m_linkName.c_str();
      m_file = new_log_file(m_template.c_str(),
#ifdef _MSC_VER
                            "", 0,
#else
                            linkname, S_IFLNK,
#endif
                            m_prevLinkName, m_periodicity, m_periodMultiple,
                            m_periodDelay, m_fileName, sizeof(m_fileName),
                            time_now, &m_nextPeriod);
    }
  }
  return m_file;
}

void Cronolog::changeOwner(const string &username, const string &symlink) {
#ifdef _MSC_VER
  return;
#else
  if (username.empty() || symlink.empty()) {
    return;
  }

  int username_length = sysconf(_SC_GETPW_R_SIZE_MAX);
  if (username_length == -1) {
    username_length = 512;
  }

  struct passwd user_info, *user_infop;
  std::vector<char> username_buf(username_length);

  if (getpwnam_r(username.c_str(), &user_info, &username_buf[0],
                 username_length, &user_infop)) {
    // invalid user
    return;
  }

  if (lchown(symlink.c_str(), user_info.pw_uid, -1) < 0) {
    fprintf(stderr, "Unable to chmod %s\n", symlink.c_str());
  }

  // using chown() isn't portable if it is a symlink
  int fd = open(symlink.c_str(), O_RDONLY | O_NONBLOCK | O_NOCTTY);
  int success = (fd >= 0 ? fchown(fd, user_info.pw_uid, -1) : -1);

  if (fd >= 0) {
    close(fd);
  }

  if (success < 0) {
    fprintf(stderr, "Unable to chmod %s\n", symlink.c_str());
  }
#endif
}

///////////////////////////////////////////////////////////////////////////////
}
