. * * Consult LICENSE file for details ************************************************/ class FileStateMachine implements IStateMachine { const SUPPORTED_STATE_VERSION = IStateMachine::STATEVERSION_02; const VERSION = "version"; private $userfilename; private $settingsfilename; private $usermapfilename; /** * Constructor * * Performs some basic checks and initilizes the state directory * * @access public * @throws FatalMisconfigurationException */ public function FileStateMachine() { if (!defined('STATE_DIR')) throw new FatalMisconfigurationException("No configuration for the state directory available."); if (substr(STATE_DIR, -1,1) != "/") throw new FatalMisconfigurationException("The configured state directory should terminate with a '/'"); if (!file_exists(STATE_DIR)) throw new FatalMisconfigurationException("The configured state directory does not exist or can not be accessed: ". STATE_DIR); // checks if the directory exists and tries to create the necessary subfolders if they do not exist $this->getDirectoryForDevice(Request::GetDeviceID()); $this->userfilename = STATE_DIR . 'users'; $this->settingsfilename = STATE_DIR . 'settings'; $this->usermapfilename = STATE_DIR . 'usermap'; if ((!file_exists($this->userfilename) && !touch($this->userfilename)) || !is_writable($this->userfilename)) throw new FatalMisconfigurationException("Not possible to write to the configured state directory."); Utils::FixFileOwner($this->userfilename); } /** * Gets a hash value indicating the latest dataset of the named * state with a specified key and counter. * If the state is changed between two calls of this method * the returned hash should be different * * @param string $devid the device id * @param string $type the state type * @param string $key (opt) * @param string $counter (opt) * * @access public * @return string * @throws StateNotFoundException, StateInvalidException */ public function GetStateHash($devid, $type, $key = false, $counter = false) { $filename = $this->getFullFilePath($devid, $type, $key, $counter); // the filemodification time is enough to track changes if(file_exists($filename)) return filemtime($filename); else throw new StateNotFoundException(sprintf("FileStateMachine->GetStateHash(): Could not locate state '%s'",$filename)); } /** * Gets a state for a specified key and counter. * This method sould call IStateMachine->CleanStates() * to remove older states (same key, previous counters) * * @param string $devid the device id * @param string $type the state type * @param string $key (opt) * @param string $counter (opt) * @param string $cleanstates (opt) * * @access public * @return mixed * @throws StateNotFoundException, StateInvalidException */ public function GetState($devid, $type, $key = false, $counter = false, $cleanstates = true) { if ($counter && $cleanstates) $this->CleanStates($devid, $type, $key, $counter); // Read current sync state $filename = $this->getFullFilePath($devid, $type, $key, $counter); ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->GetState() on file: '%s'", $filename)); if(file_exists($filename)) { return unserialize(file_get_contents($filename)); } // throw an exception on all other states, but not FAILSAVE as it's most of the times not there by default else if ($type !== IStateMachine::FAILSAVE) throw new StateNotFoundException(sprintf("FileStateMachine->GetState(): Could not locate state '%s'",$filename)); } /** * Writes ta state to for a key and counter * * @param mixed $state * @param string $devid the device id * @param string $type the state type * @param string $key (opt) * @param int $counter (opt) * * @access public * @return boolean * @throws StateInvalidException */ public function SetState($state, $devid, $type, $key = false, $counter = false) { $state = serialize($state); $filename = $this->getFullFilePath($devid, $type, $key, $counter); if (($bytes = Utils::safe_put_contents($filename, $state)) === false) throw new FatalMisconfigurationException(sprintf("FileStateMachine->SetState(): Could not write state '%s'",$filename)); ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->SetState() written %d bytes on file: '%s'", $bytes, $filename)); return $bytes; } /** * Cleans up all older states * If called with a $counter, all states previous state counter can be removed * If called without $counter, all keys (independently from the counter) can be removed * * @param string $devid the device id * @param string $type the state type * @param string $key * @param string $counter (opt) * * @access public * @return * @throws StateInvalidException */ public function CleanStates($devid, $type, $key, $counter = false) { $matching_files = glob($this->getFullFilePath($devid, $type, $key). "*", GLOB_NOSORT); if (is_array($matching_files)) { foreach($matching_files as $state) { $file = false; if($counter !== false && preg_match('/([0-9]+)$/', $state, $matches)) { if($matches[1] < $counter) { $candidate = $this->getFullFilePath($devid, $type, $key, (int)$matches[1]); if ($candidate == $state) $file = $candidate; } } else if ($counter === false) $file = $this->getFullFilePath($devid, $type, $key); if ($file !== false) { ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->CleanStates(): Deleting file: '%s'", $file)); unlink ($file); } } } } /** * Links a user to a device * * @param string $username * @param string $devid * * @access public * @return boolean indicating if the user was added or not (existed already) */ public function LinkUserDevice($username, $devid) { $mutex = new SimpleMutex(__FILE__); $changed = false; // exclusive block if ($mutex->Block()) { $filecontents = @file_get_contents($this->userfilename); if ($filecontents) $users = unserialize($filecontents); else $users = array(); // add user/device to the list if (!isset($users[$username])) { $users[$username] = array(); $changed = true; } if (!isset($users[$username][$devid])) { $users[$username][$devid] = 1; $changed = true; } if ($changed) { $bytes = Utils::safe_put_contents($this->userfilename, serialize($users)); ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->LinkUserDevice(): wrote %d bytes to users file", $bytes)); } else ZLog::Write(LOGLEVEL_DEBUG, "FileStateMachine->LinkUserDevice(): nothing changed"); $mutex->Release(); } return $changed; } /** * Unlinks a device from a user * * @param string $username * @param string $devid * * @access public * @return boolean */ public function UnLinkUserDevice($username, $devid) { $mutex = new SimpleMutex(__FILE__); $changed = false; // exclusive block if ($mutex->Block()) { $filecontents = @file_get_contents($this->userfilename); if ($filecontents) $users = unserialize($filecontents); else $users = array(); // is this user listed at all? if (isset($users[$username])) { if (isset($users[$username][$devid])) { unset($users[$username][$devid]); $changed = true; } // if there is no device left, remove the user if (empty($users[$username])) { unset($users[$username]); $changed = true; } } if ($changed) { $bytes = Utils::safe_put_contents($this->userfilename, serialize($users)); ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->UnLinkUserDevice(): wrote %d bytes to users file", $bytes)); } else ZLog::Write(LOGLEVEL_DEBUG, "FileStateMachine->UnLinkUserDevice(): nothing changed"); $mutex->Release(); } return $changed; } /** * Get all UserDevice mapping * * @access public * @return array */ public function GetAllUserDevice() { return unserialize(file_get_contents($this->userfilename))?:array(); } /** * Returns an array with all device ids for a user. * If no user is set, all device ids should be returned * * @param string $username (opt) * * @access public * @return array */ public function GetAllDevices($username = false) { $out = array(); if ($username === false) { foreach (glob(STATE_DIR. "/*/*/*-".IStateMachine::DEVICEDATA, GLOB_NOSORT) as $devdata) if (preg_match('/\/([A-Za-z0-9]+)-'. IStateMachine::DEVICEDATA. '$/', $devdata, $matches)) $out[] = $matches[1]; return $out; } else { $filecontents = file_get_contents($this->userfilename); if ($filecontents) $users = unserialize($filecontents); else $users = array(); // get device list for the user if (isset($users[$username])) return array_keys($users[$username]); else return array(); } } /** * Returns the current version of the state files * * @access public * @return int */ public function GetStateVersion() { if (file_exists($this->settingsfilename)) { $settings = unserialize(file_get_contents($this->settingsfilename)); if (strtolower(gettype($settings) == "string") && strtolower($settings) == '2:1:{s:7:"version";s:1:"2";}') { ZLog::Write(LOGLEVEL_INFO, "Broken state version file found. Attempt to autofix it. See https://jira.zarafa.com/browse/ZP-493 for more information."); unlink($this->settingsfilename); $this->SetStateVersion(IStateMachine::STATEVERSION_02); $settings = array(self::VERSION => IStateMachine::STATEVERSION_02); } } else { $filecontents = @file_get_contents($this->userfilename); if ($filecontents) $settings = array(self::VERSION => IStateMachine::STATEVERSION_01); else { $settings = array(self::VERSION => self::SUPPORTED_STATE_VERSION); $this->SetStateVersion(self::SUPPORTED_STATE_VERSION); } } return $settings[self::VERSION]; } /** * Sets the current version of the state files * * @param int $version the new supported version * * @access public * @return boolean */ public function SetStateVersion($version) { if (file_exists($this->settingsfilename)) $settings = unserialize(file_get_contents($this->settingsfilename)); else $settings = array(self::VERSION => IStateMachine::STATEVERSION_01); $settings[self::VERSION] = $version; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->SetStateVersion() saving supported state version, value '%d'", $version)); $status = Utils::safe_put_contents($this->settingsfilename, serialize($settings)); Utils::FixFileOwner($this->settingsfilename); return $status; } /** * Returns all available states for a device id * * @param string $devid the device id * * @access public * @return array(mixed) */ public function GetAllStatesForDevice($devid) { $types = array(IStateMachine::DEVICEDATA, IStateMachine::FOLDERDATA, IStateMachine::FAILSAVE, IStateMachine::HIERARCHY, IStateMachine::BACKENDSTORAGE); $typematch = implode("|", $types); $out = array(); $devdir = $this->getDirectoryForDevice($devid) . "/$devid-"; foreach (glob($devdir . "*", GLOB_NOSORT) as $devdata) { $str = substr($devdata, strlen($devdir)-1); $matches = array(); if (!preg_match("/^(?:-(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}))?(?:-($typematch))?(?:-(\d+))?$/", $str, $matches)) throw new Exception(sprintf("GetAllStatesForDevice(): didn't match the regexp !!!: %s", $str)); $out[] = array( 'uuid' => (isset($matches[1]) ? $matches[1] : false), 'type' => (isset($matches[2]) ? $matches[2] : false), 'counter' => (isset($matches[3]) ? $matches[3] : false), ); } return $out; } /** * Return if the User-Device has permission to sync against this Z-Push. * * @param string $user Username * @param string $devid DeviceId * * @access public * @return integer */ public function GetUserDevicePermission($user, $devid) { $mutex = new SimpleMutex(); $status = SYNC_COMMONSTATUS_SUCCESS; $userFile = STATE_DIR . 'PreAuthUserDevices'; if ($mutex->Block()) { if (@file_exists($userFile)) { $userList = json_decode(@file_get_contents($userFile), true); } else { $userList = Array(); } // Android PROVISIONING initial step // LG-D802 is sending an empty deviceid if ($devid != "validate" && $devid != "") { $changed = false; if (array_key_exists($user, $userList)) { // User already pre-authorized // User could be blocked if a "authorized" device exist and it's false if (!$userList[$user]["authorized"]) { $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked user '%s', tried '%s'", $user, $devid)); } else { if (array_key_exists($devid, $userList[$user])) { // Device pre-authorized found if ($userList[$user][$devid] === false) { $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked device '%s' for user '%s'", $devid, $user)); } else { ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized device '%s' for user '%s'", $devid, $user)); } } else { // Device not pre-authorized if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { $userList[$user][$devid] = true; $changed = true; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for user '%s'", $devid, $user)); } else { $status = SYNC_COMMONSTATUS_MAXDEVICESREACHED; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Max number of devices reached for user '%s', tried '%s'", $user, $devid)); } } else { $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; $userList[$user][$devid] = false; $changed = true; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new device '%s' for user '%s'", $devid, $user)); } } } } else { // User not pre-authorized if (defined('PRE_AUTHORIZE_NEW_USERS') && PRE_AUTHORIZE_NEW_USERS === true) { $userList[$user] = array("authorized" => true); if (defined('PRE_AUTHORIZE_NEW_DEVICES') && PRE_AUTHORIZE_NEW_DEVICES === true) { if (defined('PRE_AUTHORIZE_MAX_DEVICES') && PRE_AUTHORIZE_MAX_DEVICES >= count($userList[$user])) { $userList[$user][$devid] = true; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Pre-authorized new device '%s' for new user '%s'", $devid, $user)); } } else { $status = SYNC_COMMONSTATUS_DEVICEBLOCKEDFORUSER; $userList[$user][$devid] = false; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new device '%s' for new user '%s'", $devid, $user)); } $changed = true; } else { $status = SYNC_COMMONSTATUS_USERDISABLEDFORSYNC; $userList[$user] = array("authorized" => false, $devid => false); $changed = true; ZLog::Write(LOGLEVEL_INFO, sprintf("FileStateMachine->GetUserDevicePermission(): Blocked new user '%s' and device '%s'", $user, $devid)); } } if ($changed) { file_put_contents($userFile, json_encode($userList)); } } $mutex->Release(); } return $status; } /**---------------------------------------------------------------------------------------------------------- * Private FileStateMachine stuff */ /** * Returns the full path incl. filename for a key (generally uuid) and a counter * * @param string $devid the device id * @param string $type the state type * @param string $key (opt) * @param string $counter (opt) default false * @param boolean $doNotCreateDirs (opt) indicates if missing subdirectories should be created, default false * * @access private * @return string * @throws StateInvalidException */ private function getFullFilePath($devid, $type, $key = false, $counter = false, $doNotCreateDirs = false) { $testkey = $devid . (($key !== false)? "-". $key : "") . (($type !== "")? "-". $type : ""); if (preg_match('/^[a-zA-Z0-9-]+$/', $testkey, $matches) || ($type == "" && $key === false)) $internkey = $testkey . (($counter && is_int($counter))?"-".$counter:""); else throw new StateInvalidException("FileStateMachine->getFullFilePath(): Invalid state deviceid, type, key or in any combination"); return $this->getDirectoryForDevice($devid, $doNotCreateDirs) ."/". $internkey; } /** * Checks if the configured path exists and if a subfolder structure is available * A two level deep subdirectory structure is build to save the states. * The subdirectories where to save, are determined with device id * * @param string $devid the device id * @param boolen $doNotCreateDirs (opt) by default false - indicates if the subdirs should be created * * @access private * @return string/boolean returns the full directory of false if the dirs can not be created * @throws FatalMisconfigurationException when configured directory is not writeable */ private function getDirectoryForDevice($devid, $doNotCreateDirs = false) { $firstLevel = substr(strtolower($devid), -1, 1); $secondLevel = substr(strtolower($devid), -2, 1); $dir = STATE_DIR . $firstLevel . "/" . $secondLevel; if (is_dir($dir)) return $dir; if ($doNotCreateDirs === false) { // try to create the subdirectory structure necessary $fldir = STATE_DIR . $firstLevel; if (!is_dir($fldir)) { $dirOK = mkdir($fldir); if (!$dirOK) throw new FatalMisconfigurationException("FileStateMachine->getDirectoryForDevice(): Not possible to create state sub-directory: ". $fldir); } if (!is_dir($dir)) { $dirOK = mkdir($dir); if (!$dirOK) throw new FatalMisconfigurationException("FileStateMachine->getDirectoryForDevice(): Not possible to create state sub-directory: ". $dir); } else return $dir; } return false; } /** * Retrieves the mapped username for a specific username and backend. * * @param string $username The username to lookup * @param string $backend Name of the backend to lookup * * @return string The mapped username or null if none found */ public function GetMappedUsername($username, $backend) { $mutex = new SimpleMutex(); // exclusive block if ($mutex->Block()) { // Read current mapping $filecontents = @file_get_contents($this->usermapfilename); if ($filecontents) $mapping = unserialize($filecontents); else $mapping = array(); $mutex->Release(); } // Find mapping $key = $username . '/' . $backend; if (isset($mapping[$key])) { return $mapping[$key]; } return null; } /** * Maps a username for a specific backend to another username. * * @param string $username The username to map * @param string $backend Name of the backend * @param string $mappedname The mappend username * * @return boolean */ public function MapUsername($username, $backend, $mappedname) { $mutex = new SimpleMutex(); // exclusive block if ($mutex->Block()) { // Read current mapping $filecontents = @file_get_contents($this->usermapfilename); if ($filecontents) $mapping = unserialize($filecontents); else $mapping = array(); // Map username + backend to the mapped username $key = $username . '/' . $backend; $mapping[$key] = $mappedname; // Write mapping file $bytes = file_put_contents($this->usermapfilename, serialize($mapping)); if ($bytes === false) { ZLog::Write(LOGLEVEL_ERROR, "Unable to write to mapping file"); return false; } ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->MapUsername(): wrote %d bytes to mapping file", $bytes)); $mutex->Release(); } return true; } /** * Unmaps a username for a specific backend. * * @param string $username The username to unmap * @param string $backend Name of the backend * * @return boolean */ public function UnmapUsername($username, $backend) { $mutex = new SimpleMutex(); // exclusive block if ($mutex->Block()) { // Read current mapping $filecontents = @file_get_contents($this->usermapfilename); if ($filecontents) $mapping = unserialize($filecontents); else $mapping = array(); // Unmap username + backend $key = $username . '/' . $backend; if (!isset($mapping[$key])) { ZLog::Write(LOGLEVEL_INFO, "Username and backend not found in mapping file"); return false; } unset($mapping[$key]); // Write mapping file $bytes = file_put_contents($this->usermapfilename, serialize($mapping)); if ($bytes === false) { ZLog::Write(LOGLEVEL_ERROR, "Unable to write to mapping file"); return false; } ZLog::Write(LOGLEVEL_DEBUG, sprintf("FileStateMachine->UnmapUsername(): wrote %d bytes to mapping file", $bytes)); $mutex->Release(); } return true; } }