diff --git a/.gitignore b/.gitignore index f25fb52ce2..964701eea6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ !/apps/files !/apps/federation !/apps/federatedfilesharing +!/apps/sharebymail !/apps/encryption !/apps/files_external !/apps/files_sharing diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index ad9ac6c085..ef22b47318 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -188,6 +188,10 @@ class ShareAPIController extends OCSController { $result['share_with'] = $share->getSharedWith(); $result['share_with_displayname'] = $share->getSharedWith(); $result['token'] = $share->getToken(); + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $share->getSharedWith(); + $result['token'] = $share->getToken(); } $result['mail_send'] = $share->getMailSend() ? 1 : 0; @@ -400,6 +404,9 @@ class ShareAPIController extends OCSController { $share->setSharedWith($shareWith); $share->setPermissions($permissions); + } else if ($shareType === \OCP\Share::SHARE_TYPE_EMAIL) { + $share->setPermissions(\OCP\Constants::PERMISSION_READ); + $share->setSharedWith($shareWith); } else { throw new OCSBadRequestException($this->l->t('Unknown share type')); } @@ -466,6 +473,7 @@ class ShareAPIController extends OCSController { $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $node, false, -1, 0)); $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $node, false, -1, 0)); $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $node, false, -1, 0)); + $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $node, false, -1, 0)); if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $node, false, -1, 0)); } @@ -541,7 +549,8 @@ class ShareAPIController extends OCSController { $userShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $path, $reshares, -1, 0); $groupShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $path, $reshares, -1, 0); $linkShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $path, $reshares, -1, 0); - $shares = array_merge($userShares, $groupShares, $linkShares); + $mailShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $path, $reshares, -1, 0); + $shares = array_merge($userShares, $groupShares, $linkShares, $mailShares); if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { $federatedShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $path, $reshares, -1, 0); @@ -774,6 +783,14 @@ class ShareAPIController extends OCSController { // First check if it is an internal share. try { $share = $this->shareManager->getShareById('ocinternal:' . $id); + return $share; + } catch (ShareNotFound $e) { + // Do nothing, just try the other share type + } + + try { + $share = $this->shareManager->getShareById('ocMailShare:' . $id); + return $share; } catch (ShareNotFound $e) { if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { throw new ShareNotFound(); diff --git a/apps/sharebymail/appinfo/info.xml b/apps/sharebymail/appinfo/info.xml new file mode 100644 index 0000000000..3db007f161 --- /dev/null +++ b/apps/sharebymail/appinfo/info.xml @@ -0,0 +1,14 @@ + + + sharebymail + Share by mail + Share provider which allows you to share files by mail + AGPL + Bjoern Schiessle + 1.0.0 + ShareByMail + other + + + + diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php new file mode 100644 index 0000000000..e432ab139e --- /dev/null +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -0,0 +1,594 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\ShareByMail; + +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; +use OC\Share20\Share; +use OCP\Share\IShare; +use OCP\Share\IShareProvider; + +/** + * Class ShareByMail + * + * @package OCA\ShareByMail + */ +class ShareByMailProvider implements IShareProvider { + + /** @var IDBConnection */ + private $dbConnection; + + /** @var ILogger */ + private $logger; + + /** @var ISecureRandom */ + private $secureRandom; + + /** @var IUserManager */ + private $userManager; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IL10N */ + private $l; + + /** + * Return the identifier of this provider. + * + * @return string Containing only [a-zA-Z0-9] + */ + public function identifier() { + return 'ocShareByMail'; + } + + /** + * DefaultShareProvider constructor. + * + * @param IDBConnection $connection + * @param ISecureRandom $secureRandom + * @param IUserManager $userManager + * @param IRootFolder $rootFolder + * @param IL10N $l + * @param ILogger $logger + */ + public function __construct( + IDBConnection $connection, + ISecureRandom $secureRandom, + IUserManager $userManager, + IRootFolder $rootFolder, + IL10N $l, + ILogger $logger + ) { + $this->dbConnection = $connection; + $this->secureRandom = $secureRandom; + $this->userManager = $userManager; + $this->rootFolder = $rootFolder; + $this->l = $l; + $this->logger = $logger; + } + + /** + * Share a path + * + * @param IShare $share + * @return IShare The share object + * @throws ShareNotFound + * @throws \Exception + */ + public function create(IShare $share) { + + $shareWith = $share->getSharedWith(); + /* + * Check if file is not already shared with the remote user + */ + $alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0); + if (!empty($alreadyShared)) { + $message = 'Sharing %s failed, this item is already shared with %s'; + $message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + $shareId = $this->createMailShare($share); + + $data = $this->getRawShare($shareId); + return $this->createShareObject($data); + + } + + /** + * @param IShare $share + * @return int + * @throws \Exception + */ + private function createMailShare(IShare $share) { + $token = $this->generateToken(); + $shareId = $this->addShareToDB( + $share->getNodeId(), + $share->getNodeType(), + $share->getSharedWith(), + $share->getSharedBy(), + $share->getShareOwner(), + $share->getPermissions(), + $token + ); + + try { + // TODO: Send email with public link + } catch (\Exception $e) { + $this->logger->error('Failed to notify remote server of federated share, removing share (' . $e->getMessage() . ')'); + $this->removeShareFromTable($shareId); + throw $e; + } + + return $shareId; + + } + + /** + * generate share token + * + * @return string + */ + public function generateToken() { + $token = $this->secureRandom->generate( + 15, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + return $token; + } + + /** + * Get all children of this share + * + * @param IShare $parent + * @return IShare[] + */ + public function getChildren(IShare $parent) { + $children = []; + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->orderBy('id'); + + $cursor = $qb->execute(); + while($data = $cursor->fetch()) { + $children[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * add share to the database and return the ID + * + * @param int $itemSource + * @param string $itemType + * @param string $shareWith + * @param string $sharedBy + * @param string $uidOwner + * @param int $permissions + * @param string $token + * @return int + */ + private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue('file_target', $qb->createNamedParameter('')); + + $qb->execute(); + $id = $qb->getLastInsertId(); + + return (int)$id; + } + + /** + * Update a share + * + * @param IShare $share + * @return IShare The share object + */ + public function update(IShare $share) { + /* + * We allow updating the permissions of mail shares + */ + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) + ->execute(); + + return $share; + } + + /** + * @inheritdoc + */ + public function move(IShare $share, $recipient) { + /** + * nothing to do here, mail shares are only outgoing shares + */ + return $share; + } + + /** + * Delete a share (owner unShares the file) + * + * @param IShare $share + */ + public function delete(IShare $share) { + $this->removeShareFromTable($share->getId()); + } + + /** + * @inheritdoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // nothing to do here, mail shares are only outgoing shares + return; + } + + /** + * @inheritdoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->setFirstResult($offset); + $qb->orderBy('id'); + + $cursor = $qb->execute(); + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getShareById($id, $recipientId = null) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * Get shares for a given path + * + * @param \OCP\Files\Node $path + * @return IShare[] + */ + public function getSharesByPath(Node $path) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->execute(); + + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + /** @var IShare[] $shares */ + $shares = []; + + //Get shares directly with this user + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + // Order by id + $qb->orderBy('id'); + + // Set limit and offset + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + $qb->setFirstResult($offset); + + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + + // Filter by node if provided + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $cursor = $qb->execute(); + + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + + return $shares; + } + + /** + * Get a share by token + * + * @param string $token + * @return IShare + * @throws ShareNotFound + */ + public function getShareByToken($token) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->execute(); + + $data = $cursor->fetch(); + + if ($data === false) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + return $share; + } + + /** + * remove share from table + * + * @param string $shareId + */ + private function removeShareFromTable($shareId) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); + $qb->execute(); + } + + /** + * Create a share object from an database row + * + * @param array $data + * @return IShare + * @throws InvalidShare + * @throws ShareNotFound + */ + private function createShareObject($data) { + + $share = new Share($this->rootFolder, $this->userManager); + $share->setId((int)$data['id']) + ->setShareType((int)$data['share_type']) + ->setPermissions((int)$data['permissions']) + ->setTarget($data['file_target']) + ->setMailSend((bool)$data['mail_send']) + ->setToken($data['token']); + + $shareTime = new \DateTime(); + $shareTime->setTimestamp((int)$data['stime']); + $share->setShareTime($shareTime); + $share->setSharedWith($data['share_with']); + + if ($data['uid_initiator'] !== null) { + $share->setShareOwner($data['uid_owner']); + $share->setSharedBy($data['uid_initiator']); + } else { + //OLD SHARE + $share->setSharedBy($data['uid_owner']); + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); + + $owner = $path->getOwner(); + $share->setShareOwner($owner->getUID()); + } + + $share->setNodeId((int)$data['file_source']); + $share->setNodeType($data['item_type']); + + $share->setProviderId($this->identifier()); + + return $share; + } + + /** + * Get the node with file $id for $user + * + * @param string $userId + * @param int $id + * @return \OCP\Files\File|\OCP\Files\Folder + * @throws InvalidShare + */ + private function getNode($userId, $id) { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + } catch (NotFoundException $e) { + throw new InvalidShare(); + } + + $nodes = $userFolder->getById($id); + + if (empty($nodes)) { + throw new InvalidShare(); + } + + return $nodes[0]; + } + + /** + * A user is deleted from the system + * So clean up the relevant shares. + * + * @param string $uid + * @param int $shareType + */ + public function userDeleted($uid, $shareType) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(Share::SHARE_TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) + ->execute(); + } + + /** + * This provider does not support group shares + * + * @param string $gid + */ + public function groupDeleted($gid) { + return; + } + + /** + * This provider does not support group shares + * + * @param string $uid + * @param string $gid + */ + public function userDeletedFromGroup($uid, $gid) { + return; + } + + /** + * get database row of a give share + * + * @param $id + * @return array + * @throws ShareNotFound + */ + private function getRawShare($id) { + + // Now fetch the inserted share and create a complete share object + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound; + } + + return $data; + } + +} diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index 78622faea2..30cb0b1fbf 100644 --- a/core/js/sharedialogview.js +++ b/core/js/sharedialogview.js @@ -215,15 +215,15 @@ } } } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { - emailsLength = emails.length; - for (j = 0; j < emailsLength; j++) { - if (emails[j].value.shareWith === share.share_with) { - emails.splice(j, 1); - break; + emailsLength = emails.length; + for (j = 0; j < emailsLength; j++) { + if (emails[j].value.shareWith === share.share_with) { + emails.splice(j, 1); + break; + } } } } - } var suggestions = users.concat(groups).concat(remotes).concat(emails); diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 838650ada1..19d6151a03 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -185,6 +185,10 @@ class Manager implements IManager { if ($share->getSharedWith() === null) { throw new \InvalidArgumentException('SharedWith should not be empty'); } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + if ($share->getSharedWith() === null) { + throw new \InvalidArgumentException('SharedWith should not be empty'); + } } else { // We can't handle other types yet throw new \InvalidArgumentException('unkown share type'); @@ -580,6 +584,16 @@ class Manager implements IManager { if ($share->getPassword() !== null) { $share->setPassword($this->hasher->hash($share->getPassword())); } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + $this->linkCreateChecks($share); + $share->setToken( + $this->secureRandom->generate( + \OC\Share\Constants::TOKEN_LENGTH, + \OCP\Security\ISecureRandom::CHAR_LOWER. + \OCP\Security\ISecureRandom::CHAR_UPPER. + \OCP\Security\ISecureRandom::CHAR_DIGITS + ) + ); } // Cannot share with the owner diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index 5cdc9a51a2..e1ed64d66a 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -28,6 +28,7 @@ use OCA\FederatedFileSharing\DiscoveryManager; use OCA\FederatedFileSharing\FederatedShareProvider; use OCA\FederatedFileSharing\Notifications; use OCA\FederatedFileSharing\TokenHandler; +use OCA\ShareByMail\ShareByMailProvider; use OCP\Share\IProviderFactory; use OC\Share20\Exception\ProviderException; use OCP\IServerContainer; @@ -45,6 +46,8 @@ class ProviderFactory implements IProviderFactory { private $defaultProvider = null; /** @var FederatedShareProvider */ private $federatedProvider = null; + /** @var ShareByMailProvider */ + private $shareByMailProvider; /** * IProviderFactory constructor. @@ -125,6 +128,37 @@ class ProviderFactory implements IProviderFactory { return $this->federatedProvider; } + /** + * Create the federated share provider + * + * @return FederatedShareProvider + */ + protected function getShareByMailProvider() { + if ($this->shareByMailProvider === null) { + /* + * Check if the app is enabled + */ + $appManager = $this->serverContainer->getAppManager(); + if (!$appManager->isEnabledForUser('sharebymail')) { + return null; + } + + $l = $this->serverContainer->getL10N('sharebymail'); + + $this->shareByMailProvider = new ShareByMailProvider( + $this->serverContainer->getDatabaseConnection(), + $this->serverContainer->getSecureRandom(), + $this->serverContainer->getUserManager(), + $this->serverContainer->getLazyRootFolder(), + $l, + $this->serverContainer->getLogger() + ); + } + + return $this->shareByMailProvider; + } + + /** * @inheritdoc */ @@ -134,6 +168,8 @@ class ProviderFactory implements IProviderFactory { $provider = $this->defaultShareProvider(); } else if ($id === 'ocFederatedSharing') { $provider = $this->federatedShareProvider(); + } else if ($id = 'ocMailShare') { + $provider = $this->getShareByMailProvider(); } if ($provider === null) { @@ -155,6 +191,8 @@ class ProviderFactory implements IProviderFactory { $provider = $this->defaultShareProvider(); } else if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) { $provider = $this->federatedShareProvider(); + } else if ($shareType === \OCP\Share::SHARE_TYPE_EMAIL) { + $provider = $this->getShareByMailProvider(); } if ($provider === null) {