Passed
Push — master ( 48bb54...fe33e9 )
by John
13:16 queued 11s
created
apps/user_ldap/lib/User/Manager.php 1 patch
Indentation   +201 added lines, -201 removed lines patch added patch discarded remove patch
@@ -47,205 +47,205 @@
 block discarded – undo
47 47
  * cache
48 48
  */
49 49
 class Manager {
50
-	protected ?Access $access = null;
51
-	protected IConfig $ocConfig;
52
-	protected IDBConnection $db;
53
-	protected IUserManager $userManager;
54
-	protected INotificationManager $notificationManager;
55
-	protected FilesystemHelper $ocFilesystem;
56
-	protected LoggerInterface $logger;
57
-	protected Image $image;
58
-	protected IAvatarManager $avatarManager;
59
-	/** @var CappedMemoryCache<User> $usersByDN */
60
-	protected CappedMemoryCache $usersByDN;
61
-	/** @var CappedMemoryCache<User> $usersByUid */
62
-	protected CappedMemoryCache $usersByUid;
63
-	private IManager $shareManager;
64
-
65
-	public function __construct(
66
-		IConfig $ocConfig,
67
-		FilesystemHelper $ocFilesystem,
68
-		LoggerInterface $logger,
69
-		IAvatarManager $avatarManager,
70
-		Image $image,
71
-		IUserManager $userManager,
72
-		INotificationManager $notificationManager,
73
-		IManager $shareManager
74
-	) {
75
-		$this->ocConfig = $ocConfig;
76
-		$this->ocFilesystem = $ocFilesystem;
77
-		$this->logger = $logger;
78
-		$this->avatarManager = $avatarManager;
79
-		$this->image = $image;
80
-		$this->userManager = $userManager;
81
-		$this->notificationManager = $notificationManager;
82
-		$this->usersByDN = new CappedMemoryCache();
83
-		$this->usersByUid = new CappedMemoryCache();
84
-		$this->shareManager = $shareManager;
85
-	}
86
-
87
-	/**
88
-	 * Binds manager to an instance of Access.
89
-	 * It needs to be assigned first before the manager can be used.
90
-	 * @param Access
91
-	 */
92
-	public function setLdapAccess(Access $access) {
93
-		$this->access = $access;
94
-	}
95
-
96
-	/**
97
-	 * @brief creates an instance of User and caches (just runtime) it in the
98
-	 * property array
99
-	 * @param string $dn the DN of the user
100
-	 * @param string $uid the internal (owncloud) username
101
-	 * @return \OCA\User_LDAP\User\User
102
-	 */
103
-	private function createAndCache($dn, $uid) {
104
-		$this->checkAccess();
105
-		$user = new User($uid, $dn, $this->access, $this->ocConfig,
106
-			$this->ocFilesystem, clone $this->image, $this->logger,
107
-			$this->avatarManager, $this->userManager,
108
-			$this->notificationManager);
109
-		$this->usersByDN[$dn] = $user;
110
-		$this->usersByUid[$uid] = $user;
111
-		return $user;
112
-	}
113
-
114
-	/**
115
-	 * removes a user entry from the cache
116
-	 * @param $uid
117
-	 */
118
-	public function invalidate($uid) {
119
-		if (!isset($this->usersByUid[$uid])) {
120
-			return;
121
-		}
122
-		$dn = $this->usersByUid[$uid]->getDN();
123
-		unset($this->usersByUid[$uid]);
124
-		unset($this->usersByDN[$dn]);
125
-	}
126
-
127
-	/**
128
-	 * @brief checks whether the Access instance has been set
129
-	 * @throws \Exception if Access has not been set
130
-	 * @return null
131
-	 */
132
-	private function checkAccess() {
133
-		if (is_null($this->access)) {
134
-			throw new \Exception('LDAP Access instance must be set first');
135
-		}
136
-	}
137
-
138
-	/**
139
-	 * returns a list of attributes that will be processed further, e.g. quota,
140
-	 * email, displayname, or others.
141
-	 *
142
-	 * @param bool $minimal - optional, set to true to skip attributes with big
143
-	 * payload
144
-	 * @return string[]
145
-	 */
146
-	public function getAttributes($minimal = false) {
147
-		$baseAttributes = array_merge(Access::UUID_ATTRIBUTES, ['dn', 'uid', 'samaccountname', 'memberof']);
148
-		$attributes = [
149
-			$this->access->getConnection()->ldapExpertUUIDUserAttr,
150
-			$this->access->getConnection()->ldapQuotaAttribute,
151
-			$this->access->getConnection()->ldapEmailAttribute,
152
-			$this->access->getConnection()->ldapUserDisplayName,
153
-			$this->access->getConnection()->ldapUserDisplayName2,
154
-			$this->access->getConnection()->ldapExtStorageHomeAttribute,
155
-		];
156
-
157
-		$homeRule = (string)$this->access->getConnection()->homeFolderNamingRule;
158
-		if (strpos($homeRule, 'attr:') === 0) {
159
-			$attributes[] = substr($homeRule, strlen('attr:'));
160
-		}
161
-
162
-		if (!$minimal) {
163
-			// attributes that are not really important but may come with big
164
-			// payload.
165
-			$attributes = array_merge(
166
-				$attributes,
167
-				$this->access->getConnection()->resolveRule('avatar')
168
-			);
169
-		}
170
-
171
-		$attributes = array_reduce($attributes,
172
-			function ($list, $attribute) {
173
-				$attribute = strtolower(trim((string)$attribute));
174
-				if (!empty($attribute) && !in_array($attribute, $list)) {
175
-					$list[] = $attribute;
176
-				}
177
-
178
-				return $list;
179
-			},
180
-			$baseAttributes // hard-coded, lower-case, non-empty attributes
181
-		);
182
-
183
-		return $attributes;
184
-	}
185
-
186
-	/**
187
-	 * Checks whether the specified user is marked as deleted
188
-	 * @param string $id the Nextcloud user name
189
-	 * @return bool
190
-	 */
191
-	public function isDeletedUser($id) {
192
-		$isDeleted = $this->ocConfig->getUserValue(
193
-			$id, 'user_ldap', 'isDeleted', 0);
194
-		return (int)$isDeleted === 1;
195
-	}
196
-
197
-	/**
198
-	 * creates and returns an instance of OfflineUser for the specified user
199
-	 * @param string $id
200
-	 * @return \OCA\User_LDAP\User\OfflineUser
201
-	 */
202
-	public function getDeletedUser($id) {
203
-		return new OfflineUser(
204
-			$id,
205
-			$this->ocConfig,
206
-			$this->access->getUserMapper(),
207
-			$this->shareManager
208
-		);
209
-	}
210
-
211
-	/**
212
-	 * @brief returns a User object by it's Nextcloud username
213
-	 * @param string $id the DN or username of the user
214
-	 * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
215
-	 */
216
-	protected function createInstancyByUserName($id) {
217
-		//most likely a uid. Check whether it is a deleted user
218
-		if ($this->isDeletedUser($id)) {
219
-			return $this->getDeletedUser($id);
220
-		}
221
-		$dn = $this->access->username2dn($id);
222
-		if ($dn !== false) {
223
-			return $this->createAndCache($dn, $id);
224
-		}
225
-		return null;
226
-	}
227
-
228
-	/**
229
-	 * @brief returns a User object by it's DN or Nextcloud username
230
-	 * @param string $id the DN or username of the user
231
-	 * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
232
-	 * @throws \Exception when connection could not be established
233
-	 */
234
-	public function get($id) {
235
-		$this->checkAccess();
236
-		if (isset($this->usersByDN[$id])) {
237
-			return $this->usersByDN[$id];
238
-		} elseif (isset($this->usersByUid[$id])) {
239
-			return $this->usersByUid[$id];
240
-		}
241
-
242
-		if ($this->access->stringResemblesDN($id)) {
243
-			$uid = $this->access->dn2username($id);
244
-			if ($uid !== false) {
245
-				return $this->createAndCache($id, $uid);
246
-			}
247
-		}
248
-
249
-		return $this->createInstancyByUserName($id);
250
-	}
50
+    protected ?Access $access = null;
51
+    protected IConfig $ocConfig;
52
+    protected IDBConnection $db;
53
+    protected IUserManager $userManager;
54
+    protected INotificationManager $notificationManager;
55
+    protected FilesystemHelper $ocFilesystem;
56
+    protected LoggerInterface $logger;
57
+    protected Image $image;
58
+    protected IAvatarManager $avatarManager;
59
+    /** @var CappedMemoryCache<User> $usersByDN */
60
+    protected CappedMemoryCache $usersByDN;
61
+    /** @var CappedMemoryCache<User> $usersByUid */
62
+    protected CappedMemoryCache $usersByUid;
63
+    private IManager $shareManager;
64
+
65
+    public function __construct(
66
+        IConfig $ocConfig,
67
+        FilesystemHelper $ocFilesystem,
68
+        LoggerInterface $logger,
69
+        IAvatarManager $avatarManager,
70
+        Image $image,
71
+        IUserManager $userManager,
72
+        INotificationManager $notificationManager,
73
+        IManager $shareManager
74
+    ) {
75
+        $this->ocConfig = $ocConfig;
76
+        $this->ocFilesystem = $ocFilesystem;
77
+        $this->logger = $logger;
78
+        $this->avatarManager = $avatarManager;
79
+        $this->image = $image;
80
+        $this->userManager = $userManager;
81
+        $this->notificationManager = $notificationManager;
82
+        $this->usersByDN = new CappedMemoryCache();
83
+        $this->usersByUid = new CappedMemoryCache();
84
+        $this->shareManager = $shareManager;
85
+    }
86
+
87
+    /**
88
+     * Binds manager to an instance of Access.
89
+     * It needs to be assigned first before the manager can be used.
90
+     * @param Access
91
+     */
92
+    public function setLdapAccess(Access $access) {
93
+        $this->access = $access;
94
+    }
95
+
96
+    /**
97
+     * @brief creates an instance of User and caches (just runtime) it in the
98
+     * property array
99
+     * @param string $dn the DN of the user
100
+     * @param string $uid the internal (owncloud) username
101
+     * @return \OCA\User_LDAP\User\User
102
+     */
103
+    private function createAndCache($dn, $uid) {
104
+        $this->checkAccess();
105
+        $user = new User($uid, $dn, $this->access, $this->ocConfig,
106
+            $this->ocFilesystem, clone $this->image, $this->logger,
107
+            $this->avatarManager, $this->userManager,
108
+            $this->notificationManager);
109
+        $this->usersByDN[$dn] = $user;
110
+        $this->usersByUid[$uid] = $user;
111
+        return $user;
112
+    }
113
+
114
+    /**
115
+     * removes a user entry from the cache
116
+     * @param $uid
117
+     */
118
+    public function invalidate($uid) {
119
+        if (!isset($this->usersByUid[$uid])) {
120
+            return;
121
+        }
122
+        $dn = $this->usersByUid[$uid]->getDN();
123
+        unset($this->usersByUid[$uid]);
124
+        unset($this->usersByDN[$dn]);
125
+    }
126
+
127
+    /**
128
+     * @brief checks whether the Access instance has been set
129
+     * @throws \Exception if Access has not been set
130
+     * @return null
131
+     */
132
+    private function checkAccess() {
133
+        if (is_null($this->access)) {
134
+            throw new \Exception('LDAP Access instance must be set first');
135
+        }
136
+    }
137
+
138
+    /**
139
+     * returns a list of attributes that will be processed further, e.g. quota,
140
+     * email, displayname, or others.
141
+     *
142
+     * @param bool $minimal - optional, set to true to skip attributes with big
143
+     * payload
144
+     * @return string[]
145
+     */
146
+    public function getAttributes($minimal = false) {
147
+        $baseAttributes = array_merge(Access::UUID_ATTRIBUTES, ['dn', 'uid', 'samaccountname', 'memberof']);
148
+        $attributes = [
149
+            $this->access->getConnection()->ldapExpertUUIDUserAttr,
150
+            $this->access->getConnection()->ldapQuotaAttribute,
151
+            $this->access->getConnection()->ldapEmailAttribute,
152
+            $this->access->getConnection()->ldapUserDisplayName,
153
+            $this->access->getConnection()->ldapUserDisplayName2,
154
+            $this->access->getConnection()->ldapExtStorageHomeAttribute,
155
+        ];
156
+
157
+        $homeRule = (string)$this->access->getConnection()->homeFolderNamingRule;
158
+        if (strpos($homeRule, 'attr:') === 0) {
159
+            $attributes[] = substr($homeRule, strlen('attr:'));
160
+        }
161
+
162
+        if (!$minimal) {
163
+            // attributes that are not really important but may come with big
164
+            // payload.
165
+            $attributes = array_merge(
166
+                $attributes,
167
+                $this->access->getConnection()->resolveRule('avatar')
168
+            );
169
+        }
170
+
171
+        $attributes = array_reduce($attributes,
172
+            function ($list, $attribute) {
173
+                $attribute = strtolower(trim((string)$attribute));
174
+                if (!empty($attribute) && !in_array($attribute, $list)) {
175
+                    $list[] = $attribute;
176
+                }
177
+
178
+                return $list;
179
+            },
180
+            $baseAttributes // hard-coded, lower-case, non-empty attributes
181
+        );
182
+
183
+        return $attributes;
184
+    }
185
+
186
+    /**
187
+     * Checks whether the specified user is marked as deleted
188
+     * @param string $id the Nextcloud user name
189
+     * @return bool
190
+     */
191
+    public function isDeletedUser($id) {
192
+        $isDeleted = $this->ocConfig->getUserValue(
193
+            $id, 'user_ldap', 'isDeleted', 0);
194
+        return (int)$isDeleted === 1;
195
+    }
196
+
197
+    /**
198
+     * creates and returns an instance of OfflineUser for the specified user
199
+     * @param string $id
200
+     * @return \OCA\User_LDAP\User\OfflineUser
201
+     */
202
+    public function getDeletedUser($id) {
203
+        return new OfflineUser(
204
+            $id,
205
+            $this->ocConfig,
206
+            $this->access->getUserMapper(),
207
+            $this->shareManager
208
+        );
209
+    }
210
+
211
+    /**
212
+     * @brief returns a User object by it's Nextcloud username
213
+     * @param string $id the DN or username of the user
214
+     * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
215
+     */
216
+    protected function createInstancyByUserName($id) {
217
+        //most likely a uid. Check whether it is a deleted user
218
+        if ($this->isDeletedUser($id)) {
219
+            return $this->getDeletedUser($id);
220
+        }
221
+        $dn = $this->access->username2dn($id);
222
+        if ($dn !== false) {
223
+            return $this->createAndCache($dn, $id);
224
+        }
225
+        return null;
226
+    }
227
+
228
+    /**
229
+     * @brief returns a User object by it's DN or Nextcloud username
230
+     * @param string $id the DN or username of the user
231
+     * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null
232
+     * @throws \Exception when connection could not be established
233
+     */
234
+    public function get($id) {
235
+        $this->checkAccess();
236
+        if (isset($this->usersByDN[$id])) {
237
+            return $this->usersByDN[$id];
238
+        } elseif (isset($this->usersByUid[$id])) {
239
+            return $this->usersByUid[$id];
240
+        }
241
+
242
+        if ($this->access->stringResemblesDN($id)) {
243
+            $uid = $this->access->dn2username($id);
244
+            if ($uid !== false) {
245
+                return $this->createAndCache($id, $uid);
246
+            }
247
+        }
248
+
249
+        return $this->createInstancyByUserName($id);
250
+    }
251 251
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Helper.php 1 patch
Indentation   +249 added lines, -249 removed lines patch added patch discarded remove patch
@@ -35,253 +35,253 @@
 block discarded – undo
35 35
 use OCP\IDBConnection;
36 36
 
37 37
 class Helper {
38
-	private IConfig $config;
39
-	private IDBConnection $connection;
40
-	/** @var CappedMemoryCache<string> */
41
-	protected CappedMemoryCache $sanitizeDnCache;
42
-
43
-	public function __construct(IConfig $config,
44
-								IDBConnection $connection) {
45
-		$this->config = $config;
46
-		$this->connection = $connection;
47
-		$this->sanitizeDnCache = new CappedMemoryCache(10000);
48
-	}
49
-
50
-	/**
51
-	 * returns prefixes for each saved LDAP/AD server configuration.
52
-	 *
53
-	 * @param bool $activeConfigurations optional, whether only active configuration shall be
54
-	 * retrieved, defaults to false
55
-	 * @return array with a list of the available prefixes
56
-	 *
57
-	 * Configuration prefixes are used to set up configurations for n LDAP or
58
-	 * AD servers. Since configuration is stored in the database, table
59
-	 * appconfig under appid user_ldap, the common identifiers in column
60
-	 * 'configkey' have a prefix. The prefix for the very first server
61
-	 * configuration is empty.
62
-	 * Configkey Examples:
63
-	 * Server 1: ldap_login_filter
64
-	 * Server 2: s1_ldap_login_filter
65
-	 * Server 3: s2_ldap_login_filter
66
-	 *
67
-	 * The prefix needs to be passed to the constructor of Connection class,
68
-	 * except the default (first) server shall be connected to.
69
-	 *
70
-	 */
71
-	public function getServerConfigurationPrefixes($activeConfigurations = false): array {
72
-		$referenceConfigkey = 'ldap_configuration_active';
73
-
74
-		$keys = $this->getServersConfig($referenceConfigkey);
75
-
76
-		$prefixes = [];
77
-		foreach ($keys as $key) {
78
-			if ($activeConfigurations && $this->config->getAppValue('user_ldap', $key, '0') !== '1') {
79
-				continue;
80
-			}
81
-
82
-			$len = strlen($key) - strlen($referenceConfigkey);
83
-			$prefixes[] = substr($key, 0, $len);
84
-		}
85
-		asort($prefixes);
86
-
87
-		return $prefixes;
88
-	}
89
-
90
-	/**
91
-	 *
92
-	 * determines the host for every configured connection
93
-	 *
94
-	 * @return array an array with configprefix as keys
95
-	 *
96
-	 */
97
-	public function getServerConfigurationHosts() {
98
-		$referenceConfigkey = 'ldap_host';
99
-
100
-		$keys = $this->getServersConfig($referenceConfigkey);
101
-
102
-		$result = [];
103
-		foreach ($keys as $key) {
104
-			$len = strlen($key) - strlen($referenceConfigkey);
105
-			$prefix = substr($key, 0, $len);
106
-			$result[$prefix] = $this->config->getAppValue('user_ldap', $key);
107
-		}
108
-
109
-		return $result;
110
-	}
111
-
112
-	/**
113
-	 * return the next available configuration prefix
114
-	 *
115
-	 * @return string
116
-	 */
117
-	public function getNextServerConfigurationPrefix() {
118
-		$serverConnections = $this->getServerConfigurationPrefixes();
119
-
120
-		if (count($serverConnections) === 0) {
121
-			return 's01';
122
-		}
123
-
124
-		sort($serverConnections);
125
-		$lastKey = array_pop($serverConnections);
126
-		$lastNumber = (int)str_replace('s', '', $lastKey);
127
-		return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
128
-	}
129
-
130
-	private function getServersConfig(string $value): array {
131
-		$regex = '/' . $value . '$/S';
132
-
133
-		$keys = $this->config->getAppKeys('user_ldap');
134
-		$result = [];
135
-		foreach ($keys as $key) {
136
-			if (preg_match($regex, $key) === 1) {
137
-				$result[] = $key;
138
-			}
139
-		}
140
-
141
-		return $result;
142
-	}
143
-
144
-	/**
145
-	 * deletes a given saved LDAP/AD server configuration.
146
-	 *
147
-	 * @param string $prefix the configuration prefix of the config to delete
148
-	 * @return bool true on success, false otherwise
149
-	 */
150
-	public function deleteServerConfiguration($prefix) {
151
-		if (!in_array($prefix, self::getServerConfigurationPrefixes())) {
152
-			return false;
153
-		}
154
-
155
-		$query = $this->connection->getQueryBuilder();
156
-		$query->delete('appconfig')
157
-			->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
158
-			->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
159
-			->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
160
-				'enabled',
161
-				'installed_version',
162
-				'types',
163
-				'bgjUpdateGroupsLastRun',
164
-			], IQueryBuilder::PARAM_STR_ARRAY)));
165
-
166
-		if (empty($prefix)) {
167
-			$query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
168
-		}
169
-
170
-		$deletedRows = $query->execute();
171
-		return $deletedRows !== 0;
172
-	}
173
-
174
-	/**
175
-	 * checks whether there is one or more disabled LDAP configurations
176
-	 */
177
-	public function haveDisabledConfigurations(): bool {
178
-		$all = $this->getServerConfigurationPrefixes(false);
179
-		$active = $this->getServerConfigurationPrefixes(true);
180
-
181
-		return count($all) !== count($active) || count($all) === 0;
182
-	}
183
-
184
-	/**
185
-	 * extracts the domain from a given URL
186
-	 *
187
-	 * @param string $url the URL
188
-	 * @return string|false domain as string on success, false otherwise
189
-	 */
190
-	public function getDomainFromURL($url) {
191
-		$uinfo = parse_url($url);
192
-		if (!is_array($uinfo)) {
193
-			return false;
194
-		}
195
-
196
-		$domain = false;
197
-		if (isset($uinfo['host'])) {
198
-			$domain = $uinfo['host'];
199
-		} elseif (isset($uinfo['path'])) {
200
-			$domain = $uinfo['path'];
201
-		}
202
-
203
-		return $domain;
204
-	}
205
-
206
-	/**
207
-	 * sanitizes a DN received from the LDAP server
208
-	 *
209
-	 * @param array|string $dn the DN in question
210
-	 * @return array|string the sanitized DN
211
-	 */
212
-	public function sanitizeDN($dn) {
213
-		//treating multiple base DNs
214
-		if (is_array($dn)) {
215
-			$result = [];
216
-			foreach ($dn as $singleDN) {
217
-				$result[] = $this->sanitizeDN($singleDN);
218
-			}
219
-			return $result;
220
-		}
221
-
222
-		if (!is_string($dn)) {
223
-			throw new \LogicException('String expected ' . \gettype($dn) . ' given');
224
-		}
225
-
226
-		if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
227
-			return $sanitizedDn;
228
-		}
229
-
230
-		//OID sometimes gives back DNs with whitespace after the comma
231
-		// a la "uid=foo, cn=bar, dn=..." We need to tackle this!
232
-		$sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
233
-
234
-		//make comparisons and everything work
235
-		$sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
236
-
237
-		//escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
238
-		//to use the DN in search filters, \ needs to be escaped to \5c additionally
239
-		//to use them in bases, we convert them back to simple backslashes in readAttribute()
240
-		$replacements = [
241
-			'\,' => '\5c2C',
242
-			'\=' => '\5c3D',
243
-			'\+' => '\5c2B',
244
-			'\<' => '\5c3C',
245
-			'\>' => '\5c3E',
246
-			'\;' => '\5c3B',
247
-			'\"' => '\5c22',
248
-			'\#' => '\5c23',
249
-			'(' => '\28',
250
-			')' => '\29',
251
-			'*' => '\2A',
252
-		];
253
-		$sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
254
-		$this->sanitizeDnCache->set($dn, $sanitizedDn);
255
-
256
-		return $sanitizedDn;
257
-	}
258
-
259
-	/**
260
-	 * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
261
-	 *
262
-	 * @param string $dn the DN
263
-	 * @return string
264
-	 */
265
-	public function DNasBaseParameter($dn) {
266
-		return str_ireplace('\\5c', '\\', $dn);
267
-	}
268
-
269
-	/**
270
-	 * listens to a hook thrown by server2server sharing and replaces the given
271
-	 * login name by a username, if it matches an LDAP user.
272
-	 *
273
-	 * @param array $param contains a reference to a $uid var under 'uid' key
274
-	 * @throws \Exception
275
-	 */
276
-	public static function loginName2UserName($param): void {
277
-		if (!isset($param['uid'])) {
278
-			throw new \Exception('key uid is expected to be set in $param');
279
-		}
280
-
281
-		$userBackend = \OC::$server->get(User_Proxy::class);
282
-		$uid = $userBackend->loginName2UserName($param['uid']);
283
-		if ($uid !== false) {
284
-			$param['uid'] = $uid;
285
-		}
286
-	}
38
+    private IConfig $config;
39
+    private IDBConnection $connection;
40
+    /** @var CappedMemoryCache<string> */
41
+    protected CappedMemoryCache $sanitizeDnCache;
42
+
43
+    public function __construct(IConfig $config,
44
+                                IDBConnection $connection) {
45
+        $this->config = $config;
46
+        $this->connection = $connection;
47
+        $this->sanitizeDnCache = new CappedMemoryCache(10000);
48
+    }
49
+
50
+    /**
51
+     * returns prefixes for each saved LDAP/AD server configuration.
52
+     *
53
+     * @param bool $activeConfigurations optional, whether only active configuration shall be
54
+     * retrieved, defaults to false
55
+     * @return array with a list of the available prefixes
56
+     *
57
+     * Configuration prefixes are used to set up configurations for n LDAP or
58
+     * AD servers. Since configuration is stored in the database, table
59
+     * appconfig under appid user_ldap, the common identifiers in column
60
+     * 'configkey' have a prefix. The prefix for the very first server
61
+     * configuration is empty.
62
+     * Configkey Examples:
63
+     * Server 1: ldap_login_filter
64
+     * Server 2: s1_ldap_login_filter
65
+     * Server 3: s2_ldap_login_filter
66
+     *
67
+     * The prefix needs to be passed to the constructor of Connection class,
68
+     * except the default (first) server shall be connected to.
69
+     *
70
+     */
71
+    public function getServerConfigurationPrefixes($activeConfigurations = false): array {
72
+        $referenceConfigkey = 'ldap_configuration_active';
73
+
74
+        $keys = $this->getServersConfig($referenceConfigkey);
75
+
76
+        $prefixes = [];
77
+        foreach ($keys as $key) {
78
+            if ($activeConfigurations && $this->config->getAppValue('user_ldap', $key, '0') !== '1') {
79
+                continue;
80
+            }
81
+
82
+            $len = strlen($key) - strlen($referenceConfigkey);
83
+            $prefixes[] = substr($key, 0, $len);
84
+        }
85
+        asort($prefixes);
86
+
87
+        return $prefixes;
88
+    }
89
+
90
+    /**
91
+     *
92
+     * determines the host for every configured connection
93
+     *
94
+     * @return array an array with configprefix as keys
95
+     *
96
+     */
97
+    public function getServerConfigurationHosts() {
98
+        $referenceConfigkey = 'ldap_host';
99
+
100
+        $keys = $this->getServersConfig($referenceConfigkey);
101
+
102
+        $result = [];
103
+        foreach ($keys as $key) {
104
+            $len = strlen($key) - strlen($referenceConfigkey);
105
+            $prefix = substr($key, 0, $len);
106
+            $result[$prefix] = $this->config->getAppValue('user_ldap', $key);
107
+        }
108
+
109
+        return $result;
110
+    }
111
+
112
+    /**
113
+     * return the next available configuration prefix
114
+     *
115
+     * @return string
116
+     */
117
+    public function getNextServerConfigurationPrefix() {
118
+        $serverConnections = $this->getServerConfigurationPrefixes();
119
+
120
+        if (count($serverConnections) === 0) {
121
+            return 's01';
122
+        }
123
+
124
+        sort($serverConnections);
125
+        $lastKey = array_pop($serverConnections);
126
+        $lastNumber = (int)str_replace('s', '', $lastKey);
127
+        return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
128
+    }
129
+
130
+    private function getServersConfig(string $value): array {
131
+        $regex = '/' . $value . '$/S';
132
+
133
+        $keys = $this->config->getAppKeys('user_ldap');
134
+        $result = [];
135
+        foreach ($keys as $key) {
136
+            if (preg_match($regex, $key) === 1) {
137
+                $result[] = $key;
138
+            }
139
+        }
140
+
141
+        return $result;
142
+    }
143
+
144
+    /**
145
+     * deletes a given saved LDAP/AD server configuration.
146
+     *
147
+     * @param string $prefix the configuration prefix of the config to delete
148
+     * @return bool true on success, false otherwise
149
+     */
150
+    public function deleteServerConfiguration($prefix) {
151
+        if (!in_array($prefix, self::getServerConfigurationPrefixes())) {
152
+            return false;
153
+        }
154
+
155
+        $query = $this->connection->getQueryBuilder();
156
+        $query->delete('appconfig')
157
+            ->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
158
+            ->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
159
+            ->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
160
+                'enabled',
161
+                'installed_version',
162
+                'types',
163
+                'bgjUpdateGroupsLastRun',
164
+            ], IQueryBuilder::PARAM_STR_ARRAY)));
165
+
166
+        if (empty($prefix)) {
167
+            $query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
168
+        }
169
+
170
+        $deletedRows = $query->execute();
171
+        return $deletedRows !== 0;
172
+    }
173
+
174
+    /**
175
+     * checks whether there is one or more disabled LDAP configurations
176
+     */
177
+    public function haveDisabledConfigurations(): bool {
178
+        $all = $this->getServerConfigurationPrefixes(false);
179
+        $active = $this->getServerConfigurationPrefixes(true);
180
+
181
+        return count($all) !== count($active) || count($all) === 0;
182
+    }
183
+
184
+    /**
185
+     * extracts the domain from a given URL
186
+     *
187
+     * @param string $url the URL
188
+     * @return string|false domain as string on success, false otherwise
189
+     */
190
+    public function getDomainFromURL($url) {
191
+        $uinfo = parse_url($url);
192
+        if (!is_array($uinfo)) {
193
+            return false;
194
+        }
195
+
196
+        $domain = false;
197
+        if (isset($uinfo['host'])) {
198
+            $domain = $uinfo['host'];
199
+        } elseif (isset($uinfo['path'])) {
200
+            $domain = $uinfo['path'];
201
+        }
202
+
203
+        return $domain;
204
+    }
205
+
206
+    /**
207
+     * sanitizes a DN received from the LDAP server
208
+     *
209
+     * @param array|string $dn the DN in question
210
+     * @return array|string the sanitized DN
211
+     */
212
+    public function sanitizeDN($dn) {
213
+        //treating multiple base DNs
214
+        if (is_array($dn)) {
215
+            $result = [];
216
+            foreach ($dn as $singleDN) {
217
+                $result[] = $this->sanitizeDN($singleDN);
218
+            }
219
+            return $result;
220
+        }
221
+
222
+        if (!is_string($dn)) {
223
+            throw new \LogicException('String expected ' . \gettype($dn) . ' given');
224
+        }
225
+
226
+        if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
227
+            return $sanitizedDn;
228
+        }
229
+
230
+        //OID sometimes gives back DNs with whitespace after the comma
231
+        // a la "uid=foo, cn=bar, dn=..." We need to tackle this!
232
+        $sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
233
+
234
+        //make comparisons and everything work
235
+        $sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
236
+
237
+        //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
238
+        //to use the DN in search filters, \ needs to be escaped to \5c additionally
239
+        //to use them in bases, we convert them back to simple backslashes in readAttribute()
240
+        $replacements = [
241
+            '\,' => '\5c2C',
242
+            '\=' => '\5c3D',
243
+            '\+' => '\5c2B',
244
+            '\<' => '\5c3C',
245
+            '\>' => '\5c3E',
246
+            '\;' => '\5c3B',
247
+            '\"' => '\5c22',
248
+            '\#' => '\5c23',
249
+            '(' => '\28',
250
+            ')' => '\29',
251
+            '*' => '\2A',
252
+        ];
253
+        $sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
254
+        $this->sanitizeDnCache->set($dn, $sanitizedDn);
255
+
256
+        return $sanitizedDn;
257
+    }
258
+
259
+    /**
260
+     * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
261
+     *
262
+     * @param string $dn the DN
263
+     * @return string
264
+     */
265
+    public function DNasBaseParameter($dn) {
266
+        return str_ireplace('\\5c', '\\', $dn);
267
+    }
268
+
269
+    /**
270
+     * listens to a hook thrown by server2server sharing and replaces the given
271
+     * login name by a username, if it matches an LDAP user.
272
+     *
273
+     * @param array $param contains a reference to a $uid var under 'uid' key
274
+     * @throws \Exception
275
+     */
276
+    public static function loginName2UserName($param): void {
277
+        if (!isset($param['uid'])) {
278
+            throw new \Exception('key uid is expected to be set in $param');
279
+        }
280
+
281
+        $userBackend = \OC::$server->get(User_Proxy::class);
282
+        $uid = $userBackend->loginName2UserName($param['uid']);
283
+        if ($uid !== false) {
284
+            $param['uid'] = $uid;
285
+        }
286
+    }
287 287
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/AmazonS3.php 1 patch
Indentation   +706 added lines, -706 removed lines patch added patch discarded remove patch
@@ -57,710 +57,710 @@
 block discarded – undo
57 57
 use OCP\ICache;
58 58
 
59 59
 class AmazonS3 extends \OC\Files\Storage\Common {
60
-	use S3ConnectionTrait;
61
-	use S3ObjectTrait;
62
-
63
-	public function needsPartFile() {
64
-		return false;
65
-	}
66
-
67
-	/** @var CappedMemoryCache<array|false> */
68
-	private CappedMemoryCache $objectCache;
69
-
70
-	/** @var CappedMemoryCache<bool> */
71
-	private CappedMemoryCache $directoryCache;
72
-
73
-	/** @var CappedMemoryCache<array> */
74
-	private CappedMemoryCache $filesCache;
75
-
76
-	private IMimeTypeDetector $mimeDetector;
77
-	private ?bool $versioningEnabled = null;
78
-	private ICache $memCache;
79
-
80
-	public function __construct($parameters) {
81
-		parent::__construct($parameters);
82
-		$this->parseParams($parameters);
83
-		$this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
84
-		$this->objectCache = new CappedMemoryCache();
85
-		$this->directoryCache = new CappedMemoryCache();
86
-		$this->filesCache = new CappedMemoryCache();
87
-		$this->mimeDetector = Server::get(IMimeTypeDetector::class);
88
-		/** @var ICacheFactory $cacheFactory */
89
-		$cacheFactory = Server::get(ICacheFactory::class);
90
-		$this->memCache = $cacheFactory->createLocal('s3-external');
91
-	}
92
-
93
-	/**
94
-	 * @param string $path
95
-	 * @return string correctly encoded path
96
-	 */
97
-	private function normalizePath($path) {
98
-		$path = trim($path, '/');
99
-
100
-		if (!$path) {
101
-			$path = '.';
102
-		}
103
-
104
-		return $path;
105
-	}
106
-
107
-	private function isRoot($path) {
108
-		return $path === '.';
109
-	}
110
-
111
-	private function cleanKey($path) {
112
-		if ($this->isRoot($path)) {
113
-			return '/';
114
-		}
115
-		return $path;
116
-	}
117
-
118
-	private function clearCache() {
119
-		$this->objectCache = new CappedMemoryCache();
120
-		$this->directoryCache = new CappedMemoryCache();
121
-		$this->filesCache = new CappedMemoryCache();
122
-	}
123
-
124
-	private function invalidateCache($key) {
125
-		unset($this->objectCache[$key]);
126
-		$keys = array_keys($this->objectCache->getData());
127
-		$keyLength = strlen($key);
128
-		foreach ($keys as $existingKey) {
129
-			if (substr($existingKey, 0, $keyLength) === $key) {
130
-				unset($this->objectCache[$existingKey]);
131
-			}
132
-		}
133
-		unset($this->filesCache[$key]);
134
-		$keys = array_keys($this->directoryCache->getData());
135
-		$keyLength = strlen($key);
136
-		foreach ($keys as $existingKey) {
137
-			if (substr($existingKey, 0, $keyLength) === $key) {
138
-				unset($this->directoryCache[$existingKey]);
139
-			}
140
-		}
141
-		unset($this->directoryCache[$key]);
142
-	}
143
-
144
-	/**
145
-	 * @return array|false
146
-	 */
147
-	private function headObject(string $key) {
148
-		if (!isset($this->objectCache[$key])) {
149
-			try {
150
-				$this->objectCache[$key] = $this->getConnection()->headObject([
151
-					'Bucket' => $this->bucket,
152
-					'Key' => $key
153
-				])->toArray();
154
-			} catch (S3Exception $e) {
155
-				if ($e->getStatusCode() >= 500) {
156
-					throw $e;
157
-				}
158
-				$this->objectCache[$key] = false;
159
-			}
160
-		}
161
-
162
-		if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) {
163
-			/** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
164
-			$this->objectCache[$key]["Key"] = $key;
165
-		}
166
-		return $this->objectCache[$key];
167
-	}
168
-
169
-	/**
170
-	 * Return true if directory exists
171
-	 *
172
-	 * There are no folders in s3. A folder like structure could be archived
173
-	 * by prefixing files with the folder name.
174
-	 *
175
-	 * Implementation from flysystem-aws-s3-v3:
176
-	 * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
177
-	 *
178
-	 * @param $path
179
-	 * @return bool
180
-	 * @throws \Exception
181
-	 */
182
-	private function doesDirectoryExist($path) {
183
-		if ($path === '.' || $path === '') {
184
-			return true;
185
-		}
186
-		$path = rtrim($path, '/') . '/';
187
-
188
-		if (isset($this->directoryCache[$path])) {
189
-			return $this->directoryCache[$path];
190
-		}
191
-		try {
192
-			// Maybe this isn't an actual key, but a prefix.
193
-			// Do a prefix listing of objects to determine.
194
-			$result = $this->getConnection()->listObjectsV2([
195
-				'Bucket' => $this->bucket,
196
-				'Prefix' => $path,
197
-				'MaxKeys' => 1,
198
-			]);
199
-
200
-			if (isset($result['Contents'])) {
201
-				$this->directoryCache[$path] = true;
202
-				return true;
203
-			}
204
-
205
-			// empty directories have their own object
206
-			$object = $this->headObject($path);
207
-
208
-			if ($object) {
209
-				$this->directoryCache[$path] = true;
210
-				return true;
211
-			}
212
-		} catch (S3Exception $e) {
213
-			if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
214
-				$this->directoryCache[$path] = false;
215
-			}
216
-			throw $e;
217
-		}
218
-
219
-
220
-		$this->directoryCache[$path] = false;
221
-		return false;
222
-	}
223
-
224
-	/**
225
-	 * Remove a file or folder
226
-	 *
227
-	 * @param string $path
228
-	 * @return bool
229
-	 */
230
-	protected function remove($path) {
231
-		// remember fileType to reduce http calls
232
-		$fileType = $this->filetype($path);
233
-		if ($fileType === 'dir') {
234
-			return $this->rmdir($path);
235
-		} elseif ($fileType === 'file') {
236
-			return $this->unlink($path);
237
-		} else {
238
-			return false;
239
-		}
240
-	}
241
-
242
-	public function mkdir($path) {
243
-		$path = $this->normalizePath($path);
244
-
245
-		if ($this->is_dir($path)) {
246
-			return false;
247
-		}
248
-
249
-		try {
250
-			$this->getConnection()->putObject([
251
-				'Bucket' => $this->bucket,
252
-				'Key' => $path . '/',
253
-				'Body' => '',
254
-				'ContentType' => FileInfo::MIMETYPE_FOLDER
255
-			]);
256
-			$this->testTimeout();
257
-		} catch (S3Exception $e) {
258
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
259
-			return false;
260
-		}
261
-
262
-		$this->invalidateCache($path);
263
-
264
-		return true;
265
-	}
266
-
267
-	public function file_exists($path) {
268
-		return $this->filetype($path) !== false;
269
-	}
270
-
271
-
272
-	public function rmdir($path) {
273
-		$path = $this->normalizePath($path);
274
-
275
-		if ($this->isRoot($path)) {
276
-			return $this->clearBucket();
277
-		}
278
-
279
-		if (!$this->file_exists($path)) {
280
-			return false;
281
-		}
282
-
283
-		$this->invalidateCache($path);
284
-		return $this->batchDelete($path);
285
-	}
286
-
287
-	protected function clearBucket() {
288
-		$this->clearCache();
289
-		try {
290
-			$this->getConnection()->clearBucket([
291
-				"Bucket" => $this->bucket
292
-			]);
293
-			return true;
294
-			// clearBucket() is not working with Ceph, so if it fails we try the slower approach
295
-		} catch (\Exception $e) {
296
-			return $this->batchDelete();
297
-		}
298
-	}
299
-
300
-	private function batchDelete($path = null) {
301
-		$params = [
302
-			'Bucket' => $this->bucket
303
-		];
304
-		if ($path !== null) {
305
-			$params['Prefix'] = $path . '/';
306
-		}
307
-		try {
308
-			$connection = $this->getConnection();
309
-			// Since there are no real directories on S3, we need
310
-			// to delete all objects prefixed with the path.
311
-			do {
312
-				// instead of the iterator, manually loop over the list ...
313
-				$objects = $connection->listObjects($params);
314
-				// ... so we can delete the files in batches
315
-				if (isset($objects['Contents'])) {
316
-					$connection->deleteObjects([
317
-						'Bucket' => $this->bucket,
318
-						'Delete' => [
319
-							'Objects' => $objects['Contents']
320
-						]
321
-					]);
322
-					$this->testTimeout();
323
-				}
324
-				// we reached the end when the list is no longer truncated
325
-			} while ($objects['IsTruncated']);
326
-			if ($path !== '' && $path !== null) {
327
-				$this->deleteObject($path);
328
-			}
329
-		} catch (S3Exception $e) {
330
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
331
-			return false;
332
-		}
333
-		return true;
334
-	}
335
-
336
-	public function opendir($path) {
337
-		try {
338
-			$content = iterator_to_array($this->getDirectoryContent($path));
339
-			return IteratorDirectory::wrap(array_map(function (array $item) {
340
-				return $item['name'];
341
-			}, $content));
342
-		} catch (S3Exception $e) {
343
-			return false;
344
-		}
345
-	}
346
-
347
-	public function stat($path) {
348
-		$path = $this->normalizePath($path);
349
-
350
-		if ($this->is_dir($path)) {
351
-			$stat = $this->getDirectoryMetaData($path);
352
-		} else {
353
-			$object = $this->headObject($path);
354
-			if ($object === false) {
355
-				return false;
356
-			}
357
-			$stat = $this->objectToMetaData($object);
358
-		}
359
-		$stat['atime'] = time();
360
-
361
-		return $stat;
362
-	}
363
-
364
-	/**
365
-	 * Return content length for object
366
-	 *
367
-	 * When the information is already present (e.g. opendir has been called before)
368
-	 * this value is return. Otherwise a headObject is emitted.
369
-	 *
370
-	 * @param $path
371
-	 * @return int|mixed
372
-	 */
373
-	private function getContentLength($path) {
374
-		if (isset($this->filesCache[$path])) {
375
-			return (int)$this->filesCache[$path]['ContentLength'];
376
-		}
377
-
378
-		$result = $this->headObject($path);
379
-		if (isset($result['ContentLength'])) {
380
-			return (int)$result['ContentLength'];
381
-		}
382
-
383
-		return 0;
384
-	}
385
-
386
-	/**
387
-	 * Return last modified for object
388
-	 *
389
-	 * When the information is already present (e.g. opendir has been called before)
390
-	 * this value is return. Otherwise a headObject is emitted.
391
-	 *
392
-	 * @param $path
393
-	 * @return mixed|string
394
-	 */
395
-	private function getLastModified($path) {
396
-		if (isset($this->filesCache[$path])) {
397
-			return $this->filesCache[$path]['LastModified'];
398
-		}
399
-
400
-		$result = $this->headObject($path);
401
-		if (isset($result['LastModified'])) {
402
-			return $result['LastModified'];
403
-		}
404
-
405
-		return 'now';
406
-	}
407
-
408
-	public function is_dir($path) {
409
-		$path = $this->normalizePath($path);
410
-
411
-		if (isset($this->filesCache[$path])) {
412
-			return false;
413
-		}
414
-
415
-		try {
416
-			return $this->doesDirectoryExist($path);
417
-		} catch (S3Exception $e) {
418
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
419
-			return false;
420
-		}
421
-	}
422
-
423
-	public function filetype($path) {
424
-		$path = $this->normalizePath($path);
425
-
426
-		if ($this->isRoot($path)) {
427
-			return 'dir';
428
-		}
429
-
430
-		try {
431
-			if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
432
-				return 'dir';
433
-			}
434
-			if (isset($this->filesCache[$path]) || $this->headObject($path)) {
435
-				return 'file';
436
-			}
437
-			if ($this->doesDirectoryExist($path)) {
438
-				return 'dir';
439
-			}
440
-		} catch (S3Exception $e) {
441
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
442
-			return false;
443
-		}
444
-
445
-		return false;
446
-	}
447
-
448
-	public function getPermissions($path) {
449
-		$type = $this->filetype($path);
450
-		if (!$type) {
451
-			return 0;
452
-		}
453
-		return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
454
-	}
455
-
456
-	public function unlink($path) {
457
-		$path = $this->normalizePath($path);
458
-
459
-		if ($this->is_dir($path)) {
460
-			return $this->rmdir($path);
461
-		}
462
-
463
-		try {
464
-			$this->deleteObject($path);
465
-			$this->invalidateCache($path);
466
-		} catch (S3Exception $e) {
467
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
468
-			return false;
469
-		}
470
-
471
-		return true;
472
-	}
473
-
474
-	public function fopen($path, $mode) {
475
-		$path = $this->normalizePath($path);
476
-
477
-		switch ($mode) {
478
-			case 'r':
479
-			case 'rb':
480
-				// Don't try to fetch empty files
481
-				$stat = $this->stat($path);
482
-				if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
483
-					return fopen('php://memory', $mode);
484
-				}
485
-
486
-				try {
487
-					return $this->readObject($path);
488
-				} catch (S3Exception $e) {
489
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
490
-					return false;
491
-				}
492
-			case 'w':
493
-			case 'wb':
494
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
495
-
496
-				$handle = fopen($tmpFile, 'w');
497
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
498
-					$this->writeBack($tmpFile, $path);
499
-				});
500
-			case 'a':
501
-			case 'ab':
502
-			case 'r+':
503
-			case 'w+':
504
-			case 'wb+':
505
-			case 'a+':
506
-			case 'x':
507
-			case 'x+':
508
-			case 'c':
509
-			case 'c+':
510
-				if (strrpos($path, '.') !== false) {
511
-					$ext = substr($path, strrpos($path, '.'));
512
-				} else {
513
-					$ext = '';
514
-				}
515
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
516
-				if ($this->file_exists($path)) {
517
-					$source = $this->readObject($path);
518
-					file_put_contents($tmpFile, $source);
519
-				}
520
-
521
-				$handle = fopen($tmpFile, $mode);
522
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
523
-					$this->writeBack($tmpFile, $path);
524
-				});
525
-		}
526
-		return false;
527
-	}
528
-
529
-	public function touch($path, $mtime = null) {
530
-		if (is_null($mtime)) {
531
-			$mtime = time();
532
-		}
533
-		$metadata = [
534
-			'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
535
-		];
536
-
537
-		try {
538
-			if (!$this->file_exists($path)) {
539
-				$mimeType = $this->mimeDetector->detectPath($path);
540
-				$this->getConnection()->putObject([
541
-					'Bucket' => $this->bucket,
542
-					'Key' => $this->cleanKey($path),
543
-					'Metadata' => $metadata,
544
-					'Body' => '',
545
-					'ContentType' => $mimeType,
546
-					'MetadataDirective' => 'REPLACE',
547
-				]);
548
-				$this->testTimeout();
549
-			}
550
-		} catch (S3Exception $e) {
551
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
552
-			return false;
553
-		}
554
-
555
-		$this->invalidateCache($path);
556
-		return true;
557
-	}
558
-
559
-	public function copy($path1, $path2, $isFile = null) {
560
-		$path1 = $this->normalizePath($path1);
561
-		$path2 = $this->normalizePath($path2);
562
-
563
-		if ($isFile === true || $this->is_file($path1)) {
564
-			try {
565
-				$this->getConnection()->copyObject([
566
-					'Bucket' => $this->bucket,
567
-					'Key' => $this->cleanKey($path2),
568
-					'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
569
-				]);
570
-				$this->testTimeout();
571
-			} catch (S3Exception $e) {
572
-				\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
573
-				return false;
574
-			}
575
-		} else {
576
-			$this->remove($path2);
577
-
578
-			try {
579
-				$this->mkdir($path2);
580
-				$this->testTimeout();
581
-			} catch (S3Exception $e) {
582
-				\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
583
-				return false;
584
-			}
585
-
586
-			foreach ($this->getDirectoryContent($path1) as $item) {
587
-				$source = $path1 . '/' . $item['name'];
588
-				$target = $path2 . '/' . $item['name'];
589
-				$this->copy($source, $target, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
590
-			}
591
-		}
592
-
593
-		$this->invalidateCache($path2);
594
-
595
-		return true;
596
-	}
597
-
598
-	public function rename($path1, $path2) {
599
-		$path1 = $this->normalizePath($path1);
600
-		$path2 = $this->normalizePath($path2);
601
-
602
-		if ($this->is_file($path1)) {
603
-			if ($this->copy($path1, $path2) === false) {
604
-				return false;
605
-			}
606
-
607
-			if ($this->unlink($path1) === false) {
608
-				$this->unlink($path2);
609
-				return false;
610
-			}
611
-		} else {
612
-			if ($this->copy($path1, $path2) === false) {
613
-				return false;
614
-			}
615
-
616
-			if ($this->rmdir($path1) === false) {
617
-				$this->rmdir($path2);
618
-				return false;
619
-			}
620
-		}
621
-
622
-		return true;
623
-	}
624
-
625
-	public function test() {
626
-		$this->getConnection()->headBucket([
627
-			'Bucket' => $this->bucket
628
-		]);
629
-		return true;
630
-	}
631
-
632
-	public function getId() {
633
-		return $this->id;
634
-	}
635
-
636
-	public function writeBack($tmpFile, $path) {
637
-		try {
638
-			$source = fopen($tmpFile, 'r');
639
-			$this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
640
-			$this->invalidateCache($path);
641
-
642
-			unlink($tmpFile);
643
-			return true;
644
-		} catch (S3Exception $e) {
645
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
646
-			return false;
647
-		}
648
-	}
649
-
650
-	/**
651
-	 * check if curl is installed
652
-	 */
653
-	public static function checkDependencies() {
654
-		return true;
655
-	}
656
-
657
-	public function getDirectoryContent($directory): \Traversable {
658
-		$path = $this->normalizePath($directory);
659
-
660
-		if ($this->isRoot($path)) {
661
-			$path = '';
662
-		} else {
663
-			$path .= '/';
664
-		}
665
-
666
-		$results = $this->getConnection()->getPaginator('ListObjectsV2', [
667
-			'Bucket' => $this->bucket,
668
-			'Delimiter' => '/',
669
-			'Prefix' => $path,
670
-		]);
671
-
672
-		foreach ($results as $result) {
673
-			// sub folders
674
-			if (is_array($result['CommonPrefixes'])) {
675
-				foreach ($result['CommonPrefixes'] as $prefix) {
676
-					$dir = $this->getDirectoryMetaData($prefix['Prefix']);
677
-					if ($dir) {
678
-						yield $dir;
679
-					}
680
-				}
681
-			}
682
-			if (is_array($result['Contents'])) {
683
-				foreach ($result['Contents'] as $object) {
684
-					$this->objectCache[$object['Key']] = $object;
685
-					if ($object['Key'] !== $path) {
686
-						yield $this->objectToMetaData($object);
687
-					}
688
-				}
689
-			}
690
-		}
691
-	}
692
-
693
-	private function objectToMetaData(array $object): array {
694
-		return [
695
-			'name' => basename($object['Key']),
696
-			'mimetype' => $this->mimeDetector->detectPath($object['Key']),
697
-			'mtime' => strtotime($object['LastModified']),
698
-			'storage_mtime' => strtotime($object['LastModified']),
699
-			'etag' => $object['ETag'],
700
-			'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
701
-			'size' => (int)($object['Size'] ?? $object['ContentLength']),
702
-		];
703
-	}
704
-
705
-	private function getDirectoryMetaData(string $path): ?array {
706
-		$path = trim($path, '/');
707
-		// when versioning is enabled, delete markers are returned as part of CommonPrefixes
708
-		// resulting in "ghost" folders, verify that each folder actually exists
709
-		if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
710
-			return null;
711
-		}
712
-		$cacheEntry = $this->getCache()->get($path);
713
-		if ($cacheEntry instanceof CacheEntry) {
714
-			return $cacheEntry->getData();
715
-		} else {
716
-			return [
717
-				'name' => basename($path),
718
-				'mimetype' => FileInfo::MIMETYPE_FOLDER,
719
-				'mtime' => time(),
720
-				'storage_mtime' => time(),
721
-				'etag' => uniqid(),
722
-				'permissions' => Constants::PERMISSION_ALL,
723
-				'size' => -1,
724
-			];
725
-		}
726
-	}
727
-
728
-	public function versioningEnabled(): bool {
729
-		if ($this->versioningEnabled === null) {
730
-			$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
731
-			if ($cached === null) {
732
-				$this->versioningEnabled = $this->getVersioningStatusFromBucket();
733
-				$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
734
-			} else {
735
-				$this->versioningEnabled = $cached;
736
-			}
737
-		}
738
-		return $this->versioningEnabled;
739
-	}
740
-
741
-	protected function getVersioningStatusFromBucket(): bool {
742
-		try {
743
-			$result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
744
-			return $result->get('Status') === 'Enabled';
745
-		} catch (S3Exception $s3Exception) {
746
-			// This is needed for compatibility with Storj gateway which does not support versioning yet
747
-			if ($s3Exception->getAwsErrorCode() === 'NotImplemented') {
748
-				return false;
749
-			}
750
-			throw $s3Exception;
751
-		}
752
-	}
753
-
754
-	public function hasUpdated($path, $time) {
755
-		// for files we can get the proper mtime
756
-		if ($path !== '' && $object = $this->headObject($path)) {
757
-			$stat = $this->objectToMetaData($object);
758
-			return $stat['mtime'] > $time;
759
-		} else {
760
-			// for directories, the only real option we have is to do a prefix listing and iterate over all objects
761
-			// however, since this is just as expensive as just re-scanning the directory, we can simply return true
762
-			// and have the scanner figure out if anything has actually changed
763
-			return true;
764
-		}
765
-	}
60
+    use S3ConnectionTrait;
61
+    use S3ObjectTrait;
62
+
63
+    public function needsPartFile() {
64
+        return false;
65
+    }
66
+
67
+    /** @var CappedMemoryCache<array|false> */
68
+    private CappedMemoryCache $objectCache;
69
+
70
+    /** @var CappedMemoryCache<bool> */
71
+    private CappedMemoryCache $directoryCache;
72
+
73
+    /** @var CappedMemoryCache<array> */
74
+    private CappedMemoryCache $filesCache;
75
+
76
+    private IMimeTypeDetector $mimeDetector;
77
+    private ?bool $versioningEnabled = null;
78
+    private ICache $memCache;
79
+
80
+    public function __construct($parameters) {
81
+        parent::__construct($parameters);
82
+        $this->parseParams($parameters);
83
+        $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
84
+        $this->objectCache = new CappedMemoryCache();
85
+        $this->directoryCache = new CappedMemoryCache();
86
+        $this->filesCache = new CappedMemoryCache();
87
+        $this->mimeDetector = Server::get(IMimeTypeDetector::class);
88
+        /** @var ICacheFactory $cacheFactory */
89
+        $cacheFactory = Server::get(ICacheFactory::class);
90
+        $this->memCache = $cacheFactory->createLocal('s3-external');
91
+    }
92
+
93
+    /**
94
+     * @param string $path
95
+     * @return string correctly encoded path
96
+     */
97
+    private function normalizePath($path) {
98
+        $path = trim($path, '/');
99
+
100
+        if (!$path) {
101
+            $path = '.';
102
+        }
103
+
104
+        return $path;
105
+    }
106
+
107
+    private function isRoot($path) {
108
+        return $path === '.';
109
+    }
110
+
111
+    private function cleanKey($path) {
112
+        if ($this->isRoot($path)) {
113
+            return '/';
114
+        }
115
+        return $path;
116
+    }
117
+
118
+    private function clearCache() {
119
+        $this->objectCache = new CappedMemoryCache();
120
+        $this->directoryCache = new CappedMemoryCache();
121
+        $this->filesCache = new CappedMemoryCache();
122
+    }
123
+
124
+    private function invalidateCache($key) {
125
+        unset($this->objectCache[$key]);
126
+        $keys = array_keys($this->objectCache->getData());
127
+        $keyLength = strlen($key);
128
+        foreach ($keys as $existingKey) {
129
+            if (substr($existingKey, 0, $keyLength) === $key) {
130
+                unset($this->objectCache[$existingKey]);
131
+            }
132
+        }
133
+        unset($this->filesCache[$key]);
134
+        $keys = array_keys($this->directoryCache->getData());
135
+        $keyLength = strlen($key);
136
+        foreach ($keys as $existingKey) {
137
+            if (substr($existingKey, 0, $keyLength) === $key) {
138
+                unset($this->directoryCache[$existingKey]);
139
+            }
140
+        }
141
+        unset($this->directoryCache[$key]);
142
+    }
143
+
144
+    /**
145
+     * @return array|false
146
+     */
147
+    private function headObject(string $key) {
148
+        if (!isset($this->objectCache[$key])) {
149
+            try {
150
+                $this->objectCache[$key] = $this->getConnection()->headObject([
151
+                    'Bucket' => $this->bucket,
152
+                    'Key' => $key
153
+                ])->toArray();
154
+            } catch (S3Exception $e) {
155
+                if ($e->getStatusCode() >= 500) {
156
+                    throw $e;
157
+                }
158
+                $this->objectCache[$key] = false;
159
+            }
160
+        }
161
+
162
+        if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) {
163
+            /** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
164
+            $this->objectCache[$key]["Key"] = $key;
165
+        }
166
+        return $this->objectCache[$key];
167
+    }
168
+
169
+    /**
170
+     * Return true if directory exists
171
+     *
172
+     * There are no folders in s3. A folder like structure could be archived
173
+     * by prefixing files with the folder name.
174
+     *
175
+     * Implementation from flysystem-aws-s3-v3:
176
+     * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
177
+     *
178
+     * @param $path
179
+     * @return bool
180
+     * @throws \Exception
181
+     */
182
+    private function doesDirectoryExist($path) {
183
+        if ($path === '.' || $path === '') {
184
+            return true;
185
+        }
186
+        $path = rtrim($path, '/') . '/';
187
+
188
+        if (isset($this->directoryCache[$path])) {
189
+            return $this->directoryCache[$path];
190
+        }
191
+        try {
192
+            // Maybe this isn't an actual key, but a prefix.
193
+            // Do a prefix listing of objects to determine.
194
+            $result = $this->getConnection()->listObjectsV2([
195
+                'Bucket' => $this->bucket,
196
+                'Prefix' => $path,
197
+                'MaxKeys' => 1,
198
+            ]);
199
+
200
+            if (isset($result['Contents'])) {
201
+                $this->directoryCache[$path] = true;
202
+                return true;
203
+            }
204
+
205
+            // empty directories have their own object
206
+            $object = $this->headObject($path);
207
+
208
+            if ($object) {
209
+                $this->directoryCache[$path] = true;
210
+                return true;
211
+            }
212
+        } catch (S3Exception $e) {
213
+            if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
214
+                $this->directoryCache[$path] = false;
215
+            }
216
+            throw $e;
217
+        }
218
+
219
+
220
+        $this->directoryCache[$path] = false;
221
+        return false;
222
+    }
223
+
224
+    /**
225
+     * Remove a file or folder
226
+     *
227
+     * @param string $path
228
+     * @return bool
229
+     */
230
+    protected function remove($path) {
231
+        // remember fileType to reduce http calls
232
+        $fileType = $this->filetype($path);
233
+        if ($fileType === 'dir') {
234
+            return $this->rmdir($path);
235
+        } elseif ($fileType === 'file') {
236
+            return $this->unlink($path);
237
+        } else {
238
+            return false;
239
+        }
240
+    }
241
+
242
+    public function mkdir($path) {
243
+        $path = $this->normalizePath($path);
244
+
245
+        if ($this->is_dir($path)) {
246
+            return false;
247
+        }
248
+
249
+        try {
250
+            $this->getConnection()->putObject([
251
+                'Bucket' => $this->bucket,
252
+                'Key' => $path . '/',
253
+                'Body' => '',
254
+                'ContentType' => FileInfo::MIMETYPE_FOLDER
255
+            ]);
256
+            $this->testTimeout();
257
+        } catch (S3Exception $e) {
258
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
259
+            return false;
260
+        }
261
+
262
+        $this->invalidateCache($path);
263
+
264
+        return true;
265
+    }
266
+
267
+    public function file_exists($path) {
268
+        return $this->filetype($path) !== false;
269
+    }
270
+
271
+
272
+    public function rmdir($path) {
273
+        $path = $this->normalizePath($path);
274
+
275
+        if ($this->isRoot($path)) {
276
+            return $this->clearBucket();
277
+        }
278
+
279
+        if (!$this->file_exists($path)) {
280
+            return false;
281
+        }
282
+
283
+        $this->invalidateCache($path);
284
+        return $this->batchDelete($path);
285
+    }
286
+
287
+    protected function clearBucket() {
288
+        $this->clearCache();
289
+        try {
290
+            $this->getConnection()->clearBucket([
291
+                "Bucket" => $this->bucket
292
+            ]);
293
+            return true;
294
+            // clearBucket() is not working with Ceph, so if it fails we try the slower approach
295
+        } catch (\Exception $e) {
296
+            return $this->batchDelete();
297
+        }
298
+    }
299
+
300
+    private function batchDelete($path = null) {
301
+        $params = [
302
+            'Bucket' => $this->bucket
303
+        ];
304
+        if ($path !== null) {
305
+            $params['Prefix'] = $path . '/';
306
+        }
307
+        try {
308
+            $connection = $this->getConnection();
309
+            // Since there are no real directories on S3, we need
310
+            // to delete all objects prefixed with the path.
311
+            do {
312
+                // instead of the iterator, manually loop over the list ...
313
+                $objects = $connection->listObjects($params);
314
+                // ... so we can delete the files in batches
315
+                if (isset($objects['Contents'])) {
316
+                    $connection->deleteObjects([
317
+                        'Bucket' => $this->bucket,
318
+                        'Delete' => [
319
+                            'Objects' => $objects['Contents']
320
+                        ]
321
+                    ]);
322
+                    $this->testTimeout();
323
+                }
324
+                // we reached the end when the list is no longer truncated
325
+            } while ($objects['IsTruncated']);
326
+            if ($path !== '' && $path !== null) {
327
+                $this->deleteObject($path);
328
+            }
329
+        } catch (S3Exception $e) {
330
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
331
+            return false;
332
+        }
333
+        return true;
334
+    }
335
+
336
+    public function opendir($path) {
337
+        try {
338
+            $content = iterator_to_array($this->getDirectoryContent($path));
339
+            return IteratorDirectory::wrap(array_map(function (array $item) {
340
+                return $item['name'];
341
+            }, $content));
342
+        } catch (S3Exception $e) {
343
+            return false;
344
+        }
345
+    }
346
+
347
+    public function stat($path) {
348
+        $path = $this->normalizePath($path);
349
+
350
+        if ($this->is_dir($path)) {
351
+            $stat = $this->getDirectoryMetaData($path);
352
+        } else {
353
+            $object = $this->headObject($path);
354
+            if ($object === false) {
355
+                return false;
356
+            }
357
+            $stat = $this->objectToMetaData($object);
358
+        }
359
+        $stat['atime'] = time();
360
+
361
+        return $stat;
362
+    }
363
+
364
+    /**
365
+     * Return content length for object
366
+     *
367
+     * When the information is already present (e.g. opendir has been called before)
368
+     * this value is return. Otherwise a headObject is emitted.
369
+     *
370
+     * @param $path
371
+     * @return int|mixed
372
+     */
373
+    private function getContentLength($path) {
374
+        if (isset($this->filesCache[$path])) {
375
+            return (int)$this->filesCache[$path]['ContentLength'];
376
+        }
377
+
378
+        $result = $this->headObject($path);
379
+        if (isset($result['ContentLength'])) {
380
+            return (int)$result['ContentLength'];
381
+        }
382
+
383
+        return 0;
384
+    }
385
+
386
+    /**
387
+     * Return last modified for object
388
+     *
389
+     * When the information is already present (e.g. opendir has been called before)
390
+     * this value is return. Otherwise a headObject is emitted.
391
+     *
392
+     * @param $path
393
+     * @return mixed|string
394
+     */
395
+    private function getLastModified($path) {
396
+        if (isset($this->filesCache[$path])) {
397
+            return $this->filesCache[$path]['LastModified'];
398
+        }
399
+
400
+        $result = $this->headObject($path);
401
+        if (isset($result['LastModified'])) {
402
+            return $result['LastModified'];
403
+        }
404
+
405
+        return 'now';
406
+    }
407
+
408
+    public function is_dir($path) {
409
+        $path = $this->normalizePath($path);
410
+
411
+        if (isset($this->filesCache[$path])) {
412
+            return false;
413
+        }
414
+
415
+        try {
416
+            return $this->doesDirectoryExist($path);
417
+        } catch (S3Exception $e) {
418
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
419
+            return false;
420
+        }
421
+    }
422
+
423
+    public function filetype($path) {
424
+        $path = $this->normalizePath($path);
425
+
426
+        if ($this->isRoot($path)) {
427
+            return 'dir';
428
+        }
429
+
430
+        try {
431
+            if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
432
+                return 'dir';
433
+            }
434
+            if (isset($this->filesCache[$path]) || $this->headObject($path)) {
435
+                return 'file';
436
+            }
437
+            if ($this->doesDirectoryExist($path)) {
438
+                return 'dir';
439
+            }
440
+        } catch (S3Exception $e) {
441
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
442
+            return false;
443
+        }
444
+
445
+        return false;
446
+    }
447
+
448
+    public function getPermissions($path) {
449
+        $type = $this->filetype($path);
450
+        if (!$type) {
451
+            return 0;
452
+        }
453
+        return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
454
+    }
455
+
456
+    public function unlink($path) {
457
+        $path = $this->normalizePath($path);
458
+
459
+        if ($this->is_dir($path)) {
460
+            return $this->rmdir($path);
461
+        }
462
+
463
+        try {
464
+            $this->deleteObject($path);
465
+            $this->invalidateCache($path);
466
+        } catch (S3Exception $e) {
467
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
468
+            return false;
469
+        }
470
+
471
+        return true;
472
+    }
473
+
474
+    public function fopen($path, $mode) {
475
+        $path = $this->normalizePath($path);
476
+
477
+        switch ($mode) {
478
+            case 'r':
479
+            case 'rb':
480
+                // Don't try to fetch empty files
481
+                $stat = $this->stat($path);
482
+                if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
483
+                    return fopen('php://memory', $mode);
484
+                }
485
+
486
+                try {
487
+                    return $this->readObject($path);
488
+                } catch (S3Exception $e) {
489
+                    \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
490
+                    return false;
491
+                }
492
+            case 'w':
493
+            case 'wb':
494
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
495
+
496
+                $handle = fopen($tmpFile, 'w');
497
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
498
+                    $this->writeBack($tmpFile, $path);
499
+                });
500
+            case 'a':
501
+            case 'ab':
502
+            case 'r+':
503
+            case 'w+':
504
+            case 'wb+':
505
+            case 'a+':
506
+            case 'x':
507
+            case 'x+':
508
+            case 'c':
509
+            case 'c+':
510
+                if (strrpos($path, '.') !== false) {
511
+                    $ext = substr($path, strrpos($path, '.'));
512
+                } else {
513
+                    $ext = '';
514
+                }
515
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
516
+                if ($this->file_exists($path)) {
517
+                    $source = $this->readObject($path);
518
+                    file_put_contents($tmpFile, $source);
519
+                }
520
+
521
+                $handle = fopen($tmpFile, $mode);
522
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
523
+                    $this->writeBack($tmpFile, $path);
524
+                });
525
+        }
526
+        return false;
527
+    }
528
+
529
+    public function touch($path, $mtime = null) {
530
+        if (is_null($mtime)) {
531
+            $mtime = time();
532
+        }
533
+        $metadata = [
534
+            'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
535
+        ];
536
+
537
+        try {
538
+            if (!$this->file_exists($path)) {
539
+                $mimeType = $this->mimeDetector->detectPath($path);
540
+                $this->getConnection()->putObject([
541
+                    'Bucket' => $this->bucket,
542
+                    'Key' => $this->cleanKey($path),
543
+                    'Metadata' => $metadata,
544
+                    'Body' => '',
545
+                    'ContentType' => $mimeType,
546
+                    'MetadataDirective' => 'REPLACE',
547
+                ]);
548
+                $this->testTimeout();
549
+            }
550
+        } catch (S3Exception $e) {
551
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
552
+            return false;
553
+        }
554
+
555
+        $this->invalidateCache($path);
556
+        return true;
557
+    }
558
+
559
+    public function copy($path1, $path2, $isFile = null) {
560
+        $path1 = $this->normalizePath($path1);
561
+        $path2 = $this->normalizePath($path2);
562
+
563
+        if ($isFile === true || $this->is_file($path1)) {
564
+            try {
565
+                $this->getConnection()->copyObject([
566
+                    'Bucket' => $this->bucket,
567
+                    'Key' => $this->cleanKey($path2),
568
+                    'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
569
+                ]);
570
+                $this->testTimeout();
571
+            } catch (S3Exception $e) {
572
+                \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
573
+                return false;
574
+            }
575
+        } else {
576
+            $this->remove($path2);
577
+
578
+            try {
579
+                $this->mkdir($path2);
580
+                $this->testTimeout();
581
+            } catch (S3Exception $e) {
582
+                \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
583
+                return false;
584
+            }
585
+
586
+            foreach ($this->getDirectoryContent($path1) as $item) {
587
+                $source = $path1 . '/' . $item['name'];
588
+                $target = $path2 . '/' . $item['name'];
589
+                $this->copy($source, $target, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
590
+            }
591
+        }
592
+
593
+        $this->invalidateCache($path2);
594
+
595
+        return true;
596
+    }
597
+
598
+    public function rename($path1, $path2) {
599
+        $path1 = $this->normalizePath($path1);
600
+        $path2 = $this->normalizePath($path2);
601
+
602
+        if ($this->is_file($path1)) {
603
+            if ($this->copy($path1, $path2) === false) {
604
+                return false;
605
+            }
606
+
607
+            if ($this->unlink($path1) === false) {
608
+                $this->unlink($path2);
609
+                return false;
610
+            }
611
+        } else {
612
+            if ($this->copy($path1, $path2) === false) {
613
+                return false;
614
+            }
615
+
616
+            if ($this->rmdir($path1) === false) {
617
+                $this->rmdir($path2);
618
+                return false;
619
+            }
620
+        }
621
+
622
+        return true;
623
+    }
624
+
625
+    public function test() {
626
+        $this->getConnection()->headBucket([
627
+            'Bucket' => $this->bucket
628
+        ]);
629
+        return true;
630
+    }
631
+
632
+    public function getId() {
633
+        return $this->id;
634
+    }
635
+
636
+    public function writeBack($tmpFile, $path) {
637
+        try {
638
+            $source = fopen($tmpFile, 'r');
639
+            $this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
640
+            $this->invalidateCache($path);
641
+
642
+            unlink($tmpFile);
643
+            return true;
644
+        } catch (S3Exception $e) {
645
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
646
+            return false;
647
+        }
648
+    }
649
+
650
+    /**
651
+     * check if curl is installed
652
+     */
653
+    public static function checkDependencies() {
654
+        return true;
655
+    }
656
+
657
+    public function getDirectoryContent($directory): \Traversable {
658
+        $path = $this->normalizePath($directory);
659
+
660
+        if ($this->isRoot($path)) {
661
+            $path = '';
662
+        } else {
663
+            $path .= '/';
664
+        }
665
+
666
+        $results = $this->getConnection()->getPaginator('ListObjectsV2', [
667
+            'Bucket' => $this->bucket,
668
+            'Delimiter' => '/',
669
+            'Prefix' => $path,
670
+        ]);
671
+
672
+        foreach ($results as $result) {
673
+            // sub folders
674
+            if (is_array($result['CommonPrefixes'])) {
675
+                foreach ($result['CommonPrefixes'] as $prefix) {
676
+                    $dir = $this->getDirectoryMetaData($prefix['Prefix']);
677
+                    if ($dir) {
678
+                        yield $dir;
679
+                    }
680
+                }
681
+            }
682
+            if (is_array($result['Contents'])) {
683
+                foreach ($result['Contents'] as $object) {
684
+                    $this->objectCache[$object['Key']] = $object;
685
+                    if ($object['Key'] !== $path) {
686
+                        yield $this->objectToMetaData($object);
687
+                    }
688
+                }
689
+            }
690
+        }
691
+    }
692
+
693
+    private function objectToMetaData(array $object): array {
694
+        return [
695
+            'name' => basename($object['Key']),
696
+            'mimetype' => $this->mimeDetector->detectPath($object['Key']),
697
+            'mtime' => strtotime($object['LastModified']),
698
+            'storage_mtime' => strtotime($object['LastModified']),
699
+            'etag' => $object['ETag'],
700
+            'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
701
+            'size' => (int)($object['Size'] ?? $object['ContentLength']),
702
+        ];
703
+    }
704
+
705
+    private function getDirectoryMetaData(string $path): ?array {
706
+        $path = trim($path, '/');
707
+        // when versioning is enabled, delete markers are returned as part of CommonPrefixes
708
+        // resulting in "ghost" folders, verify that each folder actually exists
709
+        if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
710
+            return null;
711
+        }
712
+        $cacheEntry = $this->getCache()->get($path);
713
+        if ($cacheEntry instanceof CacheEntry) {
714
+            return $cacheEntry->getData();
715
+        } else {
716
+            return [
717
+                'name' => basename($path),
718
+                'mimetype' => FileInfo::MIMETYPE_FOLDER,
719
+                'mtime' => time(),
720
+                'storage_mtime' => time(),
721
+                'etag' => uniqid(),
722
+                'permissions' => Constants::PERMISSION_ALL,
723
+                'size' => -1,
724
+            ];
725
+        }
726
+    }
727
+
728
+    public function versioningEnabled(): bool {
729
+        if ($this->versioningEnabled === null) {
730
+            $cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
731
+            if ($cached === null) {
732
+                $this->versioningEnabled = $this->getVersioningStatusFromBucket();
733
+                $this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
734
+            } else {
735
+                $this->versioningEnabled = $cached;
736
+            }
737
+        }
738
+        return $this->versioningEnabled;
739
+    }
740
+
741
+    protected function getVersioningStatusFromBucket(): bool {
742
+        try {
743
+            $result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
744
+            return $result->get('Status') === 'Enabled';
745
+        } catch (S3Exception $s3Exception) {
746
+            // This is needed for compatibility with Storj gateway which does not support versioning yet
747
+            if ($s3Exception->getAwsErrorCode() === 'NotImplemented') {
748
+                return false;
749
+            }
750
+            throw $s3Exception;
751
+        }
752
+    }
753
+
754
+    public function hasUpdated($path, $time) {
755
+        // for files we can get the proper mtime
756
+        if ($path !== '' && $object = $this->headObject($path)) {
757
+            $stat = $this->objectToMetaData($object);
758
+            return $stat['mtime'] > $time;
759
+        } else {
760
+            // for directories, the only real option we have is to do a prefix listing and iterate over all objects
761
+            // however, since this is just as expensive as just re-scanning the directory, we can simply return true
762
+            // and have the scanner figure out if anything has actually changed
763
+            return true;
764
+        }
765
+    }
766 766
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/SMB.php 1 patch
Indentation   +679 added lines, -679 removed lines patch added patch discarded remove patch
@@ -69,683 +69,683 @@
 block discarded – undo
69 69
 use OCP\ILogger;
70 70
 
71 71
 class SMB extends Common implements INotifyStorage {
72
-	/**
73
-	 * @var \Icewind\SMB\IServer
74
-	 */
75
-	protected $server;
76
-
77
-	/**
78
-	 * @var \Icewind\SMB\IShare
79
-	 */
80
-	protected $share;
81
-
82
-	/**
83
-	 * @var string
84
-	 */
85
-	protected $root;
86
-
87
-	/** @var CappedMemoryCache<IFileInfo> */
88
-	protected CappedMemoryCache $statCache;
89
-
90
-	/** @var ILogger */
91
-	protected $logger;
92
-
93
-	/** @var bool */
94
-	protected $showHidden;
95
-
96
-	/** @var bool */
97
-	protected $checkAcl;
98
-
99
-	public function __construct($params) {
100
-		if (!isset($params['host'])) {
101
-			throw new \Exception('Invalid configuration, no host provided');
102
-		}
103
-
104
-		if (isset($params['auth'])) {
105
-			$auth = $params['auth'];
106
-		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
107
-			[$workgroup, $user] = $this->splitUser($params['user']);
108
-			$auth = new BasicAuth($user, $workgroup, $params['password']);
109
-		} else {
110
-			throw new \Exception('Invalid configuration, no credentials provided');
111
-		}
112
-
113
-		if (isset($params['logger'])) {
114
-			$this->logger = $params['logger'];
115
-		} else {
116
-			$this->logger = \OC::$server->getLogger();
117
-		}
118
-
119
-		$options = new Options();
120
-		if (isset($params['timeout'])) {
121
-			$timeout = (int)$params['timeout'];
122
-			if ($timeout > 0) {
123
-				$options->setTimeout($timeout);
124
-			}
125
-		}
126
-		$serverFactory = new ServerFactory($options);
127
-		$this->server = $serverFactory->createServer($params['host'], $auth);
128
-		$this->share = $this->server->getShare(trim($params['share'], '/'));
129
-
130
-		$this->root = $params['root'] ?? '/';
131
-		$this->root = '/' . ltrim($this->root, '/');
132
-		$this->root = rtrim($this->root, '/') . '/';
133
-
134
-		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135
-		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
136
-
137
-		$this->statCache = new CappedMemoryCache();
138
-		parent::__construct($params);
139
-	}
140
-
141
-	private function splitUser($user) {
142
-		if (strpos($user, '/')) {
143
-			return explode('/', $user, 2);
144
-		} elseif (strpos($user, '\\')) {
145
-			return explode('\\', $user);
146
-		} else {
147
-			return [null, $user];
148
-		}
149
-	}
150
-
151
-	/**
152
-	 * @return string
153
-	 */
154
-	public function getId() {
155
-		// FIXME: double slash to keep compatible with the old storage ids,
156
-		// failure to do so will lead to creation of a new storage id and
157
-		// loss of shares from the storage
158
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
-	}
160
-
161
-	/**
162
-	 * @param string $path
163
-	 * @return string
164
-	 */
165
-	protected function buildPath($path) {
166
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
-	}
168
-
169
-	protected function relativePath($fullPath) {
170
-		if ($fullPath === $this->root) {
171
-			return '';
172
-		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
173
-			return substr($fullPath, strlen($this->root));
174
-		} else {
175
-			return null;
176
-		}
177
-	}
178
-
179
-	/**
180
-	 * @param string $path
181
-	 * @return IFileInfo
182
-	 * @throws StorageAuthException
183
-	 */
184
-	protected function getFileInfo($path) {
185
-		try {
186
-			$path = $this->buildPath($path);
187
-			$cached = $this->statCache[$path] ?? null;
188
-			if ($cached instanceof IFileInfo) {
189
-				return $cached;
190
-			} else {
191
-				$stat = $this->share->stat($path);
192
-				$this->statCache[$path] = $stat;
193
-				return $stat;
194
-			}
195
-		} catch (ConnectException $e) {
196
-			$this->throwUnavailable($e);
197
-		} catch (ForbiddenException $e) {
198
-			// with php-smbclient, this exception is thrown when the provided password is invalid.
199
-			// Possible is also ForbiddenException with a different error code, so we check it.
200
-			if ($e->getCode() === 1) {
201
-				$this->throwUnavailable($e);
202
-			}
203
-			throw $e;
204
-		}
205
-	}
206
-
207
-	/**
208
-	 * @param \Exception $e
209
-	 * @return never
210
-	 * @throws StorageAuthException
211
-	 */
212
-	protected function throwUnavailable(\Exception $e) {
213
-		$this->logger->logException($e, ['message' => 'Error while getting file info']);
214
-		throw new StorageAuthException($e->getMessage(), $e);
215
-	}
216
-
217
-	/**
218
-	 * get the acl from fileinfo that is relevant for the configured user
219
-	 *
220
-	 * @param IFileInfo $file
221
-	 * @return ACL|null
222
-	 */
223
-	private function getACL(IFileInfo $file): ?ACL {
224
-		$acls = $file->getAcls();
225
-		foreach ($acls as $user => $acl) {
226
-			[, $user] = $this->splitUser($user); // strip domain
227
-			if ($user === $this->server->getAuth()->getUsername()) {
228
-				return $acl;
229
-			}
230
-		}
231
-
232
-		return null;
233
-	}
234
-
235
-	/**
236
-	 * @param string $path
237
-	 * @return \Generator<IFileInfo>
238
-	 * @throws StorageNotAvailableException
239
-	 */
240
-	protected function getFolderContents($path): iterable {
241
-		try {
242
-			$path = ltrim($this->buildPath($path), '/');
243
-			try {
244
-				$files = $this->share->dir($path);
245
-			} catch (ForbiddenException $e) {
246
-				$this->logger->critical($e->getMessage(), ['exception' => $e]);
247
-				throw new NotPermittedException();
248
-			} catch (InvalidTypeException $e) {
249
-				return;
250
-			}
251
-			foreach ($files as $file) {
252
-				$this->statCache[$path . '/' . $file->getName()] = $file;
253
-			}
254
-
255
-			foreach ($files as $file) {
256
-				try {
257
-					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
258
-					// so we trigger the below exceptions where applicable
259
-					$hide = $file->isHidden() && !$this->showHidden;
260
-
261
-					if ($this->checkAcl && $acl = $this->getACL($file)) {
262
-						// if there is no explicit deny, we assume it's allowed
263
-						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
264
-						// additionally, it's better to have false negatives here then false positives
265
-						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
266
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
267
-							continue;
268
-						}
269
-					}
270
-
271
-					if ($hide) {
272
-						$this->logger->debug('hiding hidden file ' . $file->getName());
273
-					}
274
-					if (!$hide) {
275
-						yield $file;
276
-					}
277
-				} catch (ForbiddenException $e) {
278
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
279
-				} catch (NotFoundException $e) {
280
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
281
-				}
282
-			}
283
-		} catch (ConnectException $e) {
284
-			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
285
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
286
-		}
287
-	}
288
-
289
-	/**
290
-	 * @param IFileInfo $info
291
-	 * @return array
292
-	 */
293
-	protected function formatInfo($info) {
294
-		$result = [
295
-			'size' => $info->getSize(),
296
-			'mtime' => $info->getMTime(),
297
-		];
298
-		if ($info->isDirectory()) {
299
-			$result['type'] = 'dir';
300
-		} else {
301
-			$result['type'] = 'file';
302
-		}
303
-		return $result;
304
-	}
305
-
306
-	/**
307
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
308
-	 *
309
-	 * @param string $source the old name of the path
310
-	 * @param string $target the new name of the path
311
-	 * @return bool true if the rename is successful, false otherwise
312
-	 */
313
-	public function rename($source, $target, $retry = true) {
314
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
315
-			return false;
316
-		}
317
-
318
-		$absoluteSource = $this->buildPath($source);
319
-		$absoluteTarget = $this->buildPath($target);
320
-		try {
321
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
322
-		} catch (AlreadyExistsException $e) {
323
-			if ($retry) {
324
-				$this->remove($target);
325
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
326
-			} else {
327
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
328
-				return false;
329
-			}
330
-		} catch (InvalidArgumentException $e) {
331
-			if ($retry) {
332
-				$this->remove($target);
333
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
334
-			} else {
335
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
336
-				return false;
337
-			}
338
-		} catch (\Exception $e) {
339
-			$this->logger->logException($e, ['level' => ILogger::WARN]);
340
-			return false;
341
-		}
342
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
343
-		return $result;
344
-	}
345
-
346
-	public function stat($path, $retry = true) {
347
-		try {
348
-			$result = $this->formatInfo($this->getFileInfo($path));
349
-		} catch (ForbiddenException $e) {
350
-			return false;
351
-		} catch (NotFoundException $e) {
352
-			return false;
353
-		} catch (TimedOutException $e) {
354
-			if ($retry) {
355
-				return $this->stat($path, false);
356
-			} else {
357
-				throw $e;
358
-			}
359
-		}
360
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
361
-			$result['mtime'] = $this->shareMTime();
362
-		}
363
-		return $result;
364
-	}
365
-
366
-	/**
367
-	 * get the best guess for the modification time of the share
368
-	 *
369
-	 * @return int
370
-	 */
371
-	private function shareMTime() {
372
-		$highestMTime = 0;
373
-		$files = $this->share->dir($this->root);
374
-		foreach ($files as $fileInfo) {
375
-			try {
376
-				if ($fileInfo->getMTime() > $highestMTime) {
377
-					$highestMTime = $fileInfo->getMTime();
378
-				}
379
-			} catch (NotFoundException $e) {
380
-				// Ignore this, can happen on unavailable DFS shares
381
-			} catch (ForbiddenException $e) {
382
-				// Ignore this too - it's a symlink
383
-			}
384
-		}
385
-		return $highestMTime;
386
-	}
387
-
388
-	/**
389
-	 * Check if the path is our root dir (not the smb one)
390
-	 *
391
-	 * @param string $path the path
392
-	 * @return bool
393
-	 */
394
-	private function isRootDir($path) {
395
-		return $path === '' || $path === '/' || $path === '.';
396
-	}
397
-
398
-	/**
399
-	 * Check if our root points to a smb share
400
-	 *
401
-	 * @return bool true if our root points to a share false otherwise
402
-	 */
403
-	private function remoteIsShare() {
404
-		return $this->share->getName() && (!$this->root || $this->root === '/');
405
-	}
406
-
407
-	/**
408
-	 * @param string $path
409
-	 * @return bool
410
-	 */
411
-	public function unlink($path) {
412
-		if ($this->isRootDir($path)) {
413
-			return false;
414
-		}
415
-
416
-		try {
417
-			if ($this->is_dir($path)) {
418
-				return $this->rmdir($path);
419
-			} else {
420
-				$path = $this->buildPath($path);
421
-				unset($this->statCache[$path]);
422
-				$this->share->del($path);
423
-				return true;
424
-			}
425
-		} catch (NotFoundException $e) {
426
-			return false;
427
-		} catch (ForbiddenException $e) {
428
-			return false;
429
-		} catch (ConnectException $e) {
430
-			$this->logger->logException($e, ['message' => 'Error while deleting file']);
431
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
432
-		}
433
-	}
434
-
435
-	/**
436
-	 * check if a file or folder has been updated since $time
437
-	 *
438
-	 * @param string $path
439
-	 * @param int $time
440
-	 * @return bool
441
-	 */
442
-	public function hasUpdated($path, $time) {
443
-		if (!$path and $this->root === '/') {
444
-			// mtime doesn't work for shares, but giving the nature of the backend,
445
-			// doing a full update is still just fast enough
446
-			return true;
447
-		} else {
448
-			$actualTime = $this->filemtime($path);
449
-			return $actualTime > $time;
450
-		}
451
-	}
452
-
453
-	/**
454
-	 * @param string $path
455
-	 * @param string $mode
456
-	 * @return resource|bool
457
-	 */
458
-	public function fopen($path, $mode) {
459
-		$fullPath = $this->buildPath($path);
460
-		try {
461
-			switch ($mode) {
462
-				case 'r':
463
-				case 'rb':
464
-					if (!$this->file_exists($path)) {
465
-						return false;
466
-					}
467
-					return $this->share->read($fullPath);
468
-				case 'w':
469
-				case 'wb':
470
-					$source = $this->share->write($fullPath);
471
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
472
-						unset($this->statCache[$fullPath]);
473
-					});
474
-				case 'a':
475
-				case 'ab':
476
-				case 'r+':
477
-				case 'w+':
478
-				case 'wb+':
479
-				case 'a+':
480
-				case 'x':
481
-				case 'x+':
482
-				case 'c':
483
-				case 'c+':
484
-					//emulate these
485
-					if (strrpos($path, '.') !== false) {
486
-						$ext = substr($path, strrpos($path, '.'));
487
-					} else {
488
-						$ext = '';
489
-					}
490
-					if ($this->file_exists($path)) {
491
-						if (!$this->isUpdatable($path)) {
492
-							return false;
493
-						}
494
-						$tmpFile = $this->getCachedFile($path);
495
-					} else {
496
-						if (!$this->isCreatable(dirname($path))) {
497
-							return false;
498
-						}
499
-						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
500
-					}
501
-					$source = fopen($tmpFile, $mode);
502
-					$share = $this->share;
503
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
504
-						unset($this->statCache[$fullPath]);
505
-						$share->put($tmpFile, $fullPath);
506
-						unlink($tmpFile);
507
-					});
508
-			}
509
-			return false;
510
-		} catch (NotFoundException $e) {
511
-			return false;
512
-		} catch (ForbiddenException $e) {
513
-			return false;
514
-		} catch (OutOfSpaceException $e) {
515
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
516
-		} catch (ConnectException $e) {
517
-			$this->logger->logException($e, ['message' => 'Error while opening file']);
518
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
519
-		}
520
-	}
521
-
522
-	public function rmdir($path) {
523
-		if ($this->isRootDir($path)) {
524
-			return false;
525
-		}
526
-
527
-		try {
528
-			$this->statCache = new CappedMemoryCache();
529
-			$content = $this->share->dir($this->buildPath($path));
530
-			foreach ($content as $file) {
531
-				if ($file->isDirectory()) {
532
-					$this->rmdir($path . '/' . $file->getName());
533
-				} else {
534
-					$this->share->del($file->getPath());
535
-				}
536
-			}
537
-			$this->share->rmdir($this->buildPath($path));
538
-			return true;
539
-		} catch (NotFoundException $e) {
540
-			return false;
541
-		} catch (ForbiddenException $e) {
542
-			return false;
543
-		} catch (ConnectException $e) {
544
-			$this->logger->logException($e, ['message' => 'Error while removing folder']);
545
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
546
-		}
547
-	}
548
-
549
-	public function touch($path, $mtime = null) {
550
-		try {
551
-			if (!$this->file_exists($path)) {
552
-				$fh = $this->share->write($this->buildPath($path));
553
-				fclose($fh);
554
-				return true;
555
-			}
556
-			return false;
557
-		} catch (OutOfSpaceException $e) {
558
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
559
-		} catch (ConnectException $e) {
560
-			$this->logger->logException($e, ['message' => 'Error while creating file']);
561
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
562
-		}
563
-	}
564
-
565
-	public function getMetaData($path) {
566
-		try {
567
-			$fileInfo = $this->getFileInfo($path);
568
-		} catch (NotFoundException $e) {
569
-			return null;
570
-		} catch (ForbiddenException $e) {
571
-			return null;
572
-		}
573
-		if (!$fileInfo) {
574
-			return null;
575
-		}
576
-
577
-		return $this->getMetaDataFromFileInfo($fileInfo);
578
-	}
579
-
580
-	private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
581
-		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
582
-
583
-		if (
584
-			!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
585
-		) {
586
-			$permissions += Constants::PERMISSION_DELETE;
587
-			$permissions += Constants::PERMISSION_UPDATE;
588
-			if ($fileInfo->isDirectory()) {
589
-				$permissions += Constants::PERMISSION_CREATE;
590
-			}
591
-		}
592
-
593
-		$data = [];
594
-		if ($fileInfo->isDirectory()) {
595
-			$data['mimetype'] = 'httpd/unix-directory';
596
-		} else {
597
-			$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
598
-		}
599
-		$data['mtime'] = $fileInfo->getMTime();
600
-		if ($fileInfo->isDirectory()) {
601
-			$data['size'] = -1; //unknown
602
-		} else {
603
-			$data['size'] = $fileInfo->getSize();
604
-		}
605
-		$data['etag'] = $this->getETag($fileInfo->getPath());
606
-		$data['storage_mtime'] = $data['mtime'];
607
-		$data['permissions'] = $permissions;
608
-		$data['name'] = $fileInfo->getName();
609
-
610
-		return $data;
611
-	}
612
-
613
-	public function opendir($path) {
614
-		try {
615
-			$files = $this->getFolderContents($path);
616
-		} catch (NotFoundException $e) {
617
-			return false;
618
-		} catch (NotPermittedException $e) {
619
-			return false;
620
-		}
621
-		$names = array_map(function ($info) {
622
-			/** @var IFileInfo $info */
623
-			return $info->getName();
624
-		}, iterator_to_array($files));
625
-		return IteratorDirectory::wrap($names);
626
-	}
627
-
628
-	public function getDirectoryContent($directory): \Traversable {
629
-		try {
630
-			$files = $this->getFolderContents($directory);
631
-			foreach ($files as $file) {
632
-				yield $this->getMetaDataFromFileInfo($file);
633
-			}
634
-		} catch (NotFoundException $e) {
635
-			return;
636
-		} catch (NotPermittedException $e) {
637
-			return;
638
-		}
639
-	}
640
-
641
-	public function filetype($path) {
642
-		try {
643
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
644
-		} catch (NotFoundException $e) {
645
-			return false;
646
-		} catch (ForbiddenException $e) {
647
-			return false;
648
-		}
649
-	}
650
-
651
-	public function mkdir($path) {
652
-		$path = $this->buildPath($path);
653
-		try {
654
-			$this->share->mkdir($path);
655
-			return true;
656
-		} catch (ConnectException $e) {
657
-			$this->logger->logException($e, ['message' => 'Error while creating folder']);
658
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
659
-		} catch (Exception $e) {
660
-			return false;
661
-		}
662
-	}
663
-
664
-	public function file_exists($path) {
665
-		try {
666
-			$this->getFileInfo($path);
667
-			return true;
668
-		} catch (NotFoundException $e) {
669
-			return false;
670
-		} catch (ForbiddenException $e) {
671
-			return false;
672
-		} catch (ConnectException $e) {
673
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
674
-		}
675
-	}
676
-
677
-	public function isReadable($path) {
678
-		try {
679
-			$info = $this->getFileInfo($path);
680
-			return $this->showHidden || !$info->isHidden();
681
-		} catch (NotFoundException $e) {
682
-			return false;
683
-		} catch (ForbiddenException $e) {
684
-			return false;
685
-		}
686
-	}
687
-
688
-	public function isUpdatable($path) {
689
-		try {
690
-			$info = $this->getFileInfo($path);
691
-			// following windows behaviour for read-only folders: they can be written into
692
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
693
-			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
694
-		} catch (NotFoundException $e) {
695
-			return false;
696
-		} catch (ForbiddenException $e) {
697
-			return false;
698
-		}
699
-	}
700
-
701
-	public function isDeletable($path) {
702
-		try {
703
-			$info = $this->getFileInfo($path);
704
-			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
705
-		} catch (NotFoundException $e) {
706
-			return false;
707
-		} catch (ForbiddenException $e) {
708
-			return false;
709
-		}
710
-	}
711
-
712
-	/**
713
-	 * check if smbclient is installed
714
-	 */
715
-	public static function checkDependencies() {
716
-		return (
717
-			(bool)\OC_Helper::findBinaryPath('smbclient')
718
-			|| NativeServer::available(new System())
719
-		) ? true : ['smbclient'];
720
-	}
721
-
722
-	/**
723
-	 * Test a storage for availability
724
-	 *
725
-	 * @return bool
726
-	 */
727
-	public function test() {
728
-		try {
729
-			return parent::test();
730
-		} catch (Exception $e) {
731
-			$this->logger->logException($e);
732
-			return false;
733
-		}
734
-	}
735
-
736
-	public function listen($path, callable $callback) {
737
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
738
-			if ($change instanceof IRenameChange) {
739
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
740
-			} else {
741
-				return $callback($change->getType(), $change->getPath());
742
-			}
743
-		});
744
-	}
745
-
746
-	public function notify($path) {
747
-		$path = '/' . ltrim($path, '/');
748
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
749
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
750
-	}
72
+    /**
73
+     * @var \Icewind\SMB\IServer
74
+     */
75
+    protected $server;
76
+
77
+    /**
78
+     * @var \Icewind\SMB\IShare
79
+     */
80
+    protected $share;
81
+
82
+    /**
83
+     * @var string
84
+     */
85
+    protected $root;
86
+
87
+    /** @var CappedMemoryCache<IFileInfo> */
88
+    protected CappedMemoryCache $statCache;
89
+
90
+    /** @var ILogger */
91
+    protected $logger;
92
+
93
+    /** @var bool */
94
+    protected $showHidden;
95
+
96
+    /** @var bool */
97
+    protected $checkAcl;
98
+
99
+    public function __construct($params) {
100
+        if (!isset($params['host'])) {
101
+            throw new \Exception('Invalid configuration, no host provided');
102
+        }
103
+
104
+        if (isset($params['auth'])) {
105
+            $auth = $params['auth'];
106
+        } elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
107
+            [$workgroup, $user] = $this->splitUser($params['user']);
108
+            $auth = new BasicAuth($user, $workgroup, $params['password']);
109
+        } else {
110
+            throw new \Exception('Invalid configuration, no credentials provided');
111
+        }
112
+
113
+        if (isset($params['logger'])) {
114
+            $this->logger = $params['logger'];
115
+        } else {
116
+            $this->logger = \OC::$server->getLogger();
117
+        }
118
+
119
+        $options = new Options();
120
+        if (isset($params['timeout'])) {
121
+            $timeout = (int)$params['timeout'];
122
+            if ($timeout > 0) {
123
+                $options->setTimeout($timeout);
124
+            }
125
+        }
126
+        $serverFactory = new ServerFactory($options);
127
+        $this->server = $serverFactory->createServer($params['host'], $auth);
128
+        $this->share = $this->server->getShare(trim($params['share'], '/'));
129
+
130
+        $this->root = $params['root'] ?? '/';
131
+        $this->root = '/' . ltrim($this->root, '/');
132
+        $this->root = rtrim($this->root, '/') . '/';
133
+
134
+        $this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135
+        $this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
136
+
137
+        $this->statCache = new CappedMemoryCache();
138
+        parent::__construct($params);
139
+    }
140
+
141
+    private function splitUser($user) {
142
+        if (strpos($user, '/')) {
143
+            return explode('/', $user, 2);
144
+        } elseif (strpos($user, '\\')) {
145
+            return explode('\\', $user);
146
+        } else {
147
+            return [null, $user];
148
+        }
149
+    }
150
+
151
+    /**
152
+     * @return string
153
+     */
154
+    public function getId() {
155
+        // FIXME: double slash to keep compatible with the old storage ids,
156
+        // failure to do so will lead to creation of a new storage id and
157
+        // loss of shares from the storage
158
+        return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
+    }
160
+
161
+    /**
162
+     * @param string $path
163
+     * @return string
164
+     */
165
+    protected function buildPath($path) {
166
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
+    }
168
+
169
+    protected function relativePath($fullPath) {
170
+        if ($fullPath === $this->root) {
171
+            return '';
172
+        } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
173
+            return substr($fullPath, strlen($this->root));
174
+        } else {
175
+            return null;
176
+        }
177
+    }
178
+
179
+    /**
180
+     * @param string $path
181
+     * @return IFileInfo
182
+     * @throws StorageAuthException
183
+     */
184
+    protected function getFileInfo($path) {
185
+        try {
186
+            $path = $this->buildPath($path);
187
+            $cached = $this->statCache[$path] ?? null;
188
+            if ($cached instanceof IFileInfo) {
189
+                return $cached;
190
+            } else {
191
+                $stat = $this->share->stat($path);
192
+                $this->statCache[$path] = $stat;
193
+                return $stat;
194
+            }
195
+        } catch (ConnectException $e) {
196
+            $this->throwUnavailable($e);
197
+        } catch (ForbiddenException $e) {
198
+            // with php-smbclient, this exception is thrown when the provided password is invalid.
199
+            // Possible is also ForbiddenException with a different error code, so we check it.
200
+            if ($e->getCode() === 1) {
201
+                $this->throwUnavailable($e);
202
+            }
203
+            throw $e;
204
+        }
205
+    }
206
+
207
+    /**
208
+     * @param \Exception $e
209
+     * @return never
210
+     * @throws StorageAuthException
211
+     */
212
+    protected function throwUnavailable(\Exception $e) {
213
+        $this->logger->logException($e, ['message' => 'Error while getting file info']);
214
+        throw new StorageAuthException($e->getMessage(), $e);
215
+    }
216
+
217
+    /**
218
+     * get the acl from fileinfo that is relevant for the configured user
219
+     *
220
+     * @param IFileInfo $file
221
+     * @return ACL|null
222
+     */
223
+    private function getACL(IFileInfo $file): ?ACL {
224
+        $acls = $file->getAcls();
225
+        foreach ($acls as $user => $acl) {
226
+            [, $user] = $this->splitUser($user); // strip domain
227
+            if ($user === $this->server->getAuth()->getUsername()) {
228
+                return $acl;
229
+            }
230
+        }
231
+
232
+        return null;
233
+    }
234
+
235
+    /**
236
+     * @param string $path
237
+     * @return \Generator<IFileInfo>
238
+     * @throws StorageNotAvailableException
239
+     */
240
+    protected function getFolderContents($path): iterable {
241
+        try {
242
+            $path = ltrim($this->buildPath($path), '/');
243
+            try {
244
+                $files = $this->share->dir($path);
245
+            } catch (ForbiddenException $e) {
246
+                $this->logger->critical($e->getMessage(), ['exception' => $e]);
247
+                throw new NotPermittedException();
248
+            } catch (InvalidTypeException $e) {
249
+                return;
250
+            }
251
+            foreach ($files as $file) {
252
+                $this->statCache[$path . '/' . $file->getName()] = $file;
253
+            }
254
+
255
+            foreach ($files as $file) {
256
+                try {
257
+                    // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
258
+                    // so we trigger the below exceptions where applicable
259
+                    $hide = $file->isHidden() && !$this->showHidden;
260
+
261
+                    if ($this->checkAcl && $acl = $this->getACL($file)) {
262
+                        // if there is no explicit deny, we assume it's allowed
263
+                        // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
264
+                        // additionally, it's better to have false negatives here then false positives
265
+                        if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
266
+                            $this->logger->debug('Hiding non readable entry ' . $file->getName());
267
+                            continue;
268
+                        }
269
+                    }
270
+
271
+                    if ($hide) {
272
+                        $this->logger->debug('hiding hidden file ' . $file->getName());
273
+                    }
274
+                    if (!$hide) {
275
+                        yield $file;
276
+                    }
277
+                } catch (ForbiddenException $e) {
278
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
279
+                } catch (NotFoundException $e) {
280
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
281
+                }
282
+            }
283
+        } catch (ConnectException $e) {
284
+            $this->logger->logException($e, ['message' => 'Error while getting folder content']);
285
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
286
+        }
287
+    }
288
+
289
+    /**
290
+     * @param IFileInfo $info
291
+     * @return array
292
+     */
293
+    protected function formatInfo($info) {
294
+        $result = [
295
+            'size' => $info->getSize(),
296
+            'mtime' => $info->getMTime(),
297
+        ];
298
+        if ($info->isDirectory()) {
299
+            $result['type'] = 'dir';
300
+        } else {
301
+            $result['type'] = 'file';
302
+        }
303
+        return $result;
304
+    }
305
+
306
+    /**
307
+     * Rename the files. If the source or the target is the root, the rename won't happen.
308
+     *
309
+     * @param string $source the old name of the path
310
+     * @param string $target the new name of the path
311
+     * @return bool true if the rename is successful, false otherwise
312
+     */
313
+    public function rename($source, $target, $retry = true) {
314
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
315
+            return false;
316
+        }
317
+
318
+        $absoluteSource = $this->buildPath($source);
319
+        $absoluteTarget = $this->buildPath($target);
320
+        try {
321
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
322
+        } catch (AlreadyExistsException $e) {
323
+            if ($retry) {
324
+                $this->remove($target);
325
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
326
+            } else {
327
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
328
+                return false;
329
+            }
330
+        } catch (InvalidArgumentException $e) {
331
+            if ($retry) {
332
+                $this->remove($target);
333
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
334
+            } else {
335
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
336
+                return false;
337
+            }
338
+        } catch (\Exception $e) {
339
+            $this->logger->logException($e, ['level' => ILogger::WARN]);
340
+            return false;
341
+        }
342
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
343
+        return $result;
344
+    }
345
+
346
+    public function stat($path, $retry = true) {
347
+        try {
348
+            $result = $this->formatInfo($this->getFileInfo($path));
349
+        } catch (ForbiddenException $e) {
350
+            return false;
351
+        } catch (NotFoundException $e) {
352
+            return false;
353
+        } catch (TimedOutException $e) {
354
+            if ($retry) {
355
+                return $this->stat($path, false);
356
+            } else {
357
+                throw $e;
358
+            }
359
+        }
360
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
361
+            $result['mtime'] = $this->shareMTime();
362
+        }
363
+        return $result;
364
+    }
365
+
366
+    /**
367
+     * get the best guess for the modification time of the share
368
+     *
369
+     * @return int
370
+     */
371
+    private function shareMTime() {
372
+        $highestMTime = 0;
373
+        $files = $this->share->dir($this->root);
374
+        foreach ($files as $fileInfo) {
375
+            try {
376
+                if ($fileInfo->getMTime() > $highestMTime) {
377
+                    $highestMTime = $fileInfo->getMTime();
378
+                }
379
+            } catch (NotFoundException $e) {
380
+                // Ignore this, can happen on unavailable DFS shares
381
+            } catch (ForbiddenException $e) {
382
+                // Ignore this too - it's a symlink
383
+            }
384
+        }
385
+        return $highestMTime;
386
+    }
387
+
388
+    /**
389
+     * Check if the path is our root dir (not the smb one)
390
+     *
391
+     * @param string $path the path
392
+     * @return bool
393
+     */
394
+    private function isRootDir($path) {
395
+        return $path === '' || $path === '/' || $path === '.';
396
+    }
397
+
398
+    /**
399
+     * Check if our root points to a smb share
400
+     *
401
+     * @return bool true if our root points to a share false otherwise
402
+     */
403
+    private function remoteIsShare() {
404
+        return $this->share->getName() && (!$this->root || $this->root === '/');
405
+    }
406
+
407
+    /**
408
+     * @param string $path
409
+     * @return bool
410
+     */
411
+    public function unlink($path) {
412
+        if ($this->isRootDir($path)) {
413
+            return false;
414
+        }
415
+
416
+        try {
417
+            if ($this->is_dir($path)) {
418
+                return $this->rmdir($path);
419
+            } else {
420
+                $path = $this->buildPath($path);
421
+                unset($this->statCache[$path]);
422
+                $this->share->del($path);
423
+                return true;
424
+            }
425
+        } catch (NotFoundException $e) {
426
+            return false;
427
+        } catch (ForbiddenException $e) {
428
+            return false;
429
+        } catch (ConnectException $e) {
430
+            $this->logger->logException($e, ['message' => 'Error while deleting file']);
431
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
432
+        }
433
+    }
434
+
435
+    /**
436
+     * check if a file or folder has been updated since $time
437
+     *
438
+     * @param string $path
439
+     * @param int $time
440
+     * @return bool
441
+     */
442
+    public function hasUpdated($path, $time) {
443
+        if (!$path and $this->root === '/') {
444
+            // mtime doesn't work for shares, but giving the nature of the backend,
445
+            // doing a full update is still just fast enough
446
+            return true;
447
+        } else {
448
+            $actualTime = $this->filemtime($path);
449
+            return $actualTime > $time;
450
+        }
451
+    }
452
+
453
+    /**
454
+     * @param string $path
455
+     * @param string $mode
456
+     * @return resource|bool
457
+     */
458
+    public function fopen($path, $mode) {
459
+        $fullPath = $this->buildPath($path);
460
+        try {
461
+            switch ($mode) {
462
+                case 'r':
463
+                case 'rb':
464
+                    if (!$this->file_exists($path)) {
465
+                        return false;
466
+                    }
467
+                    return $this->share->read($fullPath);
468
+                case 'w':
469
+                case 'wb':
470
+                    $source = $this->share->write($fullPath);
471
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
472
+                        unset($this->statCache[$fullPath]);
473
+                    });
474
+                case 'a':
475
+                case 'ab':
476
+                case 'r+':
477
+                case 'w+':
478
+                case 'wb+':
479
+                case 'a+':
480
+                case 'x':
481
+                case 'x+':
482
+                case 'c':
483
+                case 'c+':
484
+                    //emulate these
485
+                    if (strrpos($path, '.') !== false) {
486
+                        $ext = substr($path, strrpos($path, '.'));
487
+                    } else {
488
+                        $ext = '';
489
+                    }
490
+                    if ($this->file_exists($path)) {
491
+                        if (!$this->isUpdatable($path)) {
492
+                            return false;
493
+                        }
494
+                        $tmpFile = $this->getCachedFile($path);
495
+                    } else {
496
+                        if (!$this->isCreatable(dirname($path))) {
497
+                            return false;
498
+                        }
499
+                        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
500
+                    }
501
+                    $source = fopen($tmpFile, $mode);
502
+                    $share = $this->share;
503
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
504
+                        unset($this->statCache[$fullPath]);
505
+                        $share->put($tmpFile, $fullPath);
506
+                        unlink($tmpFile);
507
+                    });
508
+            }
509
+            return false;
510
+        } catch (NotFoundException $e) {
511
+            return false;
512
+        } catch (ForbiddenException $e) {
513
+            return false;
514
+        } catch (OutOfSpaceException $e) {
515
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
516
+        } catch (ConnectException $e) {
517
+            $this->logger->logException($e, ['message' => 'Error while opening file']);
518
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
519
+        }
520
+    }
521
+
522
+    public function rmdir($path) {
523
+        if ($this->isRootDir($path)) {
524
+            return false;
525
+        }
526
+
527
+        try {
528
+            $this->statCache = new CappedMemoryCache();
529
+            $content = $this->share->dir($this->buildPath($path));
530
+            foreach ($content as $file) {
531
+                if ($file->isDirectory()) {
532
+                    $this->rmdir($path . '/' . $file->getName());
533
+                } else {
534
+                    $this->share->del($file->getPath());
535
+                }
536
+            }
537
+            $this->share->rmdir($this->buildPath($path));
538
+            return true;
539
+        } catch (NotFoundException $e) {
540
+            return false;
541
+        } catch (ForbiddenException $e) {
542
+            return false;
543
+        } catch (ConnectException $e) {
544
+            $this->logger->logException($e, ['message' => 'Error while removing folder']);
545
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
546
+        }
547
+    }
548
+
549
+    public function touch($path, $mtime = null) {
550
+        try {
551
+            if (!$this->file_exists($path)) {
552
+                $fh = $this->share->write($this->buildPath($path));
553
+                fclose($fh);
554
+                return true;
555
+            }
556
+            return false;
557
+        } catch (OutOfSpaceException $e) {
558
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
559
+        } catch (ConnectException $e) {
560
+            $this->logger->logException($e, ['message' => 'Error while creating file']);
561
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
562
+        }
563
+    }
564
+
565
+    public function getMetaData($path) {
566
+        try {
567
+            $fileInfo = $this->getFileInfo($path);
568
+        } catch (NotFoundException $e) {
569
+            return null;
570
+        } catch (ForbiddenException $e) {
571
+            return null;
572
+        }
573
+        if (!$fileInfo) {
574
+            return null;
575
+        }
576
+
577
+        return $this->getMetaDataFromFileInfo($fileInfo);
578
+    }
579
+
580
+    private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
581
+        $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
582
+
583
+        if (
584
+            !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
585
+        ) {
586
+            $permissions += Constants::PERMISSION_DELETE;
587
+            $permissions += Constants::PERMISSION_UPDATE;
588
+            if ($fileInfo->isDirectory()) {
589
+                $permissions += Constants::PERMISSION_CREATE;
590
+            }
591
+        }
592
+
593
+        $data = [];
594
+        if ($fileInfo->isDirectory()) {
595
+            $data['mimetype'] = 'httpd/unix-directory';
596
+        } else {
597
+            $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
598
+        }
599
+        $data['mtime'] = $fileInfo->getMTime();
600
+        if ($fileInfo->isDirectory()) {
601
+            $data['size'] = -1; //unknown
602
+        } else {
603
+            $data['size'] = $fileInfo->getSize();
604
+        }
605
+        $data['etag'] = $this->getETag($fileInfo->getPath());
606
+        $data['storage_mtime'] = $data['mtime'];
607
+        $data['permissions'] = $permissions;
608
+        $data['name'] = $fileInfo->getName();
609
+
610
+        return $data;
611
+    }
612
+
613
+    public function opendir($path) {
614
+        try {
615
+            $files = $this->getFolderContents($path);
616
+        } catch (NotFoundException $e) {
617
+            return false;
618
+        } catch (NotPermittedException $e) {
619
+            return false;
620
+        }
621
+        $names = array_map(function ($info) {
622
+            /** @var IFileInfo $info */
623
+            return $info->getName();
624
+        }, iterator_to_array($files));
625
+        return IteratorDirectory::wrap($names);
626
+    }
627
+
628
+    public function getDirectoryContent($directory): \Traversable {
629
+        try {
630
+            $files = $this->getFolderContents($directory);
631
+            foreach ($files as $file) {
632
+                yield $this->getMetaDataFromFileInfo($file);
633
+            }
634
+        } catch (NotFoundException $e) {
635
+            return;
636
+        } catch (NotPermittedException $e) {
637
+            return;
638
+        }
639
+    }
640
+
641
+    public function filetype($path) {
642
+        try {
643
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
644
+        } catch (NotFoundException $e) {
645
+            return false;
646
+        } catch (ForbiddenException $e) {
647
+            return false;
648
+        }
649
+    }
650
+
651
+    public function mkdir($path) {
652
+        $path = $this->buildPath($path);
653
+        try {
654
+            $this->share->mkdir($path);
655
+            return true;
656
+        } catch (ConnectException $e) {
657
+            $this->logger->logException($e, ['message' => 'Error while creating folder']);
658
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
659
+        } catch (Exception $e) {
660
+            return false;
661
+        }
662
+    }
663
+
664
+    public function file_exists($path) {
665
+        try {
666
+            $this->getFileInfo($path);
667
+            return true;
668
+        } catch (NotFoundException $e) {
669
+            return false;
670
+        } catch (ForbiddenException $e) {
671
+            return false;
672
+        } catch (ConnectException $e) {
673
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
674
+        }
675
+    }
676
+
677
+    public function isReadable($path) {
678
+        try {
679
+            $info = $this->getFileInfo($path);
680
+            return $this->showHidden || !$info->isHidden();
681
+        } catch (NotFoundException $e) {
682
+            return false;
683
+        } catch (ForbiddenException $e) {
684
+            return false;
685
+        }
686
+    }
687
+
688
+    public function isUpdatable($path) {
689
+        try {
690
+            $info = $this->getFileInfo($path);
691
+            // following windows behaviour for read-only folders: they can be written into
692
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
693
+            return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
694
+        } catch (NotFoundException $e) {
695
+            return false;
696
+        } catch (ForbiddenException $e) {
697
+            return false;
698
+        }
699
+    }
700
+
701
+    public function isDeletable($path) {
702
+        try {
703
+            $info = $this->getFileInfo($path);
704
+            return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
705
+        } catch (NotFoundException $e) {
706
+            return false;
707
+        } catch (ForbiddenException $e) {
708
+            return false;
709
+        }
710
+    }
711
+
712
+    /**
713
+     * check if smbclient is installed
714
+     */
715
+    public static function checkDependencies() {
716
+        return (
717
+            (bool)\OC_Helper::findBinaryPath('smbclient')
718
+            || NativeServer::available(new System())
719
+        ) ? true : ['smbclient'];
720
+    }
721
+
722
+    /**
723
+     * Test a storage for availability
724
+     *
725
+     * @return bool
726
+     */
727
+    public function test() {
728
+        try {
729
+            return parent::test();
730
+        } catch (Exception $e) {
731
+            $this->logger->logException($e);
732
+            return false;
733
+        }
734
+    }
735
+
736
+    public function listen($path, callable $callback) {
737
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
738
+            if ($change instanceof IRenameChange) {
739
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
740
+            } else {
741
+                return $callback($change->getType(), $change->getPath());
742
+            }
743
+        });
744
+    }
745
+
746
+    public function notify($path) {
747
+        $path = '/' . ltrim($path, '/');
748
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
749
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
750
+    }
751 751
 }
Please login to merge, or discard this patch.