friendship ended with social-app. php is my new best friend
1<?php 2/** 3 * Class FileStorage 4 * 5 * @created 26.03.2024 6 * @author smiley <smiley@chillerlan.net> 7 * @copyright 2024 smiley 8 * @license MIT 9 */ 10declare(strict_types=1); 11 12namespace chillerlan\OAuth\Storage; 13 14use chillerlan\OAuth\OAuthOptions; 15use chillerlan\OAuth\Core\AccessToken; 16use chillerlan\Settings\SettingsContainerInterface; 17use chillerlan\Utilities\{Crypto, Directory, File}; 18use Psr\Log\{LoggerInterface, NullLogger}; 19use DirectoryIterator; 20use Throwable; 21use function dirname, implode, str_starts_with, substr, trim; 22use const DIRECTORY_SEPARATOR; 23 24/** 25 * Implements a filesystem storage adapter. 26 * 27 * Please note that the storage root directory needs permissions 0777 or `is_writable()` will fail. 28 * Subfolders created by this class will have permissions set to 0755. 29 * 30 * @see \is_writable() 31 * @see \chillerlan\OAuth\OAuthOptions::$fileStoragePath 32 */ 33class FileStorage extends OAuthStorageAbstract{ 34 35 final protected const ENCRYPT_FORMAT = Crypto::ENCRYPT_FORMAT_BINARY; 36 37 /** 38 * A *unique* ID to identify the user within your application, e.g. database row id or UUID 39 */ 40 protected string|int $oauthUser; 41 42 /** 43 * OAuthStorageAbstract constructor. 44 */ 45 public function __construct( 46 string|int $oauthUser, 47 OAuthOptions|SettingsContainerInterface $options = new OAuthOptions, 48 LoggerInterface $logger = new NullLogger, 49 ){ 50 parent::__construct($options, $logger); 51 52 $this->oauthUser = trim((string)$oauthUser); 53 54 if($this->oauthUser === ''){ 55 throw new OAuthStorageException('invalid OAuth user'); 56 } 57 58 if(empty($this->options->fileStoragePath)){ 59 throw new OAuthStorageException('no storage path given'); 60 } 61 62 } 63 64 65 /* 66 * Access token 67 */ 68 69 public function storeAccessToken(AccessToken $token, string $provider):static{ 70 $this->saveFile($this->toStorage($token), $this::KEY_TOKEN, $provider); 71 72 return $this; 73 } 74 75 public function getAccessToken(string $provider):AccessToken{ 76 return $this->fromStorage($this->loadFile($this::KEY_TOKEN, $provider)); 77 } 78 79 public function hasAccessToken(string $provider):bool{ 80 return File::exists($this->getFilepath($this::KEY_TOKEN, $provider)); 81 } 82 83 public function clearAccessToken(string $provider):static{ 84 $this->deleteFile($this::KEY_TOKEN, $provider); 85 86 return $this; 87 } 88 89 public function clearAllAccessTokens():static{ 90 $this->deleteAll($this::KEY_TOKEN); 91 92 return $this; 93 } 94 95 96 /* 97 * CSRF state 98 */ 99 100 public function storeCSRFState(string $state, string $provider):static{ 101 102 if($this->options->useStorageEncryption === true){ 103 $state = $this->encrypt($state); 104 } 105 106 $this->saveFile($state, $this::KEY_STATE, $provider); 107 108 return $this; 109 } 110 111 public function getCSRFState(string $provider):string{ 112 $state = $this->loadFile($this::KEY_STATE, $provider); 113 114 if($this->options->useStorageEncryption === true){ 115 return $this->decrypt($state); 116 } 117 118 return $state; 119 } 120 121 public function hasCSRFState(string $provider):bool{ 122 return File::exists($this->getFilepath($this::KEY_STATE, $provider)); 123 } 124 125 public function clearCSRFState(string $provider):static{ 126 $this->deleteFile($this::KEY_STATE, $provider); 127 128 return $this; 129 } 130 131 public function clearAllCSRFStates():static{ 132 $this->deleteAll($this::KEY_STATE); 133 134 return $this; 135 } 136 137 138 /* 139 * PKCE verifier 140 */ 141 142 public function storeCodeVerifier(string $verifier, string $provider):static{ 143 144 if($this->options->useStorageEncryption === true){ 145 $verifier = $this->encrypt($verifier); 146 } 147 148 $this->saveFile($verifier, $this::KEY_VERIFIER, $provider); 149 150 return $this; 151 } 152 153 public function getCodeVerifier(string $provider):string{ 154 $verifier = $this->loadFile($this::KEY_VERIFIER, $provider); 155 156 if($this->options->useStorageEncryption === true){ 157 return $this->decrypt($verifier); 158 } 159 160 return $verifier; 161 } 162 163 public function hasCodeVerifier(string $provider):bool{ 164 return File::exists($this->getFilepath($this::KEY_VERIFIER, $provider)); 165 } 166 167 public function clearCodeVerifier(string $provider):static{ 168 $this->deleteFile($this::KEY_VERIFIER, $provider); 169 170 return $this; 171 } 172 173 public function clearAllCodeVerifiers():static{ 174 $this->deleteAll($this::KEY_VERIFIER); 175 176 return $this; 177 } 178 179 180 /* 181 * Common 182 */ 183 184 /** 185 * fetched the content from a file 186 */ 187 protected function loadFile(string $key, string $provider):string{ 188 $path = $this->getFilepath($key, $provider); 189 190 try{ 191 return File::load($path); 192 } 193 catch(Throwable){ 194 throw new ItemNotFoundException($key); 195 } 196 197 } 198 199 /** 200 * saves the given data to a file 201 */ 202 protected function saveFile(string $data, string $key, string $provider):void{ 203 $path = $this->getFilepath($key, $provider); 204 $dir = dirname($path); 205 206 if(!Directory::exists($dir)){ 207 Directory::create($dir, 0o755, true); // @codeCoverageIgnore 208 } 209 210 File::save($path, $data); 211 } 212 213 /** 214 * deletes an existing file 215 */ 216 protected function deleteFile(string $key, string $provider):void{ 217 File::delete($this->getFilepath($key, $provider)); 218 } 219 220 /** 221 * deletes all matching files 222 */ 223 protected function deleteAll(string $key):void{ 224 foreach(new DirectoryIterator($this->options->fileStoragePath) as $finfo){ 225 $name = $finfo->getFilename(); 226 227 if(!$finfo->isDir() || str_starts_with($name, '.')){ 228 continue; 229 } 230 231 $this->deleteFile($key, $name); 232 } 233 } 234 235 /** 236 * gets the file path for $key (token/state), $provider and the given oauth user ID 237 */ 238 protected function getFilepath(string $key, string $provider):string{ 239 $provider = $this->getProviderName($provider); 240 $hash = Crypto::sha256($provider.$this->oauthUser.$key); 241 $path = [$this->options->fileStoragePath, $provider]; 242 243 for($i = 1; $i <= 2; $i++){ // @todo: subdir depth to options? 244 $path[] = substr($hash, 0, $i); 245 } 246 247 $path[] = $hash; 248 249 return implode(DIRECTORY_SEPARATOR, $path); 250 } 251 252}