<commit_before><?php
/**
 * class SessionManager
 * Implements MySQL-backed PHP session handling
 * for persist session data and with memcache
 * for the session data.
 * @package Framework
 * @subpackage Utils
 */
/*
 * Table creation code:
 * <code>
 * CREATE TABLE usersession (
 *   fl_uid      char(32)            PRIMARY KEY,
 *   data        blob,
 *   touched     timestamp,
 *   ip_addr     int(10)    unsigned NOT NULL,
 *   create_time int(10)    unsigned NOT NULL,
 *   KEY touched (touched)
 * );
 * </code>
 */

class SessionManager {
  var $cache_obj;                    // session memcache object
  var $cookiename = "flsid";         // session cookie name
  var $session_cache_expire = 3600;  // session memcache expiration in seconds
  var $crc;                          // crc value of the $_SESSION["persist"]
  var $fluid;                        // permenant session ID (done via cookie)
  var $has_db_record = false;        // flag on whether this user has an existing persist record
  var $flsid;                        // temporary session ID (actual session id)
  var $flssid;                       // unique ID for each script session
  var $counter = 0;                  // counter (should be a static variable) 
  var $persist;                      // pointers to memory areas within $_SESSION 
  var $temporary;
  var $is_new_user;                  // flag to indicate if this is a new user (no fl-uid) 
  var $is_new_session = 0;           // determine if it is a new session 

  protected static $instance;
  public static function singleton($args=NULL) { $name = __CLASS__; if (!self::$instance) { self::$instance = new $name($args); } return self::$instance;  }

  function __construct() {
    $this->init();
  }
  
  function init() {
    // TODO - this needs to implement custom session handler functions before we can go live
    /*
    session_set_save_handler(
      array(&$this, 'open'),
      array(&$this, 'close'),
      array(&$this, 'read'),
      array(&$this, 'write'),
      array(&$this, 'destroy'),
      array(&$this, 'gc')
    );
    */
    // set the session cookie name
    session_name($this->cookiename);

    // set the cache limiter to 'private' - keeps us from sending Pragma: no-cache
    session_cache_limiter('private');

    // initiate sessionization
    session_start();
  }
}<commit_msg>- Start port of sessionmanager ...<commit_after><?php
/**
 * class SessionManager
 * Implements MySQL-backed PHP session handling
 * for persist session data and with memcache
 * for the session data.
 * @package Framework
 * @subpackage Utils
 */

/*
 * Table creation code:
 * <code>
 * CREATE TABLE usersession (
 *   fl_uid      char(32)            PRIMARY KEY,
 *   data        blob,
 *   touched     timestamp,
 *   ip_addr     int(10)    unsigned NOT NULL,
 *   create_time int(10)    unsigned NOT NULL,
 *   KEY touched (touched)
 * );
 * </code>
 */

class SessionManager
{
  public $cache_obj;                    // session memcache object
  public $cookiename = "flsid";         // session cookie name
  public $session_cache_expire = 3600;  // session memcache expiration in seconds
  public $crc;                          // crc value of the $_SESSION["persist"]
  public $fluid;                        // permenant session ID (done via cookie)
  public $has_db_record = false;        // flag on whether this user has an existing persist record
  public $flsid;                        // temporary session ID (actual session id)
  public $flssid;                       // unique ID for each script session
  public $counter = 0;                  // counter (should be a static publiciable)
  public $persist;                      // pointers to memory areas within $_SESSION
  public $temporary;
  public $is_new_user;                  // flag to indicate if this is a new user (no fl-uid)
  public $is_new_session = 0;           // determine if it is a new session

  /**
   * List of domains for which we want to serve cookies at the top level for (ie, thefind.com)
   * Everything else uses the FQDN (ie, www.glimpse.com, shop.glimpse.com, etc)
   * @fixme - these should be specified in a config somewhere
   */
  public static $domains_toplevel = array("thefind.com",
                                          "thefind.co.uk",
                                          "fatlens.com",
                                          "im2-inc.com"
                                          );

  protected static $instance;

  protected function __construct()
  {
    global $webapp;
    /**
     * If this page is one of the pages to serve the widgets, do not start
     * a session.  Log the impression to the widget
      log.
     */
    $widget_pages = array("widget_findit.fhtml", "widget_marketplace.fhtml", "widget_search.fhtml", "widget_tickets.fhtml");
    $is_widget = false;

    for ($i=0; $i<count($widget_pages); $i++) {
      if ( (strpos($webapp->request["path"], $widget_pages[$i]) !== false) ) {
        /**
         * @todo:
         * Not an elegant solution, but it's the easier to implement for now.
         * For the widget preview mode, the information is passed in the session.
         * So, we must init the session if mode=preview.
         */
        if ($webapp->request["args"]["preview_mode"] != "yes") {
          $is_widget = true;
        }
        continue;
      }
    }
    if ($is_widget == false) {
      $this->init();
    }
  }

  /**
   * singleton function to return
   * the instance of the class
   *
   * @return SessionManager
   */
  public static function singleton()
  {
    if (!self::$instance) {
      self::$instance = new SessionManager();
    }
    return self::$instance;
  }

  /**
   * Initialize the session.
   * Start the session.
   */
  protected function init()
  {
    //some al-qada shit here... 
    global $webapp;

    $this->data = DataManager::singleton();
    //Profiler::StartTimer("SessionManager::Init()");

    if ($this->data->caches["memcache"]["session"] !== NULL) {
      $cfgmgr = ConfigManager::singleton();
      $this->cache_obj = $this->data->caches["memcache"]["session"];
      //$this->session_cache_expire = $this->data->caches["memcache"]["session"]->lifetime;
    } else {
      // Continue anyway even if cannot connect to memcache.
      // Point the cache_obj to NoCache object
      //print_pre($this->data);
      Logger::Error("SessionManager::init() - Cannot connect to session memcache - " . $this->data->sources);
      $this->cache_obj =& NoCache::singleton();
    }

    // instantiate the pandora object
    $pandora = PandoraLog::singleton();

    // check to see if there is an existing cookie for flsid
    $has_flsid = (isset($_COOKIE['flsid']) || isset($_REQUEST['flsid']));
    $this->is_new_session = ($has_flsid == 1) ? 0 : 1;

    session_set_save_handler(
      array(&$this, 'open'),
      array(&$this, 'close'),
      array(&$this, 'read'),
      array(&$this, 'write'),
      array(&$this, 'destroy'),
      array(&$this, 'gc')
    );
    // register_shutdown_function('session_write_close');

    // figure out domain to set fl-uid cookie for (TLD for thefind/fatlens/im2-inc.com, FQDN for all others)
    // FIXME - this should use $this->getDomain()
    if ( (strpos($_SERVER['HTTP_HOST'], "thefind.com") === false) &&
         (strpos($_SERVER['HTTP_HOST'], "fatlens.com") === false) &&
         (strpos($_SERVER['HTTP_HOST'], "im2-inc.com") == false)) {
      $domain = $_SERVER['HTTP_HOST'];
    } else {
      $tmp = explode(".", $_SERVER['HTTP_HOST']);
      $tmp_size = count($tmp);
      $domain = "." . $tmp[$tmp_size-2] . "." . $tmp[$tmp_size-1];
    }
    //session_set_cookie_params(0, "/", $domain);
    session_set_cookie_params(0, "/"); // 10-30-08: switched session cookie to use fqdn rather than just top-level domain

    // set the garbage collection lifetime (on the DB persist data)
    ini_set('session.gc_maxlifetime', 31536000); // 31536000 = 60 * 60 * 24 * 365

    // set the session cookie name
    session_name($this->cookiename);

    // set the cache limiter to 'private' - keeps us from sending Pragma: no-cache
    session_cache_limiter('private');

    // initiate sessionization
    session_start();

    /**
     * Read the permanent session ID from cookie.
     * If there isn't one, create one.
     */

    // read the permanent cookie
    if (isset($_COOKIE['fl-uid']))
      $fluid_str = $_COOKIE['fl-uid'];
    else if (isset($_SESSION['fluid']))
      $fluid_str = $_SESSION['fluid'];
    else if (isset($_REQUEST['fluid']))
      $fluid_str = $_REQUEST['fluid'];

    $fluid_data = explode(",", $fluid_str);
    $this->fluid = $fluid_data[0];
    $this->session_count = $fluid_data[1]?$fluid_data[1]:0;
    $this->last_access = $fluid_data[2]?$fluid_data[2]:0;
    $this->first_session_for_day = 0;
    $this->days_since_last_session = 0;
    $this->is_new_user = 0;
    if (empty($this->fluid)) {
      // create new permanent cookie
      $this->is_new_user = 1;
      $this->fluid = $this->generate_fluid();
      $this->session_count = 0;
      $this->last_access = time();
      $this->first_session_for_day = 1;
      $this->days_since_last_session = 0;
      $fluid_data = $this->fluid.','.$this->session_count.','.$this->last_access;
      //setcookie('fl-uid', $fluid_data, time()+31536000, "/", $domain); // (1 year)
      $pdata_serialize = serialize($_SESSION["persist"]);
      $this->crc = strlen($pdata_serialize) . crc32($pdata_serialize);
    }
    if (!$has_flsid) {
      // new session -- update the permanent cookie

			// Deal with errant mis-named 'fl_uid' cookies...
			// (this code can safely be removed in a year or so, e.g. February 2010)
			// ^^^ perhaps not?
      if ($fluid_data_recover = explode(",", $_COOKIE['fl_uid'])) {
        $this->fluid = any($this->fluid, $fluid_data_recover[0]);
        $this->session_count = $fluid_data_recover[1];
        $this->last_access = $fluid_data_recover[2];
        //setcookie('fl_uid', '', 0, "/", $domain); // delete the errant cookie
      }

      $this->session_count++;
      if (time() > ($this->last_access+86400)) {
        $this->days_since_last_session = floor((time() - $this->last_access)/86400);
        $this->last_access = time();
        $this->first_session_for_day = 1;
      }
      $fluid_data = $this->fluid.','.$this->session_count.','.$this->last_access;
      setcookie('fl-uid', $fluid_data, time()+31536000, "/", $domain); // (update the permanent cookie)
    }



    // debugging
    // print "<hr>In SessionManager::init() - cache_obj extended stats = " . print_pre($this->cache_obj->GetExtendedStats());

    /**
     * First, check to see if there is an entry in the memcache for this fluid.
     * If so, use it.
     * If not, query the DB to get the persistent session data.
     * If not in the DB, create a new session.
     */
    $this->flsid = session_id();
    $session_memcache = $this->cache_obj->get($this->flsid);
    //$tmpsession = unserialize($session_memcache);

    if (!empty($session_memcache["fluid"]))
      $this->fluid = $session_memcache["fluid"];

    if (empty($session_memcache) && !$this->is_new_user) {
      $result = $this->data->Query("db.userdata.usersession.{$this->flsid}:nocache",
                                   "SELECT data FROM usersession.usersession WHERE fl_uid=:fl_uid LIMIT 1",
                                   array(":fl_uid" => $this->fluid));
      if ($result && $result->NumResults() == 1) {
        $data = $result->GetResult(0);
        $_SESSION["persist"] = unserialize($data->data);
        $this->has_db_record = true;
        if (! $_SESSION["persist"]["has_db_record"]) {
          $_SESSION["persist"]["has_db_record"] = true;
        }
      }
    } else {
      $_SESSION = $session_memcache;
      //Logger::Error(print_r($_SESSION, true));
    }

    $this->has_db_record = ($_SESSION["persist"]["has_db_record"]) ? true : false;

    // Store permenant session id, actual (temporary) session id,
    // and create pointers to $_SESSION memory space
    $this->flsid = session_id();
    $this->persist =& $_SESSION["persist"];
    $this->temporary =& $_SESSION["temporary"];

    // get from the persist session data if this is a new or registered user
    $userid = $_SESSION["persist"]["user"]["userid"];
    $usertype = $_SESSION["persist"]["user"]["usertype"];

    $userTypeArray = User::$usertypes;
    $pandoraUserTypeNum = any($userTypeArray[$usertype], 0);

    // log this into Pandora
    $pandora_session = array(
      "timestamp"           => time(),
      "session_id"          => $this->flsid,
      "fluid"               => $this->fluid,
      "is_new_user"         => $this->is_new_user,
      "is_registered_user"  => ($userid) ? 1 : 0,
      "ip_addr"             => $_SERVER['REMOTE_ADDR'],
      "user_agent"          => $_SERVER['HTTP_USER_AGENT'],
      "referrer_url"        => $_SERVER['HTTP_REFERER'],
      "landing_page_url"    => "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
      "referrer_id"         => $webapp->request["args"]["rid"],
      "widget_id"           => $webapp->request["args"]["wid"],
      "user_registration_id"=> "$pandoraUserTypeNum.$userid",
      "version"             => $webapp->version,
      "cobrand"             => $this->root->cobrand,
      "first_session_for_day" => $this->first_session_for_day,
      "session_count"       => $this->session_count,
      "days_since_last_session" => $this->days_since_last_session
    );

    // log this session for data warehouse (once per session)
    if (!$has_flsid) {
      //store the session start time in session
      $_SESSION['session_start_time'] = time();

      $pandora->addData("session", $pandora_session);
    }
    else {
      Logger::Notice("Pandora: Session already has an flsid. Current start time from session is: " . var_export($_SESSION['session_start_time'], true));

      if(!array_key_exists('session_start_time', $_SESSION)) {
        $_SESSION['session_start_time'] = time();
        Logger::Warn("Pandora: Set or reset the session start time as one did not exist before. Time is: " . var_export($_SESSION['session_start_time'], true));
      }
      else {
        Logger::Notice("Pandora: Session start time was not reset. Time is: " . var_export($_SESSION['session_start_time'], true));
      }
    }

    //save session data once per session
//    if(!array_key_exists('pandora_session_data_added', $_SESSION)) {
//      Logger::Warn('FUCKING PANDORA SESSION SHITE BBBBBBBBBBBBBBBBBBBBBBBB' . var_export($pandora_session, true));
//      Logger::Warn("Got session!!!!!!!!!!!!!!!!!!!!: " . $pandoraUserTypeNum);
//      $pandora->addData("session", $pandora_session);
//      $_SESSION['pandora_session_data_added'] = true;
//    }

    $_SESSION["fluid"] = $this->fluid;
    // set the script session ID
    $this->flssid = $this->generate_fluid();

    //Profiler::StopTimer("SessionManager::Init()");
  }

  /**
   * Create a new persist record in the userdata.usersssion table
   */
  protected function create_new_persist_record($force=false)
  {
    // if bot, don't create a db record
    if (isBot()) {
      return;
    }
    // insert a record into userdata.usersession
    if ($force || !empty($_SESSION["persist"])) {
      $_SESSION["persist"]["has_db_record"] = true;
      $pdata_serialize = serialize($_SESSION["persist"]);
      $ip = $_SERVER['REMOTE_ADDR'];
      Logger::Warn("Creating session in database");
      $result = $this->data->Query("db.userdata.usersession.{$this->flsid}:nocache",
                                   "INSERT INTO usersession.usersession (fl_uid, data, ip_addr, create_time) "
                                   . " VALUES (:fluid, :data, INET_ATON(:ip_addr), UNIX_TIMESTAMP(NOW()))",
                                   array(":fluid"=>$this->fluid, ":data"=>$pdata_serialize, ":ip_addr"=>$ip));
      // set the $_SESSION
      $this->has_db_record = true;
    }
  }

  /**
   * mysql session open handler
   * (no override)
   *
   * @param string file path (ignored)
   * @param string session name (ignored)
   */
  public function open($save_path, $session_name) {}

  /**
   * session close requirement
   * Persist the data warehouse log data.
   */
  public  function close()
  {
    //$this->data->Quit();

    Profiler::StartTimer("Pandora", 1);
    $pandora = PandoraLog::singleton();
    if ($pandora->getFlag() == false) {
      // if pandora logging is not turned on, return
      return;
    }
    // exclude the internal server crawls and testings
    $exclude_user_agents = array("im2-");
    for ($i=0; $i<count($exclude_user_agents); $i++) {
      if ( (strpos($_SERVER['HTTP_USER_AGENT'], $exclude_user_agents[$i]) !== false) ) {
      	return;
      }
    }
    // process the pandora logging
    $pandora = PandoraLog::singleton();
    $pandora->writeLog();
    Profiler::StopTimer("Pandora");
  }

  /**
   * mysql session read handler.
   * (no override)
   *
   * @param  string id to read data for
   * @return string data for session
   */
  public function read($id)
  {
    // calculate the crc value of this for comparison later
    $cacheentry = $this->cache_obj->get($id);
    if ($cacheentry instanceOf CacheEntry)
      $data = $cacheentry->getPayload();
    else
      $data = $cacheentry;
    //$data_unserialize = unserialize($data);
    $pdata_serialize = serialize($data['persist']);
    if (empty($data['persist']) || is_null($data['persist'])) {
      $this->crc = 0;
    } else {
      $this->crc = strlen($pdata_serialize) . crc32($pdata_serialize);
    }
    return $data;
  }

  /**
   * mysql session write handler.
   * Persist the entire portion of $_SESSION["persist"] to DB.
   * Refresh memcache with $_SESSION.
   *
   * @param string session id
   * @param string data to write
   */
  public function write($id, $data)
  {
    Profiler::StartTimer("SessionManager::write", 1);
    // Save user, and all associated lists, settings, etc.
    Profiler::StartTimer("SessionManager::write - save user");
    $user = User::singleton();
    $user->save();
    Profiler::StopTimer("SessionManager::write - save user");
    // compare the persist data before and after for changes
    $pdata = $_SESSION["persist"];
    $pdata_serialize = serialize($pdata);

    $data = $this->cache_obj->get($id);
    $pdata_before = $data["persist"];
    $pdata_before_serialize = serialize($pdata_before);

    // update the memcache with the entire $_SESSION
    //$session_serialize = serialize($_SESSION);
    $this->cache_obj->set($id, $_SESSION, 0);

    if (empty($pdata) || is_null($pdata)) {
      $new_crc = 0;
    } else {
      $new_crc = strlen($pdata_serialize) . crc32($pdata_serialize);
    }

    if ($this->crc != $new_crc) {
      // if the user does not have a record already, create one first
      // NOTE from James - removed  '&& (! $pdata["user"]["userid"]' from below, didn't seem to make sense...
      if ($this->has_db_record == false) {
        $this->create_new_persist_record();
        // need to set the session cache again with we set the has_db_record param
        //$session_serialize = serialize($_SESSION);
        $this->cache_obj->set($id, $_SESSION, 0, $this->session_cache_expire);
      } else {
        $result = $this->data->Query("db.userdata.usersession.{$id}:nocache",
                                     "UPDATE usersession.usersession SET data=:data WHERE fl_uid=:fl_uid",
                                     array(":data" => $pdata_serialize, ":fl_uid"  => $this->fluid)
                                     );
        Logger::Info("Updating userdata.usersession record for $this->fluid");
      }
    }
    Profiler::StopTimer("SessionManager::write");
  }

  /**
   * mysql session destroy handler.
   * Remove the session from the memcache.
   *
   * @param string $id - session id
   */
  public function destroy($id)
  {
    $this->cache_obj->delete($id);
  }

  /**
   * mysql session garbage collection handler
   *
   * @param integer how many seconds-old sessions to keep
   */
  public function gc($maxlifetime)
  {
    $touched_limit = time() - $maxlifetime;
    $result = $this->data->Query("db.userdata.usersession:nocache",
                                 "DELETE FROM usersession.usersession WHERE touched < :touched",
                                 array(":touched" => $touched_limit));
  }

  /**
   * Create a unique fluid
   * to save as a cookie to store permenant
   * session data.
   *
   */
  protected function generate_fluid()
  {
    return md5(uniqid($_SERVER['SERVER_NAME'] . mt_rand(), true));
  }

  /**
   * Generate GUID based on the counter and the script session ID
   */
  protected function generate_guid()
  {
    return $this->flssid . ":" . str_pad($this->counter++, 4, "0", STR_PAD_LEFT);
  }

  /**
   * Get domain name formated like ".thefind.com", ".thefind.co.uk"
   * @return string
   */
  public function getDomain()
  {
    global $webapp;
    $host = any($webapp->request["host"], $_SERVER['HTTP_HOST']);
    foreach (self::$domains_toplevel as $tld) {
      $regex = "/(?:^|\.)" . preg_quote($tld) . "$/";
      if (preg_match($regex, $host, $domain)) {
        return "." . $tld;
      }
    }
    return $host;
  }
}
{'fraction_non_alphanumeric': 0.14962370649106302, 'fraction_numerical': 0.006538099717779868, 'mean_word_length': 2.5559458103361767, 'pattern_counts': {'":': 8, '<': 13, '<?xml version=': 0, '>': 158, 'https://': 0, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 19, 'profanity_word_list.json': 1, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6252785', 'n_tokens_mistral': 6702, 'n_tokens_neox': 6484, 'n_words': 1637}