home";
const TEMPLATE_1COL = "OpenTalent.OtTemplating->1Col";
const TEMPLATE_3COL = "OpenTalent.OtTemplating->home";
const TEMPLATE_EVENTS = "OpenTalent.OtTemplating->events";
const TEMPLATE_STRUCTURESEVENTS = "OpenTalent.OtTemplating->structuresEvents";
const TEMPLATE_STRUCTURES = "OpenTalent.OtTemplating->structures";
const TEMPLATE_CONTACT = "OpenTalent.OtTemplating->contact";
const TEMPLATE_NEWS = "OpenTalent.OtTemplating->news";
const TEMPLATE_MEMBERS = "OpenTalent.OtTemplating->members";
const TEMPLATE_MEMBERSCA = "OpenTalent.OtTemplating->membersCa";
const TEMPLATE_LEGAL = "OpenTalent.OtTemplating->legal";
// Pages dokType values
const DOK_PAGE = 1;
const DOK_SHORTCUT = 4;
const DOK_FOLDER = 116;
// Contents CTypes
const CTYPE_TEXT = 'text';
const CTYPE_IMAGE = 'image';
const CTYPE_TEXTPIC = 'textpic';
const CTYPE_TEXTMEDIA = 'textmedia';
const CTYPE_HTML = 'html';
const CTYPE_HEADER = 'header';
const CTYPE_UPLOADS = 'uploads';
const CTYPE_LIST = 'list';
const CTYPE_SITEMAP = 'menu_sitemap';
// Default values
const DEFAULT_THEME = 'Classic';
const DEFAULT_COLOR = 'light-blue';
// BE rights
const BEGROUP_EDITOR_STANDARD = 10;
const BEGROUP_EDITOR_PREMIUM = 20;
const BEGROUP_ADMIN_STANDARD = 30;
const BEGROUP_ADMIN_PREMIUM = 40;
const BEGROUP_NAME = [
self::BEGROUP_EDITOR_STANDARD => "Editor_Standard",
self::BEGROUP_EDITOR_PREMIUM => "Editor_Premium",
self::BEGROUP_ADMIN_STANDARD => "Admin_Standard",
self::BEGROUP_ADMIN_PREMIUM => "Admin_Premium"
];
const IS_PRODUCT_PREMIUM = [
"school-standard" => false,
"artist-standard" => false,
"school-premium" => true,
"artist-premium" => true,
"manager" => true,
];
// access permissions
const PERM_SHOW = 1;
const PERM_EDIT_CONTENT = 16;
const PERM_EDIT_PAGE = 2;
const PERM_DELETE = 4;
const PERM_NEW = 8;
// Creation mode
const MODE_PROD = 1;
const MODE_DEV = 1;
// Domain name validation
const RX_DOMAIN = "/([a-z0-9A-Z]\.)*[a-z0-9-]+\.([a-z0-9]{2,24})+(\.co\.([a-z0-9]{2,24})|\.([a-z0-9]{2,24}))*\/?/";
// Redirections creation status
const REDIRECTION_UNKNOWN_STATUS = 0;
const REDIRECTION_UPDATED = 1;
const REDIRECTION_CREATED = 2;
/**
* @var \TYPO3\CMS\Core\Database\ConnectionPool
*/
private \TYPO3\CMS\Core\Database\ConnectionPool $connectionPool;
public function injectConnectionPool(\TYPO3\CMS\Core\Database\ConnectionPool $connectionPool)
{
$this->connectionPool = $connectionPool;
}
/**
* @var \TYPO3\CMS\Core\Cache\CacheManager
*/
private \TYPO3\CMS\Core\Cache\CacheManager $cacheManager;
public function injectCacheManager(\TYPO3\CMS\Core\Cache\CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
/**
* @var OtWebsiteRepository
*/
protected OtWebsiteRepository $otWebsiteRepository;
public function injectOtWebsiteRepository(OtWebsiteRepository $otWebsiteRepository) {
$this->otWebsiteRepository = $otWebsiteRepository;
}
/**
* Index of the pages created during the process
* >> [slug => uid]
* @var array
*/
private array $createdPagesIndex;
/**
* List of the directories created in the process (for rollback purposes)
* @var array
*/
private array $createdDirs;
/**
* List of the files created in the process (for rollback purposes)
* @var array
*/
private array $createdFiles;
public function __construct()
{
parent::__construct();
$this->createdPagesIndex = [];
$this->createdDirs = [];
$this->createdFiles = [];
}
/**
* Return the SiteInfos object for the organization's website
*
* @param int $organizationId
* @return SiteInfos
* @throws NoSuchWebsiteException
*/
public function getSiteInfosAction(int $organizationId): SiteInfos
{
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid']);
$organizationExtraData = $this->fetchOrganizationExtraData($organizationId);
$rootPage = $this->otPageRepository->getPage($rootUid);
$site = new SiteInfos(
$rootUid,
$website['organization_name'],
$this->otWebsiteRepository->resolveWebsiteDomain($website),
$website['template'],
$website['template_preferences'],
$website['matomo_id'],
self::IS_PRODUCT_PREMIUM[$organizationExtraData['admin']['product']] ?? false,
(bool)$rootPage['deleted'],
($rootPage['hidden'] || $rootPage['fe_group'] < 0),
null,
null,
$rootPage['perms_userid'],
$rootPage['perms_groupid']
);
// Owners
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
$queryBuilder->getRestrictions()->removeAll();
$beUsers = $queryBuilder
->select('uid', 'username')
->from('be_users')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
->execute()
->fetchAll();
foreach ($beUsers as $beUser) {
$site->addMountedForBeUser($beUser);
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
$queryBuilder->getRestrictions()->removeAll();
$beGroups = $queryBuilder
->select('uid', 'title')
->from('be_groups')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
->execute()
->fetchAll();
foreach ($beGroups as $beGroup) {
$site->addMountedForBeGroups($beGroup);
}
return $site;
}
/**
* Creates a new website for the given organization, and
* returns the root page uid of the newly created site
*
* @param int $organizationId
* @param int $mode Can be either MODE_PROD or MODE_DEV, MODE_PROD being the normal behaviour.
* If MODE_DEV is used, sites urls will be of the form 'http://host/subdomain'
* instead of 'http://subdomain/host'
* @return int Uid of the root page of the newly created website
* @throws \RuntimeException|\Throwable
*/
public function createSiteAction(int $organizationId, int $mode=self::MODE_PROD): int
{
$organization = $this->fetchOrganization($organizationId);
// This extra-data can not be retrieved from the API for now, but
// this shall be set up as soon as possible, to avoid requesting
// the prod-back DB directly.
$organizationExtraData = $this->fetchOrganizationExtraData($organizationId);
$isNetwork = $organizationExtraData['category'] == 'NETWORK';
// Is there already a website with this organization's id?
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder
->select('uid')
->from('ot_websites')
->where($queryBuilder->expr()->eq('organization_id', $queryBuilder->createNamedParameter($organization->getId())))
->execute();
if ($statement->rowCount() > 0) {
throw new \RuntimeException("A website with this organization's id already exists: " . $organization->getName() . "\n(if you can't see it, it might have been soft-deleted)");
}
// ** Create the new website
// start transactions
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
// keep tracks of the created folders and files to be able to remove them during a rollback
try {
// Create the website:
$websiteUid = $this->insertOtWebsite($organization);
// Create the site pages:
// > Root page
$rootUid = $this->insertRootPage(
$websiteUid,
$organization->getName()
);
// > 'Accueil' shortcut
$this->insertPage(
$websiteUid,
$rootUid,
'Accueil',
'/accueil',
'',
[
'dokType' => self::DOK_SHORTCUT,
'shortcut' => $rootUid
]
);
// > 'Présentation' page
$this->insertPage(
$websiteUid,
$rootUid,
'Présentation',
'/presentation'
);
// > 'Présentation > Qui sommes nous?' page (hidden by default)
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Qui sommes nous?',
'/qui-sommes-nous',
'',
['hidden' => 1]
);
// > 'Présentation > Les adhérents' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Les adhérents',
'/les-adherents',
self::TEMPLATE_MEMBERS
);
// > 'Présentation > Les membres du CA' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Les membres du CA',
'/les-membres-du-ca',
self::TEMPLATE_MEMBERSCA
);
if ($isNetwork) {
// > 'Présentation > Les sociétés adhérentes' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Les sociétés adhérentes',
'/societes-adherentes',
self::TEMPLATE_STRUCTURES
);
}
// > 'Présentation > Historique' page (hidden by default)
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Historique',
'/historique',
'',
['hidden' => 1]
);
// ~ Contact shortcut will be created after the contact page
// > 'Actualités' page (hidden by default)
$this->insertPage(
$websiteUid,
$rootUid,
'Actualités',
'/actualites',
self::TEMPLATE_NEWS,
['hidden' => 1]
);
// > 'Saison en cours' page
$this->insertPage(
$websiteUid,
$rootUid,
'Saison en cours',
'/saison-en-cours'
);
// > 'Saison en cours > Les évènements' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/saison-en-cours'],
'Les évènements',
'/les-evenements',
self::TEMPLATE_EVENTS
);
if ($isNetwork) {
// > 'Présentation > Les sociétés adhérentes' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Évènements des structures',
'/evenements-des-structures',
self::TEMPLATE_STRUCTURESEVENTS
);
}
// > 'Vie interne' page (restricted, hidden by default)
$this->insertPage(
$websiteUid,
$rootUid,
'Vie interne',
'/vie-interne',
'',
[
'hidden' => 1,
'fe_group' => -2
]
);
// > 'Footer' page (not in the menu)
$this->insertPage(
$websiteUid,
$rootUid,
'Footer',
'/footer',
'',
[
'dokType' => self::DOK_FOLDER,
'nav_hide' => 1
]
);
// > 'Footer > Contact' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/footer'],
'Contact',
'/contact',
self::TEMPLATE_CONTACT
);
// > 'Footer > Plan du site' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/footer'],
'Plan du site',
'/plan-du-site'
);
// > 'Footer > Mentions légales' page
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/footer'],
'Mentions légales',
'/mentions-legales',
self::TEMPLATE_LEGAL
);
// > 'Présentation > Contact' shortcut
$this->insertPage(
$websiteUid,
$this->createdPagesIndex['/presentation'],
'Contact',
'/ecrivez-nous',
'',
[
'dokType' => self::DOK_SHORTCUT,
'shortcut' => $this->createdPagesIndex['/contact']
]
);
// > 'Page introuvable' page (not in the menu, read-only)
$this->insertPage(
$websiteUid,
$rootUid,
'Page introuvable',
'/page-introuvable',
'',
[
'nav_hide' => 1,
'no_search' => 1
]
);
// Add content to these pages
// >> root page content
$this->insertContent(
$rootUid,
self::CTYPE_TEXTPIC,
'
Bienvenue sur le site de ' . $organization->getName() . '.
',
0
);
// >> page 'qui sommes nous?'
$this->insertContent(
$this->createdPagesIndex['/qui-sommes-nous'],
self::CTYPE_TEXT,
'Qui sommes nous ...',
0
);
// >> page 'historique'
$this->insertContent(
$this->createdPagesIndex['/historique'],
self::CTYPE_TEXT,
"Un peu d'histoire ...",
0
);
// >> page 'plan du site'
$this->insertContent(
$this->createdPagesIndex['/plan-du-site'],
self::CTYPE_SITEMAP
);
// update sys_template
$constants = $this->getTemplateConstants($organizationId, $organizationExtraData);
$include = "EXT:fluid_styled_content/Configuration/TypoScript/";
$include .= ",EXT:fluid_styled_content/Configuration/TypoScript/Styling/";
$include .= ",EXT:form/Configuration/TypoScript/";
$include .= ",EXT:news/Configuration/TypoScript";
$include .= ",EXT:frontend_editing/Configuration/TypoScript";
$include .= ",EXT:frontend_editing/Configuration/TypoScript/FluidStyledContent9";
$include .= ",EXT:ot_templating/Configuration/TypoScript";
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->insert('sys_template')
->values([
'pid' => $rootUid,
'title' => $organization->getName(),
'sitetitle' => $organization->getName(),
'root' => 1,
'clear' => 3,
'config' => "config.frontend_editing = 1",
'include_static_file' => $include,
'constants' => $constants
])
->execute();
// ## Create the site config.yaml file
$identifier = $this->writeConfigFile(
$rootUid,
true
);
// Update the ot_website identifier
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->update('ot_websites')
->set('config_identifier', $identifier)
->where($queryBuilder->expr()->eq('uid', $websiteUid))
->execute();
// Create the user_upload and form_definitions directories and update the sys_filemounts table
$uploadRelPath = "/user_upload/" . $organizationId;
$fileadminDir = $_ENV['TYPO3_PATH_APP'] . "/public/fileadmin";
$uploadDir = $fileadminDir . "/" . $uploadRelPath;
if (file_exists($uploadDir)) {
throw new \RuntimeException("A directory or file " . $uploadDir . " already exists. Abort.");
}
$formsRelPath = '/form_definitions/' . $organizationId;
$formsDir = $fileadminDir . $formsRelPath;
if (file_exists($formsDir)) {
throw new \RuntimeException("A directory or file " . $formsDir . " already exists. Abort.");
}
$this->mkDir($uploadDir);
$this->mkDir($formsDir);
// Insert the filemounts points (sys_filemounts)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder->insert('sys_filemounts')
->values([
'title' => 'Documents',
'path' => rtrim($uploadRelPath, '/') . '/',
'base' => 1
])
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder->insert('sys_filemounts')
->values([
'title' => 'Forms_' . $organizationId,
'path' => rtrim($formsRelPath, '/') . '/',
'base' => 1
])
->execute();
// Create the BE Editors group
// -- NB: this user will then be auto-updated by the ot_connect extension --
$beGroupUid = $this->createOrUpdateBeGroup(
$organizationId,
$rootUid,
$organizationExtraData['admin']
);
// Create the BE User
// -- NB: this user will then be auto-updated by the ot_connect extension --
$beUserUid = $this->createOrUpdateBeUser(
$organizationId,
$rootUid,
$beGroupUid,
$organizationExtraData['admin']
);
// Update the user TsConfig
$tsconfig = "options.uploadFieldsInTopOfEB = 1\n" .
"options.defaultUploadFolder=1:" . rtrim($uploadRelPath, '/') . "/\n";
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
$queryBuilder
->update('be_users')
->where($queryBuilder->expr()->eq('uid', $beUserUid))
->set('TSconfig', $tsconfig)
->execute();
// Setup user and group rights
$this->setBeUserPerms($organizationId, false, $beGroupUid, $beUserUid);
// Try to commit the result
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while committing the result');
}
} catch(\Throwable $e) {
// rollback
$this->connectionPool->getConnectionByName('Default')->rollback();
// remove created files and dirs
foreach (array_reverse($this->createdFiles) as $filename) {
unlink($filename);
}
$this->createdFiles = [];
foreach (array_reverse($this->createdDirs) as $dirname) {
rmdir($dirname);
}
$this->createdDirs = [];
throw $e;
}
// Extra steps that do not need any rollback:
$this->enableFeEditing($beUserUid);
return $rootUid;
}
/**
* Performs an update of the organization's website based on data fetched from the opentalent DB:
*
* - Update the pages table (structure id, structure domain)
* - (hard update only) Update the config.yaml file
* - Update the `sys_template`.`constants` and the `pages`.`TSConfig` fields
* - (hard update only) Reset the users permissions
* - Clear the Typo3 cache for the website
*
* @param int $organizationId
* @return int
* @throws NoSuchOrganizationException
* @throws NoSuchRecordException
* @throws NoSuchWebsiteException
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Doctrine\DBAL\DBALException
* @throws \Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException
* @throws \Throwable
*/
public function updateSiteAction(int $organizationId): int
{
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid']);
$organization = $this->fetchOrganization($organizationId);
// This extra-data can not be retrieved from the API for now, but
// this shall be set up as soon as possible, to avoid requesting
// the prod-back DB directly.
$organizationExtraData = $this->fetchOrganizationExtraData($organizationId);
// start transactions
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
// keep tracks of the created folders and files to be able to remove them during a rollback
try {
// ## Update the ot_website table
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->update('ot_websites')
->set('subdomain', $organization->getSubDomain())
->set('organization_name', $organization->getName())
->where($queryBuilder->expr()->eq('uid', $website['uid']))
->execute();
// ## Update the subpages of the rootpage
$sitePages = $this->otPageRepository->getAllSubpagesForPage($rootUid);
foreach ($sitePages as $page) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->update('pages')
->set('ot_website_uid', $website['uid'])
->where($queryBuilder->expr()->eq('uid', $page['uid']))
->execute();
}
// ## Update the `sys_template`.`constants` and the `pages`.`TSConfig` fields
$constants = $this->getTemplateConstants($organizationId, $organizationExtraData);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder
->update('sys_template')
->set('constants', $constants)
->where($queryBuilder->expr()->eq('pid', $rootUid))
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder
->update('pages')
->set('TSconfig', 'TCAdefaults.pages.ot_website_uid=' . $website['uid'])
->where($queryBuilder->expr()->eq('uid', $rootUid))
->execute();
// ## Update the config.yaml file
$identifier = $this->otWebsiteRepository->findConfigIdentifierFor($rootUid);
$this->writeConfigFile($rootUid, true, $identifier);
// ## Update the ot_website identifier
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->update('ot_websites')
->set('config_identifier', $identifier)
->where($queryBuilder->expr()->eq('uid', $website['uid']))
->execute();
// Try to commit the result
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while committing the result');
}
} catch(\Throwable $e) {
// rollback
$this->connectionPool->getConnectionByName('Default')->rollback();
throw $e;
}
// ## Clear the Typo3 cache for the website
OtCacheManager::clearSiteCache($rootUid, true);
return $rootUid;
}
/**
* Delete the website with all its pages, contents and related records
*
* If the hard parameter is false, the records' `deleted` field will be set to true and
* the files and directories will be renamed. This kind of 'soft' deletion can be undone.
*
* Otherwise, if hard is set to true, the records and files will be permanently removed,
* with no possibility of undoing anything. In this case, you'll have to confirm your intention
* by creating a file in the Typo3 root directory, named 'DEL####' (#### is the organization id)
*
* $redirectTo is the optional organization id to whom requests will be redirected
*
* The $force parameter, if true, will both bypass the 'DEL###' file confirmation and recursively delete the
* user_upload and form_definitions of the website. USE WITH CAUTION
*
* @param int $organizationId
* @param bool $hard
* @param int|null $redirectTo
* @param bool $force
* @return int
* @throws NoSuchRecordException
* @throws NoSuchWebsiteException
* @throws \Doctrine\DBAL\DBALException
* @throws \Throwable
*/
public function deleteSiteAction(int $organizationId, bool $hard=false, ?int $redirectTo=null, bool $force = false): int
{
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
$websiteUid = $website['uid'];
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid']);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$isDeleted = $queryBuilder
->select('deleted')
->from('ot_websites')
->where($queryBuilder->expr()->eq('uid', $website['uid']))
->execute()
->fetchColumn(0) == 1;
$confirm_file = $_ENV['TYPO3_PATH_APP'] . "/DEL" . $organizationId;
if ($hard && !file_exists($confirm_file) && !$force) {
throw new \RuntimeException(
"You are going to completely delete the website with root uid " . $rootUid .
", and all of its pages, files, contents...etc. If you are sure, create a file named '" .
$confirm_file . "', and launch this command again."
);
}
// Prepare the redirection
if ($redirectTo) {
if ($redirectTo == $organizationId) {
throw new \InvalidArgumentException('redirectTo value has to be different from the organizationId');
}
$originDomain = $this->otWebsiteRepository->resolveWebsiteDomain($website);
$targetOrganizationWebsite = $this->otWebsiteRepository->getWebsiteByOrganizationId($redirectTo);
$targetDomain = $this->otWebsiteRepository->resolveWebsiteDomain($targetOrganizationWebsite);
}
// start transactions
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
// keep track of renamed file for an eventual rollback
$renamed = [];
try {
$pages = $this->otPageRepository->getAllSubpagesForPage($rootUid);
foreach($pages as $page) {
$this->delete('tt_content', 'pid', $page['uid'], $hard);
$this->delete('pages', 'uid', $page['uid'], $hard);
}
$this->delete('tt_content', 'pid', $rootUid, $hard);
$this->delete('pages', 'uid', $rootUid, $hard);
$this->delete('sys_template', 'pid', $rootUid, $hard);
$this->delete('ot_websites', 'uid', $websiteUid, $hard);
try {
$adminBeUserUid = $this->findAdminBeUserUid($rootUid);
if ($adminBeUserUid !== null) {
$this->delete('be_users', 'uid', $adminBeUserUid, $hard);
}
} catch (NoSuchRecordException $e) {}
try {
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid);
if ($editorsGroupUid !== null) {
$this->delete('be_groups', 'uid', $editorsGroupUid, $hard);
}
} catch (NoSuchRecordException $e) {}
// Delete the filemounts
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder
->select('uid')
->from('sys_filemounts')
->where("path LIKE '%user_upload/" . $organizationId . "/%'")
->orWhere("path LIKE '%form_definitions/" . $organizationId . "/%'");
$statement = $queryBuilder->execute();
$rows = $statement->fetchAll();
foreach ($rows as $row) {
$this->delete('sys_filemounts', 'uid', $row['uid'], $hard);
}
// Look up for the config.yaml file of the website
$configMainDir = $_ENV['TYPO3_PATH_APP'] . '/config/sites';
$configYamlFile = "";
foreach (glob($configMainDir . '/*', GLOB_ONLYDIR) as $subdir) {
if (!$isDeleted) {
$yamlFile = $subdir . '/config.yaml';
} else {
$yamlFile = $subdir . '/config.yaml.deleted';
}
if (is_file($yamlFile)) {
$conf = Yaml::parseFile($yamlFile);
if ($conf['rootPageId'] == $rootUid) {
$configYamlFile = $yamlFile;
break;
}
}
}
if (!$isDeleted) {
$uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/' . $organizationId . '/';
$formsDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/form_definitions/' . $organizationId . '/';
} else {
$uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/deleted_' . $organizationId . '/';
$formsDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/form_definitions/deleted_' . $organizationId . '/';
}
// If hard deletion, verify that dirs are empty
if (!$force) {
foreach ([$uploadDir, $formsDir] as $dir) {
if ($hard && is_dir($dir)) {
foreach (scandir($dir) as $subdir) {
if ($subdir == '.' or $subdir == '..') {
continue;
}
$subdir = $dir . $subdir;
if (!is_dir($subdir)) {
throw new \RuntimeException(
'The directory ' . $dir . ' contains non-directory files' .
', this humble script prefers not to take care of them automatically. Abort.');
}
if (is_readable($subdir)) {
foreach (scandir($subdir) as $filename) {
if ($filename != '.' && $filename != '..') {
throw new \RuntimeException(
'The directory ' . $subdir . ' is not empty, ' .
'this humble script prefers not to take care of them automatically. Abort.');
}
}
}
}
}
}
}
// If soft deletion, check that no deleted file or directory exist
if (!$hard) {
$toRename = [];
if (!$hard) {
if (is_file($configYamlFile)) {
$toRename[$configYamlFile] = $configYamlFile . '.deleted';
}
if (is_dir($uploadDir)) {
$toRename[$uploadDir] = dirname($uploadDir) . '/deleted_' . basename($uploadDir);
}
}
foreach ($toRename as $initialPath => $newPath) {
if (is_file($newPath)) {
throw new \RuntimeException(
'A file or directory named ' . $newPath . ' already exists, what happened?. Cancel.');
}
}
}
// Delete or rename files and dirs
if ($hard) {
if (is_file($configYamlFile)) {
unlink($configYamlFile);
}
if (is_dir(dirname($configYamlFile))) {
FileUtility::rmdir(dirname($configYamlFile), $force);
}
if (is_dir($uploadDir)) {
FileUtility::rmdir($uploadDir, $force);
}
if (is_dir($formsDir)) {
FileUtility::rmdir($formsDir, $force);
}
} else {
$renamed = [];
foreach ($toRename as $initialPath => $newPath) {
rename($initialPath, $newPath);
$renamed[$initialPath] = $newPath;
}
}
// Add the redirection
if ($redirectTo) {
$this->addRedirection($originDomain, $targetDomain);
}
// Try to commit the result
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while commiting the result');
}
return $rootUid;
} catch(\Throwable $e) {
// rollback
$this->connectionPool->getConnectionByName('Default')->rollback();
if (!$hard) {
foreach ($renamed as $initialPath => $newPath) {
rename($newPath, $initialPath);
}
}
throw $e;
} finally {
if (file_exists($confirm_file)) {
unlink($confirm_file);
}
}
}
/**
* Delete a record from the typo3 db.
* If $hard is true, the record is permanently deleted.
* Else, it's just marked as deleted.
*
* @param string $table
* @param string $whereKey
* @param $whereValue
* @param int $hard
*/
private function delete(string $table, string $whereKey, $whereValue, $hard=0) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
if (!$hard) {
$queryBuilder
->update($table)
->set('deleted', 1)
->where($queryBuilder->expr()->eq($whereKey, $whereValue))
->execute();
} else {
$queryBuilder
->delete($table)
->where($queryBuilder->expr()->eq($whereKey, $whereValue))
->execute();
}
}
/**
* Undo a soft-deletion performed using deleteSiteAction()
*
* @param int $organizationId
* @return int
* @throws NoSuchWebsiteException
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Doctrine\DBAL\DBALException
*/
public function undeleteSiteAction(int $organizationId): int
{
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId, false);
$websiteUid = $website['uid'];
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid'], false);
// start transactions
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
// keep track of renamed file for an eventual rollback
$renamed = [];
try {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
->update('ot_websites')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('uid', $websiteUid))
->execute();
$pages = $this->otPageRepository->getAllSubpagesForPage($rootUid);
foreach($pages as $page) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
->update('tt_content')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('pid', $page['uid']))
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder
->update('pages')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('uid', $page['uid']))
->execute();
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
->update('tt_content')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('pid', $rootUid))
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder
->update('pages')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('uid', $rootUid))
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder
->update('sys_template')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('pid', $rootUid))
->execute();
// remove filemounts
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder
->update('sys_filemounts')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('path', "'/user_upload/" . $organizationId . "/'"))
->execute();
try {
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid, false);
if ($editorsGroupUid !== null) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
$queryBuilder
->update('be_groups')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('uid', $editorsGroupUid))
->execute();
}
} catch (NoSuchRecordException $e) {}
try {
$adminBeUserUid = $this->findAdminBeUserUid($rootUid, false);
if ($adminBeUserUid !== null) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
$queryBuilder
->update('be_users')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('uid', $adminBeUserUid))
->execute();
}
} catch (NoSuchRecordException $e) {}
// Look up for the config.yaml file of the website
$configMainDir = $_ENV['TYPO3_PATH_APP'] . '/config/sites';
$configYamlFile = "";
foreach (glob($configMainDir . '/*', GLOB_ONLYDIR) as $subdir) {
$yamlFile = $subdir . '/config.yaml.deleted';
if (is_file($yamlFile)) {
$conf = Yaml::parseFile($yamlFile);
if ($conf['rootPageId'] == $rootUid) {
$configYamlFile = $yamlFile;
break;
}
}
}
$uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/deleted_' . $organizationId . '/';
$formsDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/form_definitions/deleted_' . $organizationId . '/';
$toRename = [];
if (is_file($configYamlFile)) {
$toRename[$configYamlFile] = dirname($configYamlFile) . '/config.yaml';
}
if (is_dir($uploadDir)) {
$toRename[$uploadDir] = dirname($uploadDir) . '/' . $organizationId;
}
if (is_dir($formsDir)) {
$toRename[$formsDir] = dirname($formsDir) . '/' . $organizationId;
}
foreach ($toRename as $initialPath => $newPath) {
if (is_file($newPath)) {
throw new \RuntimeException(
'A file or directory named ' . $newPath . ' already exists, what happened?. Cancel.');
}
}
$renamed = [];
foreach ($toRename as $initialPath => $newPath) {
rename($initialPath, $newPath);
$renamed[$initialPath] = $newPath;
}
// remove eventual redirection from this site to another
$originUrl = $this->getSiteInfosAction($organizationId)->getBaseUrl();
$this->removeRedirectionsFrom($originUrl);
// Try to commit the result
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while commiting the result');
}
return $rootUid;
} catch(\Throwable $e) {
// rollback
$this->connectionPool->getConnectionByName('Default')->rollback();
foreach ($renamed as $initialPath => $newPath) {
rename($newPath, $initialPath);
}
throw $e;
}
}
/**
* Clear the cache of the organization's website
*
* @param int $organizationId the organization's id whom site cache should be cleared
* @param bool $clearAll if true, all caches will be cleared, and not only the frontend one
* @return int
* @throws NoSuchWebsiteException
*/
public function clearSiteCacheAction(int $organizationId, $clearAll=false): int
{
$rootUid = $this->otWebsiteRepository->findRootUidForOrganization($organizationId);
OtCacheManager::clearSiteCache($rootUid, $clearAll);
return $rootUid;
}
/**
* Perform a full scan of the website and returns a list of warnings
*
* @param int $organizationId
* @param int $rootUid
* @return array
* @throws NoSuchWebsiteException
*/
private function scanSite(int $organizationId, int $rootUid): array
{
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid']);
$warnings = [];
// fetch and index pages and root page
$pages = $this->otPageRepository->getPageWithSubpages($rootUid);
$rootPage = null;
$pageIndex = [];
foreach ($pages as $page) {
$pageIndex[$page['uid']] = $page;
if ($page['is_siteroot'] == 1) { $rootPage = $page; }
}
// fetch organization and extradata
$organization = $this->fetchOrganization($organizationId);
$extraData = $this->fetchOrganizationExtraData($organizationId);
// load site's settings (uncomment if needed)
// $config = $this->otWebsiteRepository->findConfigFor($rootUid);
// Check site's title
if (trim($website['organization_name']) != trim($organization->getName())) {
$warnings[] = "Website's organization name is different from what is registered in the Opentalent DB";
}
if (trim($rootPage['title']) != trim($organization->getName())) {
$warnings[] = "Root page's title does not match the organization name";
}
// Who is the expected owner among the be_users? there should be only one.
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$beUsers = $queryBuilder
->select('uid', 'username')
->from('be_users')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
->execute()
->fetchAll();
$owner = null;
if (count($beUsers) > 1) {
$warnings[] = 'Website is mounted on more than one be_user: ' .
join(', ', array_map(function($u) { return $u['username']; } ,$beUsers));
} elseif (count($beUsers) == 0) {
$warnings[] = 'Website is not mounted on any be_user';
} else {
$owner = $beUsers[0];
}
// are template constants up to date?
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$actual_constants = $queryBuilder
->select('constants')
->from('sys_template')
->where($queryBuilder->expr()->eq('pid', $rootUid))
->execute()
->fetchColumn(0);
$expected_constants = $this->getTemplateConstants($organizationId, $extraData);
$norm = function ($s) {
return strtolower(preg_replace('/\s/', '', $s));
};
if ($norm($expected_constants) != $norm($actual_constants)) {
$warnings[] = 'Template constants need an update';
}
$expected_templates = [
"OpenTalent.OtTemplating->home" => 0,
"OpenTalent.OtTemplating->legal" => 0,
"OpenTalent.OtTemplating->contact" => 0
];
foreach ($pages as $page) {
if ($page['deleted']) {
continue;
}
// Is it the correct owner?
if ($owner !== null && !$page['deleted'] && $page['perms_userid'] != $owner['uid']) {
$warnings[] = 'Page ' . $page['uid'] . ' has wrong owner';
}
if (!$page['is_siteroot']) {
// is the parent page state (deleted, hidden, restricted) the same as this page?
$parent = $pageIndex[$page['pid']];
if ($parent['deleted']) {
$warnings[] = 'The non-deleted page ' . $page['uid'] . ' has a deleted parent page';
}
if ($parent['hidden'] && !$page['hidden']) {
$warnings[] = 'The non-hidden page ' . $page['uid'] . ' has a hidden parent page';
}
if ($parent['fe_group'] < 0 && !$page['fe_group'] >= 0) {
$warnings[] = 'The non-restricted page ' . $page['uid'] . ' has a restricted parent page';
}
}
// an expected template was found, remove it from the list of expected
if (in_array($page['tx_fed_page_controller_action'], $expected_templates) &&
!$page['deleted'] && !$page['hidden']) {
unset($expected_templates[$page['tx_fed_page_controller_action']]);
}
}
foreach ($expected_templates as $template => $_) {
$warnings[] = 'No page with template ' . $template;
}
return $warnings;
}
/**
* Get the current status and informations of the organization's website
* If $fullScan is true, a deeper scan will be performed and warnings may be logged
*
* The status is among:
*
* - STATUS_NO_SUCH_WEBSITE
* - STATUS_EXISTING
* - STATUS_EXISTING_DELETED
* - STATUS_EXISTING_HIDDEN
* - STATUS_EXISTING_WITH_WARNINGS
*
* @param int $organizationId the organization's id whom site cache should be cleared
* @param bool $fullScan If true, a 'warnings' entry will be added to the result, and a full scan of
* the website pages will be performed.
* @return SiteStatus
* @throws NoSuchWebsiteException
*/
public function getSiteStatusAction(int $organizationId, bool $fullScan = false): SiteStatus
{
try {
$siteInfos = $this->getSiteInfosAction($organizationId);
} catch (NoSuchWebsiteException $e) {
return new SiteStatus($organizationId, SiteStatus::STATUS_NO_SUCH_WEBSITE);
}
if ($siteInfos->isDeleted()) {
return new SiteStatus($organizationId, SiteStatus::STATUS_EXISTING_DELETED, $siteInfos);
}
if ($siteInfos->isHiddenOrRestricted()) {
return new SiteStatus($organizationId, SiteStatus::STATUS_EXISTING_HIDDEN, $siteInfos);
}
$warnings = null;
if ($fullScan) {
// ** Look for potential issues
$warnings = $this->scanSite($organizationId, $siteInfos->getRootUid());
}
return new SiteStatus(
$organizationId,
$warnings ? SiteStatus::STATUS_EXISTING_WITH_WARNINGS : SiteStatus::STATUS_EXISTING,
$siteInfos,
$warnings
);
}
/**
* Set a new domain for the website.
* If $redirect is true, also add a redirection from the former domain to the new one
*
* @param int $organizationId
* @param string $newDomain
* @param bool $redirect
* @return int
* @throws NoSuchWebsiteException
* @throws \Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException
*/
public function setSiteCustomDomainAction(int $organizationId, string $newDomain, bool $redirect = true): int
{
if (!preg_match(self::RX_DOMAIN,$newDomain) &&
!preg_match("/[a-z0-9A-Z-]+\//", $newDomain)) {
throw new \InvalidArgumentException("The given domain does not seems to be a valid domain: " . $newDomain);
}
$website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
$websiteUid = $website['uid'];
$rootUid = $this->otWebsiteRepository->getWebsiteRootUid($websiteUid);
// ## Update the ot_website table
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->update('ot_websites')
->set('custom_domain', $newDomain)
->where($queryBuilder->expr()->eq('uid', $websiteUid))
->execute();
$originDomain = $this->otWebsiteRepository->resolveWebsiteDomain($website);
if (preg_replace('/https?:\/\//', '', $originDomain) == preg_replace('/https?:\/\//', '', $newDomain) ) {
throw new \RuntimeException('The new domain should be different of the current one');
}
$this->writeConfigFile($organizationId, $rootUid, $newDomain);
if ($redirect) {
// Add the redirection
$this->addRedirection($originDomain, $newDomain);
}
return $rootUid;
}
/**
* Return all of the redirections from the given domain name,
* even if they have been marked as deleted.
*
* @param $domain string Domain name, without the http(s):// part
* @return array Rows from the sys_redirect table
*/
protected function getRedirectionsFrom(string $domain): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$queryBuilder->getRestrictions()->removeAll();
return $queryBuilder
->select('*')
->from('sys_redirect')
->where($queryBuilder->expr()->eq('source_host', $queryBuilder->expr()->literal($domain)))
->execute()
->fetchAll();
}
/**
* Return all of the redirections to the given domain name,
* even if they have been marked as deleted.
*
* @param $domain string Domain name, without the http(s):// part
* @return array Rows from the sys_redirect table
*/
protected function getRedirectionsTo(string $domain): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$queryBuilder->getRestrictions()->removeAll();
return $queryBuilder
->select('*')
->from('sys_redirect')
->where(
$queryBuilder->expr()->in(
'target',
[
'http://' . $queryBuilder->expr()->literal($domain),
'https://' . $queryBuilder->expr()->literal($domain)
]
)
)
->execute()
->fetchAll();
}
/**
* Add a new redirection from $fromDomain to $toDomain.
* If this redirection already exists but has been deleted and/or disabled, it will be restored and enabled
* If a redirection already exists but is not deleted and targets another domain, a RuntimeException will be thrown.
*
* @param $fromDomain
* @param $toDomain
* @return int Status of the operation
*/
public function addRedirection($fromDomain, $toDomain): int
{
$fromDomain = preg_replace('/https?:\/\//', '', $fromDomain);
$toDomain = preg_replace('/https?:\/\//', '', $toDomain);
if (!preg_match(self::RX_DOMAIN, $fromDomain)) {
throw new \InvalidArgumentException("The does not seems to be a valid domain: " . $fromDomain);
}
if (!preg_match(self::RX_DOMAIN, $toDomain)) {
throw new \InvalidArgumentException("The does not seems to be a valid domain: " . $toDomain);
}
$existing = $this->getRedirectionsFrom($fromDomain);
$toUpdate = null;
foreach ($existing as $redirection) {
if (!$redirection['deleted'] && !$redirection['disabled']) {
// a redirection from this domain already exists, and it is not deleted nor disabled
if (!preg_match('/https?:\/\/' . $toDomain . '/', $redirection['target'])) {
// the target is not the same domain
throw new \RuntimeException(
'A redirection is already active for ' . $fromDomain . ' targeting ' . $redirection['target']
);
} else {
// else, target is already the same domain, it will be updated
$toUpdate = $redirection;
break;
}
} else {
// a redirection from this domain already exists, but it is deleted or disabled
if (preg_match('/https?:\/\/' . $toDomain . '/', $redirection['target'])) {
// the target is the same domain, we'll reactivate this record
$toUpdate = $redirection;
break;
}
}
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$data = [
'deleted' => 0,
'disabled' => 0,
'source_host' => $fromDomain,
'source_path' => '/.*/',
'is_regexp' => 1,
'force_https' => 0,
'respect_query_parameters' => 0,
'keep_query_parameters' => 1,
'target' => 'https://' . $toDomain,
'target_statuscode' => 301
];
if ($toUpdate !== null) {
$q = $queryBuilder
->update('sys_redirect')
->where($queryBuilder->expr()->eq('uid', $toUpdate['uid']));
foreach ($data as $k => $v) {
$q->set($k, $v);
}
$q->execute();
return self::REDIRECTION_UPDATED;
} else {
$queryBuilder
->insert('sys_redirect')
->values($data)
->execute();
return self::REDIRECTION_CREATED;
}
}
/**
* Remove any existing redirection from $fromDomain to any url by marking it as deleted.
* If $hard is true, delete it completely.
*
* @param $fromDomain
* @param bool $hard
* @return int Number of affected rows
*/
public function removeRedirectionsFrom($fromDomain, $hard=false): int
{
$fromDomain = preg_replace('/https?:\/\//', '', $fromDomain);
if (!preg_match(self::RX_DOMAIN, $fromDomain)) {
throw new \InvalidArgumentException("The does not seems to be a valid domain: " . $fromDomain);
}
$existing = $this->getRedirectionsFrom($fromDomain);
$deleted = 0;
foreach ($existing as $redirection) {
$this->delete('sys_redirect', 'uid', $redirection['uid'], $hard);
$deleted += 1;
}
return $deleted;
}
/**
* Set the rights of admin and editors of the website
* on all of the existing pages, including deleted ones
*
* @param int $organizationId
* @param bool $createIfMissing Create the admin be user and/or the editors group if they are not found in the DB
* @param int|null $editorsGroupUid Force the editors be-group uid
* @param int|null $adminUid Force the admin be-user uid
* @return int The uid of the website root page
* @throws NoSuchWebsiteException
* @throws NoSuchRecordException
*/
protected function setBeUserPerms(
int $organizationId,
bool $createIfMissing = false,
int $editorsGroupUid = null,
int $adminUid = null
): int
{
if ($createIfMissing && ($editorsGroupUid || $adminUid)) {
throw new \InvalidArgumentException("You can not set $createIfMissing to true " .
"and force the admin or group uid at the same time.");
}
$rootUid = $this->otWebsiteRepository->findRootUidForOrganization($organizationId);
$organizationExtraData = $this->fetchOrganizationExtraData($organizationId);
$isPremium = self::IS_PRODUCT_PREMIUM[$organizationExtraData['admin']['product']];
if ($editorsGroupUid == null) {
try {
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid);
} catch (NoSuchRecordException $e) {
if (!$createIfMissing) {
throw $e;
}
}
}
if ($adminUid == null) {
try {
$adminUid = $this->findAdminBeUserUid($rootUid);
} catch (NoSuchRecordException $e) {
if (!$createIfMissing) {
throw $e;
}
}
}
// Creates or update the admin be_group
$editorsGroupUid = $this->createOrUpdateBeGroup(
$organizationId,
$rootUid,
$organizationExtraData['admin'],
$editorsGroupUid
);
// Creates or update the admin be_user
$adminUid = $this->createOrUpdateBeUser(
$organizationId,
$rootUid,
$editorsGroupUid,
$organizationExtraData['admin'],
$adminUid
);
// Reset the appartenance to groups
$adminGroupUid = $this->getBaseBeGroupUid($isPremium ? self::BEGROUP_ADMIN_PREMIUM : self::BEGROUP_ADMIN_STANDARD);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
$queryBuilder
->update('be_users')
->where($queryBuilder->expr()->eq('uid', $adminUid))
->set('usergroup', $adminGroupUid . ',' . $editorsGroupUid)
->execute();
$mainEditorGroupUid = $this->getBaseBeGroupUid($isPremium ? self::BEGROUP_EDITOR_PREMIUM : self::BEGROUP_EDITOR_STANDARD);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
$queryBuilder
->update('be_groups')
->where($queryBuilder->expr()->eq('uid', $editorsGroupUid))
->set('subgroup', $mainEditorGroupUid)
->execute();
// setup default owner for the website
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$tsConfig = $queryBuilder->select('TSconfig')
->from('pages')
->where($queryBuilder->expr()->eq('uid', $rootUid))
->execute()
->fetchColumn(0);
$tsConfig = trim(preg_replace('/TCEMAIN {[^{]*}/', '', $tsConfig));
$tsConfig .= "\nTCEMAIN {\n" .
" permissions.userid = " . $adminUid ."\n" .
" permissions.groupid = " . $editorsGroupUid . "\n" .
"}";
$queryBuilder
->update('pages')
->where($queryBuilder->expr()->eq('uid', $rootUid))
->set('TSconfig', $tsConfig)
->execute();
// fetch pages and root page
$pages = $this->otPageRepository->getPageWithSubpages($rootUid);
// To understand how the rights levels are computed:
// @see https://ressources.opentalent.fr/display/EX/Droits+des+BE+Users
foreach ($pages as $page) {
if ($page['is_siteroot']) {
$adminPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
if ($isPremium) {
$adminPerms += self::PERM_NEW;
}
$editorsPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT;
} else if (
$page['slug'] == '/footer' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->legal' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->contact' ||
$page['slug'] == '/plan-du-site'
) {
$adminPerms = self::PERM_SHOW;
if ($isPremium) {
$adminPerms += self::PERM_NEW;
}
$editorsPerms = self::PERM_SHOW;
} else if (
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->members' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->membersCa' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->structures' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->events' ||
$page['tx_fed_page_controller_action'] == 'OpenTalent.OtTemplating->structuresEvents'
) {
$adminPerms = self::PERM_SHOW;
if ($isPremium) {
$adminPerms += self::PERM_NEW + self::PERM_EDIT_PAGE;
}
$editorsPerms = self::PERM_SHOW;
} else {
$adminPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
if ($isPremium) {
$adminPerms += self::PERM_DELETE + self::PERM_NEW;
}
$editorsPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
if ($isPremium) {
$editorsPerms += self::PERM_NEW;
}
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder
->update('pages')
->where($queryBuilder->expr()->eq('uid', $page['uid']))
->set('perms_userid', $adminUid)
->set('perms_groupid', $editorsGroupUid)
->set('perms_user', $adminPerms)
->set('perms_group', $editorsPerms)
->set('perms_everybody', 0)
->execute();
}
return $rootUid;
}
/**
* Action for resetting the rights of admin and editors of the website
* on all of the existing pages, including deleted ones
*
* @param int $organizationId
* @param bool $createIfMissing
* @return int
* @throws NoSuchRecordException
* @throws NoSuchWebsiteException
* @throws \Throwable
*/
public function resetBeUserPermsAction(int $organizationId, bool $createIfMissing = false): int
{
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
try {
$rootUid = $this->setBeUserPerms($organizationId, $createIfMissing);
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while commiting the result');
}
return $rootUid;
}
catch (\Throwable $e) {
// rollback
$this->connectionPool->getConnectionByName('Default')->rollback();
throw $e;
}
}
/**
* Delete then regenerate all of the typo3 sites yaml config files
*
* This is a more efficient alternative to the update --all, designed to be executed on a development environment
* just after the databases cloning.
*
* @throws NoSuchRecordException
* @throws NoSuchWebsiteException
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Doctrine\DBAL\DBALException
* @throws \Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException
* @throws \Throwable
*/
public function regenConfigFilesAction()
{
$configRootDir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/";
$backupConfigDir = $_ENV['TYPO3_PATH_APP'] . "/config/bkp_sites";
if (file_exists($backupConfigDir)) {
throw new \RuntimeException('A directory or a file with this name already exist: ' . $backupConfigDir);
}
$this->connectionPool->getConnectionByName('Default')->beginTransaction();
try {
// archive the existing files, in case a rollback is needed
rename($configRootDir, $backupConfigDir);
$this->mkDir($configRootDir);
$websites = $this->otWebsiteRepository->getAll();
foreach ($websites as $website) {
$identifier = $website['subdomain'] . '_' . $website['organization_id'];
$configDir = $configRootDir . $identifier;
$configFilename = $configDir . "/config.yaml";
$siteConfig = $this->otWebsiteRepository->generateWebsiteConfiguration($website, $identifier);
$config = $siteConfig->getConfiguration();
$yamlConfig = Yaml::dump($config, 99, 2);
$this->mkDir($configDir);
$this->writeFile($configFilename, $yamlConfig);
// ## Update the ot_website identifier
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->update('ot_websites')
->set('config_identifier', $identifier)
->where($queryBuilder->expr()->eq('uid', $website['uid']))
->execute();
}
$commitSuccess = $this->connectionPool->getConnectionByName('Default')->commit();
if (!$commitSuccess) {
throw new \RuntimeException('Something went wrong while committing the result');
}
$this->rrmdir($backupConfigDir);
}
catch (\Throwable $e) {
// rollback
if (!file_exists($configRootDir)) {
$this->rrmdir($configRootDir);
};
if (!file_exists($backupConfigDir)) {
rename($backupConfigDir, $configRootDir);
}
$this->connectionPool->getConnectionByName('Default')->rollback();
throw $e;
}
}
/**
* Retrieve the Organization object from the repository and then,
* from the Opentalent API
*
* @param $organizationId
* @return Organization
* @throws NoSuchOrganizationException
*/
private function fetchOrganization($organizationId): Organization
{
$organizationRepository = GeneralUtility::makeInstance(ObjectManager::class)->get(OrganizationRepository::class);
try {
return $organizationRepository->findById($organizationId);
} catch (ApiRequestException $e) {
throw new NoSuchOrganizationException('Unable to fetch the organization with id: ' . $organizationId);
}
}
/**
* Insert a new row in the 'pages' table of the Typo3 DB
* and return its uid
*
* @param Organization $organization
* @return int
*/
private function insertOtWebsite(Organization $organization): int
{
$values = [
'organization_id' => $organization->getId(),
'subdomain' => $organization->getSubDomain(),
'locale' => 'fr_FR',
'organization_name' => $organization->getName()
];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
$queryBuilder->insert('ot_websites')
->values($values)
->execute();
return (int)$queryBuilder->getConnection()->lastInsertId();
}
/**
* Determine which folder-type Typo3 page should contain the new website
* CREATES IT if needed, and return its uid
*
* @return int
*/
private function getParentFolderUid(): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$siteCount = $queryBuilder
->count('uid')
->from('pages')
->where('is_siteroot=1')
->execute()
->fetchColumn(0);
$thousand = (int)(($siteCount + 1) / 1000);
$folderName = "Web Sites " . (1000 * $thousand) . " - " . ((1000 * $thousand) + 999);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$uid = $queryBuilder
->select('uid')
->from('pages')
->where($queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($folderName)))
->andWhere('dokType=254')
->execute()
->fetchColumn(0);
if ($uid == null) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->insert('pages')
->values([
'pid' => 0,
'title' => $folderName,
'dokType' => 254,
'sorting' => 11264,
'perms_userid' => 1,
'perms_groupid' => 31,
'perms_group' => 27,
])
->execute();
$uid = $queryBuilder->getConnection()->lastInsertId();
}
return $uid;
}
/**
* Insert a new row in the 'pages' table of the Typo3 DB
* and return its uid
*
* @param Organization $organization
* @param int $pid
* @param string $title
* @param string $slug
* @param string $template
* @param array $moreValues
* @return int
*/
private function insertPage(int $website_uid,
int $pid,
string $title,
string $slug,
string $template = '',
array $moreValues = []
): int
{
$defaultValues = [
'pid' => $pid,
'perms_groupid' => 3,
'perms_user' => 27,
'cruser_id' => 1,
'dokType' => self::DOK_PAGE,
'title' => $title,
'slug' => $slug,
'backend_layout' => 'flux__grid',
'backend_layout_next_level' => 'flux__grid',
'ot_website_uid' => $website_uid,
];
if ($template) {
$defaultValues['tx_fed_page_controller_action'] = $template;
$defaultValues['tx_fed_page_controller_action_sub'] = self::TEMPLATE_1COL;
}
$values = array_merge($defaultValues, $moreValues);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->insert('pages')
->values($values)
->execute();
$uid = (int)$queryBuilder->getConnection()->lastInsertId();
$this->createdPagesIndex[$slug] = $uid;
return $uid;
}
/**
* Insert the root page of a new organization's website
* and return its uid
*
* @param int $website_uid
* @param string $title
* @return int
*/
private function insertRootPage(int $website_uid, string $title): int
{
return $this->insertPage(
$website_uid,
$this->getParentFolderUid(),
$title,
'/',
self::TEMPLATE_HOME,
[
'is_siteroot' => 1,
'TSconfig' => 'TCAdefaults.pages.ot_website_uid=' . $website_uid
]
);
}
/**
* Insert a new row in the 'tt_content' table of the Typo3 DB
*
* @param int $pid
* @param string $cType
* @param string $bodyText
* @param int $colPos
* @param array $moreValues
*/
private function insertContent(int $pid,
string $cType=self::CTYPE_TEXT,
string $bodyText = '',
int $colPos=0,
array $moreValues = []) {
$defaultValues = [
'pid' => $pid,
'cruser_id' => 1,
'CType' => $cType,
'colPos' => $colPos,
'bodyText' => $bodyText
];
$values = array_merge($defaultValues, $moreValues);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->insert('tt_content')
->values($values)
->execute();
}
private function fetchOrganizationExtraData(int $organizationId) {
$cnn = new PDO(
"mysql:host=prod-back;dbname=opentalent",
'dbcloner',
'wWZ4hYcrmHLW2mUK',
array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8')
);
$cnn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $cnn->prepare(
"SELECT o.id, o.name, o.facebook, o.twitter, o.instagram,
o.category, o.logo_id, p.logoDonorsMove
FROM opentalent.Organization o INNER JOIN opentalent.Parameters p
ON o.parameters_id = p.id
WHERE o.id=" . $organizationId . ";"
);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$data = $stmt->fetch();
$stmt = $cnn->prepare(
"SELECT c.email
FROM opentalent.ContactPoint c
INNER JOIN opentalent.organization_contactpoint o
ON o.contactPoint_id = c.id
WHERE c.contactType = 'PRINCIPAL'
AND o.organization_id = " . $organizationId . ";"
);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$data['email'] = $stmt->fetch()['email'];
$stmt = $cnn->prepare(
"SELECT n.name, n.logo, n.url
FROM opentalent.Network n
INNER JOIN
(opentalent.NetworkOrganization l
INNER JOIN opentalent.Organization o
ON l.organization_id = o.id)
ON l.network_id = n.id
WHERE l.endDate is NULL
AND o.id=" . $organizationId . ";"
);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$data['network'] = $stmt->fetch();
$stmt = $cnn->prepare(
"SELECT p.username, p.id as person_id, s.product
FROM opentalent.Person p
INNER JOIN opentalent.Access a ON p.id = a.person_id
INNER JOIN opentalent.Settings s on a.organization_id = s.organization_id
where a.organization_id=" . $organizationId . " AND a.adminAccess=1;"
);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$data['admin'] = $stmt->fetch();
return $data;
}
/**
* Return the content of `sys_template`.`constants` of
* the website of the given organization
*
* @param int $organizationId
* @param array $organizationExtraData
* @return string
*/
private function getTemplateConstants(int $organizationId, array $organizationExtraData): string
{
return "plugin.tx_ottemplating {\n" .
" settings {\n" .
" organization {\n" .
" id = " . $organizationId . "\n" .
" name = " . $organizationExtraData['name'] . "\n" .
" is_network = " . ($organizationExtraData['category'] == 'NETWORK' ? '1' : '0') . "\n" .
" email = " . $organizationExtraData['email'] . "\n" .
" logoid = " . $organizationExtraData['logo_id'] . "\n" .
" twitter = " . $organizationExtraData['twitter'] . "\n" .
" facebook = " . $organizationExtraData['facebook'] . "\n" .
" instagram = " . $organizationExtraData['instagram'] . "\n" .
" }\n" .
" network {\n" .
" logo = " . $organizationExtraData['network']['logo'] . "\n" .
" name = " . $organizationExtraData['network']['name'] . "\n" .
" url = " . $organizationExtraData['network']['url'] . "\n" .
" }\n" .
" }\n" .
"}";
}
/**
* Create the given directory, give its property to the www-data group and
* record it as a newly created dir (for an eventual rollback)
*
* @param string $dirPath
*/
private function mkDir(string $dirPath) {
mkdir($dirPath);
$this->createdDirs[] = $dirPath;
chgrp($dirPath, 'www-data');
}
/**
* Write the given file with content, give its property to the www-data group and
* record it as a newly created file (for an eventual rollback)
*
* @param string $path
* @param string $content
*/
private function writeFile(string $path, string $content) {
$f = fopen($path, "w");
try
{
fwrite($f, $content);
$this->createdFiles[] = $path;
chgrp($path, 'www-data');
} finally {
fclose($f);
}
}
/**
* Create or update the .../sites/.../config.yaml file of the given site
* Return the identifier of the created website
*
* @param int $rootUid
* @param bool $forceRecreate
* @param null $identifier
* @return string Identifier of the newly created configuration file
* @throws NoSuchWebsiteException
* @throws \Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException
*/
private function writeConfigFile(int $rootUid, bool $forceRecreate = false, $identifier = null): string
{
$website = $this->otWebsiteRepository->getWebsiteByPageUid($rootUid);
$domain = $this->otWebsiteRepository->resolveWebsiteDomain($website);
try {
$existing = $this->otWebsiteRepository->findConfigFileAndContentFor($rootUid, $identifier);
} catch (\RuntimeException $e) {
// identifier might be obsolete
$existing = $this->otWebsiteRepository->findConfigFileAndContentFor($rootUid);
}
$configFilename = $existing[0];
$config = $existing[1];
if (!$configFilename) {
$identifier = $website['subdomain'] . '_' . $website['organization_id'];
$configDir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/" . $identifier;
$configFilename = $configDir . "/config.yaml";
$isNew = true;
if (file_exists($configFilename)) {
throw new \RuntimeException("A file named " . $configFilename . " already exists. Abort.");
}
} else {
$configDir = dirname($configFilename);
if ($identifier == null) {
$identifier = basename($configDir);
}
$config['base'] = 'https://' . $domain;
$isNew = false;
}
if ($isNew || $forceRecreate) {
$siteConfig = $this->otWebsiteRepository->generateWebsiteConfiguration($website, $identifier);
$config = $siteConfig->getConfiguration();
}
$yamlConfig = Yaml::dump($config, 99, 2);
if (!file_exists($configDir)) {
$this->mkDir($configDir);
}
$this->writeFile($configFilename, $yamlConfig);
// Set the owner and mods, in case www-data is not the one who run this command
// @see https://www.php.net/manual/fr/function.stat.php
try {
$stats = stat($_ENV['TYPO3_PATH_APP'] . '/public/index.php');
chown($configFilename, $stats['4']);
chgrp($configFilename, $stats['5']);
chmod($configFilename, $stats['2']);
} catch (\Exception $e) {
}
// Flush cache:
try {
$cacheSystem = $this->cacheManager->getCache('cache_core');
$cacheSystem->remove('site-configuration');
$cacheSystem->remove('pseudo-sites');
} catch (NoSuchCacheException $e) {
}
return $identifier;
}
/**
* Recursively remove the target directory (! no rollback available)
*/
private function rrmdir(string $dir) {
if (!is_dir($dir) || is_link($dir)) return unlink($dir);
foreach (scandir($dir) as $file) {
if ($file == '.' || $file == '..') continue;
if (!$this->rrmdir($dir . DIRECTORY_SEPARATOR . $file)) {
chmod($dir . DIRECTORY_SEPARATOR . $file, 0777);
if (!$this->rrmdir($dir . DIRECTORY_SEPARATOR . $file)) return false;
};
}
return rmdir($dir);
}
/**
* Create the BE user for the website, then return its uid
* The user shall be already created in the Opentalent DB
*
* @param int $organizationId
* @param int $rootUid
* @param int $siteGroupUid
* @param array $userData
* @param int|null $updateUid If passed, this method will update this be user instead of creating a new one
* @return int The uid of the created be_user
*/
private function createOrUpdateBeUser(int $organizationId,
int $rootUid,
int $siteGroupUid,
array $userData,
int $updateUid = null): int
{
if (!isset($userData['username'])) {
throw new \RuntimeException('Can not find any user with admin access in the Opentalent DB. Abort.');
}
// Since we don't want to store the password in the TYPO3 DB, we store a random string instead
$randomStr = (new Random)->generateRandomHexString(20);
$adminGroup = self::IS_PRODUCT_PREMIUM[$userData['product']] ? self::BEGROUP_ADMIN_PREMIUM : self::BEGROUP_ADMIN_STANDARD;
$adminGroupUid = $this->getBaseBeGroupUid($adminGroup);
$values = [
'username' => $userData['username'],
'password' => $randomStr,
'description' => '[Auto-generated] BE Admin for organization ' . $organizationId,
'deleted' => 0,
'lang' => 'fr',
'usergroup' => $siteGroupUid . ',' . $adminGroupUid,
'userMods' => null, // inherited from the base AdminGroup
'db_mountpoints' => null, // inherited from the editors group
'file_mountpoints' => null, // inherited from the editors group
'options' => 3, // allow to inherit both db and file mountpoints from groups
'tx_opentalent_opentalentId' => $userData['id'],
'tx_opentalent_organizationId' => $organizationId,
'tx_opentalent_generationDate' => date('Y/m/d H:i:s')
];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
if ($updateUid != null) {
$q = $queryBuilder->update('be_users')->where($queryBuilder->expr()->eq('uid', $updateUid));;
foreach ($values as $k => $v) {
$q->set($k, $v);
}
$q->execute();
return $updateUid;
} else {
$queryBuilder->insert('be_users')
->values($values)
->execute();
return (int)$queryBuilder->getConnection()->lastInsertId();
}
}
/**
* Create the BE editors group for the website, then return its uid
*
* @param int $organizationId
* @param int $rootUid
* @param array $userData
* @param int|null $updateUid If passed, this method will update this be group instead of creating a new one
* @return int The uid of the created be_group
*/
private function createOrUpdateBeGroup(int $organizationId,
int $rootUid,
array $userData,
int $updateUid = null): int
{
$groupName = 'editors_' . $organizationId;
// get the existing filemounts
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder
->select('uid')
->from('sys_filemounts')
->where("path LIKE '%user_upload/" . $organizationId . "/'")
->orWhere("path LIKE '%form_definitions/" . $organizationId . "/'");
$statement = $queryBuilder->execute();
$rows = $statement->fetchAll(3) ?: [];
$files = [];
foreach ($rows as $row) {
$files[] = $row[0];
}
$mainGroup = self::IS_PRODUCT_PREMIUM[$userData['product']] ? self::BEGROUP_EDITOR_PREMIUM : self::BEGROUP_EDITOR_STANDARD;
$mainGroupUid = $this->getBaseBeGroupUid($mainGroup);
$values = [
'title' => $groupName,
'deleted' => 0,
'subgroup' => $mainGroupUid,
'db_mountpoints' => $rootUid,
'file_mountPoints' => join(',', $files),
'file_permissions' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,replaceFile,moveFile,copyFile,deleteFile',
'groupMods' => '', // inherited from the base EditorsGroup
'pagetypes_select' => '', // inherited from the base EditorsGroup
'tables_select' => '', // inherited from the base EditorsGroup
'tables_modify' => '', // inherited from the base EditorsGroup
'non_exclude_fields' => '', // inherited from the base EditorsGroup
];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
if ($updateUid !== null) {
$q = $queryBuilder->update('be_groups')->where($queryBuilder->expr()->eq('uid', $updateUid));;
foreach ($values as $k => $v) {
$q->set($k, $v);
}
$q->execute();
return $updateUid;
} else {
$queryBuilder->insert('be_groups')
->values($values)
->execute();
return $queryBuilder->getConnection()->lastInsertId();
}
}
/**
* Try to find and return the uid of the editors be_group
* for this website
*
* @param int $rootUid
* @param bool $withRestrictions If false, the default restrictions won't apply, meaning this could return a deleted record
* @return int
* @throws NoSuchRecordException
*/
protected function findEditorsBeGroupUid(int $rootUid, bool $withRestrictions=true): int {
$editorsGroups = [
$this->getBaseBeGroupUid(self::BEGROUP_EDITOR_STANDARD),
$this->getBaseBeGroupUid(self::BEGROUP_EDITOR_PREMIUM)
];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
if (!$withRestrictions) {
$queryBuilder->getRestrictions()->removeAll();
}
$editorsGroupUid = $queryBuilder
->select('uid')
->from('be_groups')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
->andWhere('(FIND_IN_SET(' . $editorsGroups[0] . ', subgroup) > 0 OR FIND_IN_SET(' . $editorsGroups[1] . ', subgroup) > 0)')
->execute()
->fetchColumn(0);
if ($editorsGroupUid == null) {
throw new NoSuchRecordException("No editors be_group found " .
"among the groups that have this website mounted (root uid: " . $rootUid . ")");
}
return $editorsGroupUid;
}
/**
* Try to find and return the uid of the admin be_user
* for this website
*
* @param int $rootUid
* @param bool $withRestrictions If false, the default restrictions won't apply, meaning this could return a deleted record
* @return int
* @throws NoSuchRecordException
* @throws NoSuchWebsiteException
*/
protected function findAdminBeUserUid(int $rootUid, bool $withRestrictions=true): int {
$adminGroups = [
$this->getBaseBeGroupUid(self::BEGROUP_ADMIN_STANDARD),
$this->getBaseBeGroupUid(self::BEGROUP_ADMIN_PREMIUM)
];
$adminUid = null;
try {
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
if (!$withRestrictions) {
$queryBuilder->getRestrictions()->removeAll();
}
$adminUid = $queryBuilder
->select('uid')
->from('be_users')
->where('FIND_IN_SET(' . $editorsGroupUid . ', usergroup) > 0')
->andWhere('(FIND_IN_SET(' . $adminGroups[0] . ', usergroup) > 0 OR FIND_IN_SET(' . $adminGroups[1] . ', usergroup) > 0)')
->execute()
->fetchColumn(0);
return $adminUid;
} catch (NoSuchRecordException $e) {
// the editors group does not exist
}
// [For retrocompatibility] Try to find if there is a be_user still in the v8.7 data format
if ($adminUid == null) {
$website = $this->otWebsiteRepository->getWebsiteByPageUid($rootUid);
$extraData = $this->fetchOrganizationExtraData($website['organization_id']);
$expectedUsername = $extraData['admin']['username'];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
if (!$withRestrictions) {
$queryBuilder->getRestrictions()->removeAll();
}
$row = $queryBuilder
->select('uid', 'db_mountpoints')
->from('be_users')
->where($queryBuilder->expr()->eq('username', $queryBuilder->expr()->literal($expectedUsername)))
->execute()
->fetch();
if ($row['uid']) {
if ((string)$rootUid != (string)$row['db_mountpoints']) {
throw new \RuntimeException(
"The be_user named '" . $expectedUsername .
"' has unexpected mounted website(s) (expected: " . $rootUid .
", found: " . (string)$row['db_mountpoints'] . "). Abort."
);
}
$adminUid = $row['uid'];
}
}
if ($adminUid == null) {
throw new NoSuchRecordException("No admin be_user found " .
"among the users that have this website mounted (root uid: " . $rootUid . ")");
}
return $adminUid;
}
/**
* Return the uid of one of the base groups (BEGROUP_EDITOR_STANDARD, BEGROUP_EDITOR_PREMIUM, ...)
*
* @param int $groupType
* @return int
*/
protected function getBaseBeGroupUid(int $groupType): int
{
$expectedName = self::BEGROUP_NAME[$groupType];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_groups');
$uid = $queryBuilder
->select('uid')
->from('be_groups')
->where($queryBuilder->expr()->eq('title', $queryBuilder->expr()->literal($expectedName)))
->execute()
->fetchColumn(0);
if (!$uid) {
throw new \RuntimeException("Expects a BE group named '" . $expectedName . "', but none was found.");
}
return $uid;
}
/**
* Enable frontend editing for user
*
* @param int $adminUid
*/
private function enableFeEditing(int $adminUid) {
$BE_USER = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication');
$user = $BE_USER->getRawUserByUid($adminUid);
$BE_USER->user = $user;
$BE_USER->backendSetUC();
$BE_USER->uc['frontend_editing'] = 1;
$BE_USER->uc['frontend_editing_overlay'] = 1;
$BE_USER->writeUC($BE_USER->uc);
}
}