1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/z-push_ynh.git synced 2024-09-03 18:05:58 +02:00
z-push_ynh/sources/lib/default/filestatemachine.php

705 lines
No EOL
28 KiB
PHP

<?php
/***********************************************
* File : filestatemachine.php
* Project : Z-Push
* Descr : This class handles state requests;
* Each Import/Export mechanism can
* store its own state information,
* which is stored through the
* state machine.
*
* Created : 01.10.2007
*
* Copyright 2007 - 2013 Zarafa Deutschland GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation with the following additional
* term according to sec. 7:
*
* According to sec. 7 of the GNU Affero General Public License, version 3,
* the terms of the AGPL are supplemented with the following terms:
*
* "Zarafa" is a registered trademark of Zarafa B.V.
* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
* The licensing of the Program under the AGPL does not imply a trademark license.
* Therefore any rights, title and interest in our trademarks remain entirely with us.
*
* However, if you propagate an unmodified version of the Program you are
* allowed to use the term "Z-Push" to indicate that you distribute the Program.
* Furthermore you may use our trademarks where it is necessary to indicate
* the intended purpose of a product or service provided you use it in accordance
* with honest practices in industrial or commercial matters.
* If you want to propagate modified versions of the Program under the name "Z-Push",
* you may only do so if you have a written permission by Zarafa Deutschland GmbH
* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* 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;
}
}