<?php
/*
  Copyright 2011-2019 Xavier Guerrin and the TuxFamily team
  This file is part of the tuxfamily_repository module.

  This module is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 2 of the License, or
  (at your option) any later version.

  This module 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 module.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * @file
 * Implements the core hooks, defines, public and private functions.
 */

define('TUXFAMILY_REPOSITORY_ENABLED_VARIABLE',             'tuxfamily_repository_enabled');
define('TUXFAMILY_REPOSITORY_DEFAULT_ENABLED',              FALSE);

define('TUXFAMILY_REPOSITORY_PROJECT_VARIABLE',             'tuxfamily_project');
define('TUXFAMILY_REPOSITORY_DEFAULT_PROJECT',              'project');

define('TUXFAMILY_REPOSITORY_DRUPAL_STATIC_PATH_VARIABLE',  'tuxfamily_repository_drupal_static_path');
define('TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_STATIC_PATH',   'drupal_files/static');

define('TUXFAMILY_REPOSITORY_DRUPAL_FILES_PATH_VARIABLE',   'tuxfamily_repository_drupal_files_path');
define('TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_FILES_PATH',    'drupal_files/sites/@sitename/files');

define('TUXFAMILY_REPOSITORY_BASE_URL_PATTERN_VARIABLE',    'tuxfamily_repository_base_url_pattern');
define('TUXFAMILY_REPOSITORY_DEFAULT_BASE_URL_PATTERN',     'https://download.tuxfamily.org/@project/@path');

define('TUXFAMILY_REPOSITORY_DRUPAL_PATH_PATTERN_VARIABLE', 'tuxfamily_repository_drupal_path_pattern');
define('TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_PATH_PATTERN',  '/data/repository/@project/@path');

define('TUXFAMILY_REPOSITORY_SSH_PATH_PATTERN_VARIABLE',    'tuxfamily_repository_ssh_path_pattern');
define('TUXFAMILY_REPOSITORY_DEFAULT_SSH_PATH_PATTERN',     '/home/@project/@project-repository/@path');

define('TUXFAMILY_REPOSITORY_RSYNC_PATH_VARIABLE',          'tuxfamily_repository_rsync_path');
define('TUXFAMILY_REPOSITORY_DEFAULT_RSYNC_PATH',           '/usr/bin/rsync');

/**
 * Implements hook_boot().
 * Used to adjust the public_file_path variable in case Drupal is run from
 * TuxFamily's SSH access.
 */
function tuxfamily_repository_boot() {
  // Not configurable yet, this arbitrary condition checks whether we are run through SSH / Cron
  if (isset($_SERVER['HOME']) && $_SERVER['HOME'] == '/home') {
    $GLOBALS['conf']['tuxfamily_ssh'] = TRUE;
    $GLOBALS['conf']['file_public_path'] = _tuxfamily_repository_get_files_base_path();
  }
}

/**
 * Implements hook_modules_installed().
 * Used here to sync static files to the TuxFamily repository.
 */
function tuxfamily_repository_modules_installed($modules_installed) {
  // do nothing when the module itself is enabled/installed (i.e. probably not configured yet)
  if (in_array('tuxfamily_repository', $modules_installed)) {
    return;
  }

  _tuxfamily_repository_rsync_static_files();
}

/**
 * Implements hook_file_url_alter().
 * Used here to modify generated file URLs.
 */
function tuxfamily_repository_file_url_alter(&$path) {
  $enabled = variable_get(TUXFAMILY_REPOSITORY_ENABLED_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_ENABLED);
  if (!$enabled) {
    return;
  }
  $data = drupal_static(__FUNCTION__);
  if (!$data) {
    $data['static_url'] = _tuxfamily_repository_get_static_base_url();
    $data['files_url']  = _tuxfamily_repository_get_files_base_url();
    $data['files_path'] = _tuxfamily_repository_get_files_base_path();
  }

  // We do not alter private files
  if (preg_match('/^(private|http):\/\//', $path)) {
    return;
  }

  // We have to handle a special case: /styles/ pictures are generated the
  // first time they are accessed -- therefore, if the file does not exist on
  // the download repository, we must output a URL that will trigger this
  // generation.
  $matches = array();
  if (preg_match('/^public:\/\/styles\/(.+)$/', $path, $matches)) {
    if (!file_exists($path)) {
      $path = $GLOBALS['base_url'] . '/system/files/styles/' . $matches[1];
      return;
    }
  }

  // Eventually, we replace public:// (9 characters) with our base url.
  if (preg_match('/^public:.*$/', $path)) {
    $path = $data['files_url'] . '/' . substr($path, 9);
  }
  else {
    $path = $data['static_url'] . '/' . ltrim($path, '/');
  }
}

/**
 * Implements hook_menu().
 */
function tuxfamily_repository_menu() {
  $items['admin/config/tuxfamily'] = array(
    'title' => 'TuxFamily repository',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('_tuxfamily_repository_config'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_form_alter().
 */
function tuxfamily_repository_form_alter(&$form, &$form_state, $form_id) {
  if ($form_id == 'system_file_system_settings') {
    _tuxfamily_repository_alter_file_system_settings($form, $form_state, $form_id);
  }
}

/**
 * @return the pattern to be used in order to determine the absolute path to
 * the TuxFamily repository. Note the result depends on the environment: web
 * server or SSH server.
 */
function _tuxfamily_repository_get_repository_path_pattern() {
  if ($GLOBALS['conf']['tuxfamily_ssh']) {
    $path = variable_get(TUXFAMILY_REPOSITORY_SSH_PATH_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_SSH_PATH_PATTERN);
  }
  else {
    $path = variable_get(TUXFAMILY_REPOSITORY_DRUPAL_PATH_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_PATH_PATTERN);
  }
  return $path;
}

/**
 * @return the path, relative to the TuxFamily repository, to the directory
 * that will host static content, i.e. content provided by Drupal modules.
 */
function _tuxfamily_repository_get_static_path() {
  $static_path = variable_get(TUXFAMILY_REPOSITORY_DRUPAL_STATIC_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_STATIC_PATH);
  $static_path = _tuxfamily_repository_apply_tokens($static_path);
  return $static_path;
}

/**
 * @return the path, relative to the TuxFamily repository, to the directory
 * that will host the Drupal files, i.e. uploaded and dynamically generated files.
 */
function _tuxfamily_repository_get_files_path() {
  $files_path = variable_get(TUXFAMILY_REPOSITORY_DRUPAL_FILES_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_FILES_PATH);
  $files_path = _tuxfamily_repository_apply_tokens($files_path);
  return $files_path;
}

/**
 * @return the base URL to the directory that will host the Drupal files, i.e.
 * uploaded and dynamically generated files.
 */
function _tuxfamily_repository_get_files_base_url() {
  $url = variable_get(TUXFAMILY_REPOSITORY_BASE_URL_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_BASE_URL_PATTERN);
  $url = _tuxfamily_repository_apply_tokens($url, array('@path' => _tuxfamily_repository_get_files_path()));
  return $url;
}

/**
 * @return the base URL to the directory that will host static content, i.e.
 * content provided by Drupal modules.
 */
function _tuxfamily_repository_get_static_base_url() {
  $url = variable_get(TUXFAMILY_REPOSITORY_BASE_URL_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_BASE_URL_PATTERN);
  $url = _tuxfamily_repository_apply_tokens($url, array('@path' => _tuxfamily_repository_get_static_path()));
  return $url;
}

/**
 * @return the absolute path to the directory that will host the Drupal files,
 * i.e. uploaded and dynamically generated files.
 */
function _tuxfamily_repository_get_files_base_path() {
  $path = _tuxfamily_repository_get_repository_path_pattern();
  $path = _tuxfamily_repository_apply_tokens($path, array('@path' => _tuxfamily_repository_get_files_path()));
  return $path;
}

/**
 * @return the absolute path to the directory that will host static content, i.e.
 * content provided by Drupal modules.
 */
function _tuxfamily_repository_get_static_base_path() {
  $path = _tuxfamily_repository_get_repository_path_pattern();
  $path = _tuxfamily_repository_apply_tokens($path, array('@path' => _tuxfamily_repository_get_static_path()));
  return $path;
}

/**
 * Modify the 'file_system_settings' form to let the user enable and configure
 * its TuxFamily repository.
 */
function _tuxfamily_repository_alter_file_system_settings(&$form, &$form_state, $form_id) {
  // we add our own submit function
  $form['#submit'][] = '_tuxfamily_repository_submit_file_system_settings';

  // should we work?
  $tf_repos_enabled = variable_get(TUXFAMILY_REPOSITORY_ENABLED_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_ENABLED);

  if ($tf_repos_enabled) {
    $form['file_public_path']['#default_value'] = _tuxfamily_repository_get_files_base_path();
    $form['file_public_path']['#description'] .= ' ' . t('Note its value is computed by the TuxFamily repository module');
  }

  $form[TUXFAMILY_REPOSITORY_ENABLED_VARIABLE] = array(
    '#type' => 'checkbox',
    '#title' => t('Use TuxFamily download repository for public files'),
    '#default_value' => $tf_repos_enabled,
    '#weight' => -1000,
  );

  // usual options
  $form['tuxfamily_repository_options'] = array(
    '#type' => 'fieldset',
    '#title' => 'TuxFamily repository options',
    '#collapsible' => TRUE,
    '#collapsed' => !$tf_repos_enabled,
    '#weight' => -999,
  );
  $form['tuxfamily_repository_options']['tokens'] = array(
    '#markup' => 'The following tokens are available for paths: @sitename (e.g. default if you are not using a multisite setup), @drupalroot (path to the Drupal directory) and @project (value filled in the first field).',
  );
  $form['tuxfamily_repository_options'][TUXFAMILY_REPOSITORY_PROJECT_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Name of your TuxFamily project'),
    '#description' => t('Your TuxFamily repository usually has the same name as its parent project. Please enter this name here.'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_PROJECT_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_PROJECT),
  );
  $form['tuxfamily_repository_options'][TUXFAMILY_REPOSITORY_DRUPAL_FILES_PATH_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Directory for your site\'s files in your TuxFamily repository'),
    '#description' => t('Which subdirectory within your TuxFamily repository should be used to store files uploaded to your website?'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_DRUPAL_FILES_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_FILES_PATH),
  );
  $form['tuxfamily_repository_options'][TUXFAMILY_REPOSITORY_DRUPAL_STATIC_PATH_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Directory for Drupal static files in your TuxFamily repository'),
    '#description' => t('Which subdirectory within your TuxFamily repository should be used to store static resources (CSS, JS, images, ...) provided by Drupal and its modules?'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_DRUPAL_STATIC_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_STATIC_PATH),
  );
  $form['tuxfamily_repository_options']['tuxfamily_repository_sync_instructions'] = array(
    '#markup' => sprintf(t('You may rsync Drupal static files on-demand using <a href="%s">%s</a>.'), url('admin/config/tuxfamily'), 'admin/config/tuxfamily'),
  );

  // advanced options
  $form['tuxfamily_repository_advanced_options'] = array(
    '#type' => 'fieldset',
    '#title' => 'TuxFamily repository advanced options',
    '#description' => 'Unless TuxFamily has modified their architecture, you should not need to change these settings.',
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#weight' => -998,
  );
  $form['tuxfamily_repository_advanced_options']['tokens'] = array(
    '#markup' => 'The following tokens are available for paths/URLs: @sitename (e.g. default if you are not using a multisite setup), @drupalroot (path to the Drupal directory) and @project (your TuxFamily project short name), @path (path that will be appended, to be considered mandatory).',
  );
  $form['tuxfamily_repository_advanced_options'][TUXFAMILY_REPOSITORY_BASE_URL_PATTERN_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Base URL of your TuxFamily repository'),
    '#description' => t('This pattern is used to compose the complete base URL to your TuxFamily repository.'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_BASE_URL_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_BASE_URL_PATTERN),
  );
  $form['tuxfamily_repository_advanced_options'][TUXFAMILY_REPOSITORY_DRUPAL_PATH_PATTERN_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Absolute path to the Drupal-dedicated directory in your TuxFamily repository, as seen on web servers'),
    '#description' => t('This pattern is used to compose the absolute path to the directory that will store your Drupal files within your TuxFamily repository.'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_DRUPAL_PATH_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_DRUPAL_PATH_PATTERN),
  );
  $form['tuxfamily_repository_advanced_options'][TUXFAMILY_REPOSITORY_SSH_PATH_PATTERN_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Absolute path to the Drupal-dedicated directory in your TuxFamily repository, as seen through SSH'),
    '#description' => t('Path to the same directory as seen through SSH (useful for cron jobs).'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_SSH_PATH_PATTERN_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_SSH_PATH_PATTERN),
  );
  $form['tuxfamily_repository_advanced_options'][TUXFAMILY_REPOSITORY_RSYNC_PATH_VARIABLE] = array(
    '#type' => 'textfield',
    '#title' => t('Rsync command'),
    '#description' => t('Path to the rsync tool.'),
    '#required' => TRUE,
    '#default_value' => variable_get(TUXFAMILY_REPOSITORY_RSYNC_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_RSYNC_PATH),
  );
}

/**
 * Describe the _tuxfamily_repository_config form, which allows administrators
 * to rsync static files to their TuxFamily repository on-demand.
 */
function _tuxfamily_repository_config($form, &$form_state) {
  $form['instructions'] = array(
    '#markup' => t('This page allows you to manually rsync Drupal static files to your TuxFamily repository. Use it with caution since it does not implement a lock yet, which could lead to multiple executions of rsync at the same time.') . '<br /><br />',
  );
  $form['go'] = array(
    '#type' => 'submit',
    '#value' =>  'Go',
  );
  return $form;
}

/**
 * Handle submission of the _tuxfamily_repository_config form.
 */
function _tuxfamily_repository_config_submit($form, &$form_state) {
  _tuxfamily_repository_rsync_static_files();
}

/**
 * Handle part of the submission of the file_system_settings form.
 * @see _tuxfamily_repository_alter_file_system_settings()
 */
function _tuxfamily_repository_submit_file_system_settings($form, $form_state) {
  $tf_repos_enabled = variable_get(TUXFAMILY_REPOSITORY_ENABLED_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_ENABLED);
  if ($tf_repos_enabled) {
    variable_set('file_public_path', _tuxfamily_repository_get_files_base_path());
  }
}

/**
 * @return the list of directories that should be deported to the TuxFamily
 * repository for this module to be efficient.
 */
function _tuxfamily_repository_deported_directories() {
  return array('misc', 'modules', 'themes', 'sites/all');
}

/**
 * Copy all directories returned by _tuxfamily_repository_deported_directories
 * to the Tuxfamily repository using rsync.
 * @return true if everything went ok, false otherwise.
 */
function _tuxfamily_repository_rsync_static_files() {
  $tf_repos_enabled = variable_get(TUXFAMILY_REPOSITORY_ENABLED_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_ENABLED);
  if (!$tf_repos_enabled) {
    return FALSE;
  }
  if (!_tuxfamily_repository_check_rsync_presence()) {
    drupal_set_message(t('Unable to find the rsync tool'), 'error');
    return FALSE;
  }
  foreach (array('misc', 'modules', 'themes', 'sites/all') as $subset) {
    $exec = _tuxfamily_repository_rsync_subset($subset);
    if ($exec['code'] !== 0) {
      // avoid displaying too much information
      $output = drupal_strlen($exec['output']) > 1024 ? drupal_substr(0, 1024, $exec['output']) : $exec['output'];
      drupal_set_message(t('An error occured when syncing @subset to your TuxFamily download repository: @output', array('@subset' => $subset, '@output' => $exec['output'])), 'error');
      return FALSE;
    }
  }
  drupal_set_message(t('Static files have been rsync\'ed to your TuxFamily repository'));
  return TRUE;
}

/**
 * @return true if the rsync tool is present, false otherwise.
 */
function _tuxfamily_repository_check_rsync_presence() {
  // this module simply expects to find the rsync tool in the PATH
  return file_exists(variable_get(TUXFAMILY_REPOSITORY_RSYNC_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_RSYNC_PATH));
}

/**
 * @return the exclude options to be used within rsync commands.
 */
function _tuxfamily_repository_exclude_options() {
  $options = '';
  foreach (array('php', 'inc', 'module', 'install', 'test', 'info') as $ext) {
    $options .= sprintf(' --exclude=\'*.%s\'', $ext);
  }
  return $options;
}

/**
 * Copy a given directory to the TuxFamily repository using rsync.
 * @param $subset Subdirectory to be rsync'ed.
 */
function _tuxfamily_repository_rsync_subset($subset) {
  $static_root = _tuxfamily_repository_get_static_base_path();
  if (!is_dir($static_root)) {
    if (!mkdir($static_root)) {
      drupal_set_message(t('Unable to create directory @root.', array('@root' => $static_root)), 'error');
      return FALSE;
    }
  }

  $command = _tuxfamily_repository_apply_tokens(
    '@rsync --delete --archive --quiet @exclude_options --relative @drupal_root/./@subset @repository_static_root',
    array(
      '@subset' => $subset,
      '@repository_static_root' => $static_root,
      '@exclude_options' => _tuxfamily_repository_exclude_options(),
    )
  );
  drupal_set_time_limit(120);
  return _tuxfamily_repository_execute_command($command);
}

/**
 * @return the base name of the directory hosting this site's configuration
 * file
 */
function _tuxfamily_repository_site_name() {
  return basename(conf_path());
}

/**
 * @return the base tokens available when interpreting paths configured for
 * this module.
 */
function _tuxfamily_repository_default_tokens() {
  return array(
    '@drupal_root' => DRUPAL_ROOT,
    '@sitename'    => _tuxfamily_repository_site_name(),
    '@project'     => variable_get(TUXFAMILY_REPOSITORY_PROJECT_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_PROJECT),
    '@rsync'       => variable_get(TUXFAMILY_REPOSITORY_RSYNC_PATH_VARIABLE, TUXFAMILY_REPOSITORY_DEFAULT_RSYNC_PATH),
  );
}

/**
 * @param $string String to be interpreted; tokens from
 * _tuxfamily_repository_default_tokens will be replaced
 * @param $extra_tokens List of extra tokens that should be replaced in $string
 * @return The string with all known and provided tokens replaced
 */
function _tuxfamily_repository_apply_tokens($string, $extra_tokens = array()) {
  $tokens = _tuxfamily_repository_default_tokens() + $extra_tokens;

  foreach ($tokens as $token => $value) {
    $string = str_replace($token, $value, $string);
  }

  return $string;
}

/**
 * @param $command Command to be executed
 * $command_redirections input/output redirections applied to the command (see
 * popen documentation); default to '< /dev/null 2>&1'.
 */
function _tuxfamily_repository_execute_command($command, $command_redirections = '< /dev/null 2>&1') {
  $return = array('command' => $command, 'code' => -1, 'output' => '');
  $proc_res = popen($command . ' ' . $command_redirections, 'r');
  if ($proc_res === FALSE) return $return;
  $return['output'] = stream_get_contents($proc_res);
  $return['code'] = pclose($proc_res);
  return $return;
}