You are here

connector.module in Connector 6

Same filename and directory in other branches
  1. 7 connector.module

Connector module

File

connector.module
View source
<?php

/**
 * @file
 * Connector module
 */

//TODO: Make the user edit page more themable by other modules/themes - they should be able to rearrange or hide parts of it

//TODO: Check whether 'invalidate old info' is set or not in hook_requirements()?

//TODO: Enable user to remove itself/it's connection from the site?

//TODO: React on disabling and uninstalling of another connector module

//TODO: Show indication of the connection when logged in?

//TODO: Is there really a need for a separate avatar callback?

//TODO: Make it possible to upload an icon for a button?

//TODO: Make it possible to disable default login, create account etc?

//TODO: Always refresh info on log in? Because we will most certainly have access to the info then. Need to make sure not to refresh too often though

//TODO: Show status of connections to at least the admin - do that on eg. the user's profile page

//TODO: Should the backing of really be in this module?

//TODO: Move syncing of info to a hook-system that everyone can hook into?

//TODO: Provide more hooks!

//TODO: Remove $uid from info callbacks?

/**
 * Implementation of hook_theme().
 */
function connector_theme() {
  return array(
    'connector_buttons' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
    'connector_connections_list_tableselect' => array(
      'arguments' => array(
        'form' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_menu().
 */
function connector_menu() {
  $items = array();
  $items['user/%user_category/edit/connector'] = array(
    'title' => 'Connections',
    'page callback' => 'connector_user_settings',
    'page arguments' => array(
      1,
    ),
    //TODO: Change the access check to a Connector specific permission?
    'access callback' => 'user_edit_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'load arguments' => array(
      '%map',
      '%index',
    ),
    'file' => 'connector.pages.inc',
  );
  return $items;
}

/**
 * Implementation of hook_init().
 */
function connector_init() {
  drupal_add_css(drupal_get_path('module', 'connector') . '/connector.css', 'module');
}

/**
 * Implementation of hook_cron().
 */
function connector_cron() {

  //TODO: If we don't have time to refresh all data - just remove the ones we didn't have time to refresh?
  while (_connector_cron_time()) {
    $result = db_query_range("SELECT uid, type, max_life FROM {connector_info} WHERE max_life < %d ORDER BY max_life ASC", array(
      ':time' => time(),
    ), 0, 20);
    if ($info = db_fetch_object($result)) {
      do {
        _connector_information_update($info->uid, array(
          $info->type => $info->max_life,
        ));
      } while ($info = db_fetch_object($result));
    }
    else {
      break;
    }
  }
}

/**
 * Implementation of hook_block().
 */
function connector_block($op = 'list', $delta = 0) {
  global $user;
  if ($op == 'list') {
    $block['one_click_block']['info'] = t('Connector');
    return $block;
  }
  elseif ($op == 'view') {
    switch ($delta) {
      case 'one_click_block':
        if (!$user->uid) {
          return array(
            'content' => drupal_get_form('connector_button_form'),
          );
        }
        break;
    }
  }
}

/**
 * Implementation of hook_user().
 */
function connector_user($op, &$edit, &$user, $category = NULL) {
  switch ($op) {
    case 'delete':
      $connectors = _connector_get_connectors();
      $connections = _connector_get_user_connections($user);
      foreach ($connections as $connection) {
        if (array_key_exists($connection->connector, $connectors)) {
          $connector = $connectors[$connection->connector];
          if (isset($connector['delete callback']) && is_callable($connector['delete callback'])) {
            call_user_func($connector['delete callback'], $connector, $connection->cid);
          }
        }
      }
      db_query('DELETE FROM {connector_info} WHERE uid = %d', array(
        ':uid' => $user->uid,
      ));
      db_query('DELETE FROM {connector_user} WHERE uid = %d', array(
        ':uid' => $user->uid,
      ));
      break;
    case 'logout':
      $connectors = _connector_get_connectors();
      $connections = _connector_get_user_connections($user);
      foreach ($connections as $connection) {
        if (array_key_exists($connection->connector, $connectors)) {
          $connector = $connectors[$connection->connector];
          if (isset($connector['logout callback']) && is_callable($connector['logout callback'])) {
            call_user_func($connector['logout callback'], $connector, $connection->cid);
          }
        }
      }
      break;
    case 'categories':
      return array(
        array(
          'name' => 'connector',
          'title' => 'Connections',
          'weight' => 3,
        ),
      );
      break;
  }
}

/**
 * Implementation of hook_realname().
 */
function connector_realname() {
  return array(
    'name' => 'Connector',
    'types' => FALSE,
    'fields' => FALSE,
    'cache' => FALSE,
  );
}

/**
 * Implementation of hook_realname_make().
 */
function connector_realname_make($account) {
  $info = _connector_information_fetch($account, array(
    'real name' => TRUE,
  ));
  if ($info['real name'] === FALSE) {
    return NULL;
  }
  elseif (empty($info['real name'])) {
    return t('Hidden name');
  }
  return $info['real name'];
}

/**
 * Implementation of hook_views_api().
 */
function connector_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'connector') . '/includes',
  );
}
function connector_button_form(&$form_state, $account = FALSE) {
  $form = array(
    '#theme' => 'connector_buttons',
  );
  $i = 0;
  $connectors = _connector_get_connectors();
  if ($account && $account->uid != 0) {
    $callback = 'connect button callback';
  }
  else {
    $callback = 'button callback';
  }
  foreach ($connectors as $key => $connector) {
    if (isset($connector[$callback]) && is_callable($connector[$callback])) {
      $form[$key] = array(
        '#type' => 'submit',
        '#value' => t('Connect with !title', array(
          '!title' => $connector['title'],
        )),
        '#submit' => array(
          $connector[$callback],
        ),
        'connector' => array(
          '#type' => 'value',
          '#value' => $connector,
        ),
      );
    }
  }
  return $form;
}
function _connector_cron_time() {
  static $time_limit;
  if (!$time_limit) {
    $max = ini_get('max_execution_time');
    if (!$max) {
      $max = 240;
    }
    $time_limit = time() + 0.15 * $max;

    // However, check for left time, maybe some other cron processing already occured
    $time_limit = min($time_limit, variable_get('cron_semaphore', time()) + $max);
  }
  return max($time_limit - time(), 0);
}
function _connector_get_connectors($connector = NULL) {
  static $connectors;
  if (!isset($connectors)) {
    $connectors = (array) module_invoke_all('connector');

    // Make sure all connectors has a reference to their own name
    foreach (array_keys($connectors) as $key) {
      if (!isset($connectors[$key]['name'])) {
        $connectors[$key]['name'] = $key;
      }
    }
    drupal_alter('connector', $connectors);
  }
  if ($connector) {
    if (array_key_exists($connector, $connectors)) {
      return $connectors[$connector];
    }
    else {
      return FALSE;
    }
  }
  return $connectors;
}
function _connector_get_user_connections($uid) {
  $connectors = array();
  if (is_object($uid)) {
    $uid = $uid->uid;
  }
  $result = db_query("SELECT authname FROM {authmap} WHERE module = 'connector' AND uid = %d", $uid);
  while ($row = db_fetch_object($result)) {
    $row = explode('__', $row->authname, 2);
    if (count($row) === 2) {
      $connectors[] = (object) array(
        'connector' => $row[0],
        'cid' => $row[1],
      );
    }
  }
  return $connectors;
}
function _connector_get_primary_connection($uid) {
  $result = FALSE;
  if (is_object($uid)) {
    $uid = $uid->uid;
  }
  $primary_connection = db_result(db_query("SELECT primary_connection FROM {connector_user} WHERE uid = %d", $uid));
  if ($primary_connection) {
    $row = explode('__', $primary_connection, 2);
    if (count($row) === 2) {
      $result = (object) array(
        'connector' => $row[0],
        'cid' => $row[1],
      );
    }
  }
  return $result;
}
function _connector_set_primary_connection($uid, $connection) {
  if (is_object($uid)) {
    $uid = $uid->uid;
  }
  if (is_object($connection)) {
    $connection = $connection->connector . '__' . $connection->cid;
  }
  db_query("UPDATE {connector_user} SET primary_connection = '%s' WHERE uid = %d", array(
    ':primary_connection' => $connection,
    ':uid' => $uid,
  ));
  if (!db_affected_rows()) {
    db_query("INSERT INTO {connector_user} (uid, primary_connection) VALUES (%d, '%s')", array(
      ':uid' => $uid,
      ':primary_connection' => $connection,
    ));
  }
}
function _connector_log_in($connector_name, $cid = NULL) {
  global $user;
  if (user_is_logged_in()) {
    return FALSE;
  }
  $connector = _connector_get_connectors($connector_name);
  if (!$connector) {
    return FALSE;
  }

  //Fetch connector ID
  if ($cid === NULL && isset($connector['id callback']) && is_callable($connector['id callback'])) {
    $cid = call_user_func($connector['id callback'], $connector);
  }
  if ($cid !== NULL) {
    $authname = $connector_name . '__' . $cid;
    $account = user_external_load($authname);
    if (!$account) {
      if (variable_get('user_register', 1)) {

        // Mostly copied from user_external_login_register - because it doesn't check user_register
        // Register this new user.
        $userinfo = array(
          'name' => $authname,
          'pass' => user_password(),
          'init' => $authname,
          'status' => variable_get('user_register', 1) == 1,
          'access' => time(),
        );
        $new_account = user_save('', $userinfo);

        // Terminate if an error occured during user_save().
        if (!$new_account) {
          drupal_set_message(t("Error saving user account."), 'error');
        }
        else {
          db_query("INSERT INTO {authmap} (uid, authname, module) VALUES (%d, '%s','connector')", $new_account->uid, $authname);
          _connector_set_primary_connection($new_account->uid, $authname);
          _connector_information_update($new_account);
          if ($new_account->status) {
            $user = $new_account;
            return TRUE;
          }
          else {
            drupal_set_message(t('Your account is currently pending approval by the site administrator.'), 'warning');
            if (isset($connector['logout callback']) && is_callable($connector['logout callback'])) {
              call_user_func($connector['logout callback'], $connector, $connection->cid);
            }
          }
          watchdog('user', 'New external user: %name using module %module.', array(
            '%name' => $authname,
            '%module' => 'connector',
          ), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $new_account->uid . '/edit'));
        }
      }
      else {
        drupal_set_message(t('Only site administrators can create new user accounts.'), 'error');
        if (isset($connector['logout callback']) && is_callable($connector['logout callback'])) {
          call_user_func($connector['logout callback'], $connector, $connection->cid);
        }
      }
    }
    else {

      //Log in user
      if ($account->status) {
        $result = user_external_login($account);
        if ($result) {
          return TRUE;
        }
      }
      else {
        drupal_set_message(t('Your account is currently pending approval by the site administrator.'), 'warning');
        if (isset($connector['logout callback']) && is_callable($connector['logout callback'])) {
          call_user_func($connector['logout callback'], $connector, $connection->cid);
        }
      }
    }
  }
  return FALSE;
}
function _connector_add_connection($connector_name, $cid = NULL, $uid = NULL) {
  global $user;
  $connector = _connector_get_connectors($connector_name);
  if (!$connector) {
    return FALSE;
  }
  $result = FALSE;
  if (empty($uid)) {
    $uid = $user->uid;
  }
  else {
    if (is_object($uid)) {
      $uid = $uid->uid;
    }
  }

  //Fetch connector ID
  if ($cid === NULL && isset($connector['id callback']) && is_callable($connector['id callback'])) {
    $cid = call_user_func($connector['id callback'], $connector);
  }

  // Check that we have an external id to connect with
  if ($cid !== NULL) {
    $authname = $connector_name . '__' . $cid;
    $account = user_external_load($authname);

    // Check if the external id already connected to someone
    if (!$account) {
      $result = (bool) db_query("INSERT INTO {authmap} (uid, authname, module) VALUES (%d, '%s','connector')", $uid, $authname);
      if (!_connector_get_primary_connection($uid)) {
        _connector_set_primary_connection($uid, $authname);
      }
    }
    else {
      $result = TRUE;
    }
  }
  return $result;
}
function _connector_information_fetch($uid, $types = NULL, $update = TRUE, $reset = FALSE) {

  //TODO: Use $types if more info is added for a user
  static $cache;
  if (is_object($uid)) {
    $uid = $uid->uid;
  }
  if (empty($cache)) {
    $cache = array();
  }
  if ($reset) {
    unset($cache[$uid]);
    return;
  }
  elseif (!isset($cache[$uid])) {
    $result = db_result(db_query("SELECT value FROM {connector_info} WHERE uid = %d AND type = 'real name'", array(
      ':uid' => $uid,
    )));
    if ($result === FALSE && $update) {
      _connector_information_update($uid, array(
        'real name' => TRUE,
      ));
      $result = db_result(db_query("SELECT value FROM {connector_info} WHERE uid = %d AND type = 'real name'", array(
        ':uid' => $uid,
      )));
    }
    $cache[$uid] = array(
      'real name' => $result,
    );
  }
  return $cache[$uid];
}
function _connector_information_update($uid, $types = NULL) {

  //TODO: Configure more types of information that we want fetched?
  if (is_object($uid)) {
    $uid = $uid->uid;
  }
  $connection = _connector_get_primary_connection($uid);
  $connector = isset($connection) ? _connector_get_connectors($connection->connector) : FALSE;
  if ($connector) {
    if (isset($connector['information callback']) && is_callable($connector['information callback'])) {
      $info = call_user_func($connector['information callback'], $connector, $connection->cid, $types);
    }
    $info = $info ? (array) $info : array();
    foreach ((array) $types as $type => $value) {
      if (!empty($value) && !array_key_exists($type, $info) && $type != 'avatar') {
        $info[$type] = FALSE;
      }
    }
    if (empty($types) || !empty($types['avatar'])) {
      if (variable_get('user_pictures', 0) && isset($connector['avatar callback']) && is_callable($connector['avatar callback'])) {
        $avatar = call_user_func($connector['avatar callback'], $connector, $connection->cid);
      }
      else {
        $avatar = NULL;
      }
    }
    if (!empty($connector['cache'])) {
      $max_life = time() + $connector['cache'];
    }
    else {

      //TODO: Make default cache time configurable?
      $max_life = time() + 432000;

      //5 days x 24 hours per day x 3600 seconds per hour = 432000
    }
  }
  if (!empty($info)) {
    if (array_key_exists('real name', $info)) {
      $real_name = (object) array(
        'uid' => $uid,
        'type' => 'real name',
      );
      $existing = db_fetch_object(db_query("SELECT value, failure_level FROM {connector_info} WHERE uid = %d AND type = 'real name'", array(
        ':uid' => $uid,
      )));
      if ($info['real name'] === FALSE) {
        $real_name->max_life = intval(time() + 30 * 60 * pow(5.6346264945, $existing->failure_level));

        // 30 minutes * 60 seconds per minute * failure level 4 = delay of 3 weeks
        if ($existing->failure_level < 4) {
          $real_name->failure_level = $existing->failure_level + 1;
        }
      }
      else {
        $real_name->max_life = $max_life;
        if ($existing && $existing->failure_level > 0) {
          $real_name->failure_level = 0;
        }
      }
      if ($info['real name'] !== FALSE || !empty($connector['invalidate old info'])) {
        $real_name->value = empty($info['real name']) ? '' : $info['real name'];

        //TODO: Empty should be saved as NULL?
      }
      if ($existing !== FALSE) {
        if ($existing->value == $info['real name']) {
          unset($real_name->value);
        }
        drupal_write_record('connector_info', $real_name, array(
          'uid',
          'type',
        ));
      }
      else {
        drupal_write_record('connector_info', $real_name);
      }
      _connector_information_fetch($uid, NULL, FALSE, TRUE);
    }
  }
  if (isset($avatar)) {
    if ($avatar !== FALSE || !empty($connector['invalidate old info'])) {
      $account = user_load($uid);
      if (isset($account->picture) && $account->picture != $destination && file_exists($account->picture)) {
        file_delete($account->picture);
      }
    }
    $avatar_record = (object) array(
      'uid' => $uid,
      'type' => 'avatar',
    );
    $existing = db_fetch_object(db_query("SELECT failure_level FROM {connector_info} WHERE uid = %d AND type = 'avatar'", array(
      ':uid' => $uid,
    )));
    if ($avatar === FALSE) {
      $avatar_record->max_life = time() + 30 * pow(5.6346264945, $existing->failure_level);

      // Failure level 4 = delay of 3 weeks
      if ($existing->failure_level < 4) {
        $avatar_record->failure_level = $avatar_record->failure_level + 1;
      }
    }
    elseif ($existing->failure_level > 0) {
      $avatar_record->max_life = $max_life;
      $avatar_record->failure_level = 0;
    }
    if (db_result(db_query("SELECT COUNT(*) FROM {connector_info} WHERE uid = %d AND type = 'avatar'", array(
      ':uid' => $uid,
    )))) {
      drupal_write_record('connector_info', $avatar_record, array(
        'uid',
        'type',
      ));
    }
    else {
      drupal_write_record('connector_info', $avatar_record);
    }
    if (!empty($avatar)) {
      $result = drupal_http_request($avatar);
      if ($result->code != 200) {
        watchdog('connector', 'Failed importing avatar for user @uid, code : @code', array(
          '@uid' => $uid,
          '@code' => $result->code,
        ));
      }
      else {

        //Copied from file_save_data - needs to write the file before validating it
        $temp = file_directory_temp();

        // On Windows, tempnam() requires an absolute path, so we use realpath().
        $tmp_file = tempnam(realpath($temp), 'file');
        if (!($fp = fopen($tmp_file, 'wb'))) {
          drupal_set_message(t('The file could not be created.'), 'error');
        }
        else {
          fwrite($fp, $result->data);
          fclose($fp);
          $file = new stdClass();
          $file->filename = file_munge_filename(trim(basename($tmp_file), '.'), 'jpg jpeg gif png', FALSE);
          $file->filepath = $tmp_file;
          $file->filemime = file_get_mimetype($tmp_file);
          $file->filesize = filesize($tmp_file);
          $errors = array();
          $errors += file_validate_is_image($file);
          $errors += file_validate_image_resolution($file, variable_get('user_picture_dimensions', '85x85'));
          $errors += file_validate_size($file, variable_get('user_picture_file_size', '30') * 1024);
          if (empty($errors)) {
            $info = image_get_info($file->filepath);
            $destination = file_create_path(variable_get('user_picture_path', 'pictures'));
            file_check_directory($destination, FILE_CREATE_DIRECTORY);
            if (file_copy($file, $destination . '/picture-' . $uid . '.' . $info['extension'], FILE_EXISTS_REPLACE)) {
              user_save($account, array(
                'picture' => $file->filepath,
              ));
            }
          }
          file_delete($tmp_file);
        }
      }
    }
  }
}
function theme_connector_buttons($form) {
  $output = '';
  $buttons = array();
  foreach ($form as $key => $value) {
    if (drupal_substr($key, 0, 1) != '#') {
      $buttons[$key] = $value;
    }
  }
  $num_links = count($buttons);
  $i = 1;
  foreach ($buttons as $key => $value) {
    $class = 'connector-' . str_replace('_', '-', $key);
    if ($i == 1) {
      $class .= ' first';
    }
    if ($i == $num_links) {
      $class .= ' last';
    }
    $output .= '<li' . drupal_attributes(array(
      'class' => $class,
    )) . '>';
    $output .= drupal_render($value);
    $output .= '</li>';
    $i += 1;
  }
  return '<ul class="connector-buttons">' . $output . '</ul>';
}