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 => "EditorStandard",
self::BEGROUP_EDITOR_PREMIUM => "EditorPremium",
self::BEGROUP_ADMIN_STANDARD => "AdminStandard",
self::BEGROUP_ADMIN_PREMIUM => "AdminPremium"
];
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 $connectionPool;
public function injectConnectionPool(\TYPO3\CMS\Core\Database\ConnectionPool $connectionPool)
{
$this->connectionPool = $connectionPool;
}
/**
* @var \TYPO3\CMS\Core\Cache\CacheManager
*/
private $cacheManager;
public function injectCacheManager(\TYPO3\CMS\Core\Cache\CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
/**
* 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
{
$rootUid = $this->findRootUidFor($organizationId);
$config = $this->findConfigFor($rootUid);
$organizationExtraData = $this->fetchOrganizationExtraData($organizationId);
$rootPage = $this->otPageRepository->getPage($rootUid);
$site = new SiteInfos(
$rootUid,
$rootPage['title'],
$config['base'],
$rootPage['tx_opentalent_template'],
$rootPage['tx_opentalent_template_preferences'],
$rootPage['tx_opentalent_matomo_id'],
self::IS_PRODUCT_PREMIUM[$organizationExtraData['admin']['product']] ?? false,
(bool)$rootPage['deleted'],
(bool)($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('pages')
->where($queryBuilder->expr()->eq('tx_opentalent_structure_id', $queryBuilder->createNamedParameter($organization->getId())))
->andWhere('is_siteroot=1')
->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 site pages:
// > Root page
$rootUid = $this->insertRootPage($organization);
// > 'Accueil' shortcut
$this->insertPage(
$organization,
$rootUid,
'Accueil',
'/accueil',
'',
[
'dokType' => self::DOK_SHORTCUT,
'shortcut' => $rootUid
]
);
// > 'Présentation' page
$this->insertPage(
$organization,
$rootUid,
'Présentation',
'/presentation'
);
// > 'Présentation > Qui sommes nous?' page (hidden by default)
$this->insertPage(
$organization,
$this->createdPagesIndex['/presentation'],
'Qui sommes nous?',
'/qui-sommes-nous',
'',
['hidden' => 1]
);
// > 'Présentation > Les adhérents' page
$this->insertPage(
$organization,
$this->createdPagesIndex['/presentation'],
'Les adhérents',
'/les-adherents',
self::TEMPLATE_MEMBERS
);
// > 'Présentation > Les membres du CA' page
$this->insertPage(
$organization,
$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(
$organization,
$this->createdPagesIndex['/presentation'],
'Les sociétés adhérentes',
'/societes-adherentes',
self::TEMPLATE_STRUCTURES
);
}
// > 'Présentation > Historique' page (hidden by default)
$this->insertPage(
$organization,
$this->createdPagesIndex['/presentation'],
'Historique',
'/historique',
'',
['hidden' => 1]
);
// ~ Contact shortcut will be created after the contact page
// > 'Actualités' page (hidden by default)
$this->insertPage(
$organization,
$rootUid,
'Actualités',
'/actualites',
self::TEMPLATE_NEWS,
['hidden' => 1]
);
// > 'Saison en cours' page
$this->insertPage(
$organization,
$rootUid,
'Saison en cours',
'/saison-en-cours'
);
// > 'Saison en cours > Les évènements' page
$this->insertPage(
$organization,
$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(
$organization,
$this->createdPagesIndex['/presentation'],
'Évènements des structures',
'/evenements-des-structures',
self::TEMPLATE_STRUCTURESEVENTS
);
}
// > 'Vie interne' page (restricted, hidden by default)
$this->insertPage(
$organization,
$rootUid,
'Vie interne',
'/vie-interne',
'',
[
'hidden' => 1,
'fe_group' => -2
]
);
// > 'Footer' page (not in the menu)
$this->insertPage(
$organization,
$rootUid,
'Footer',
'/footer',
'',
[
'dokType' => self::DOK_FOLDER,
'nav_hide' => 1
]
);
// > 'Footer > Contact' page
$this->insertPage(
$organization,
$this->createdPagesIndex['/footer'],
'Contact',
'/contact',
self::TEMPLATE_CONTACT
);
// > 'Footer > Plan du site' page
$this->insertPage(
$organization,
$this->createdPagesIndex['/footer'],
'Plan du site',
'/plan-du-site'
);
// > 'Footer > Mentions légales' page
$this->insertPage(
$organization,
$this->createdPagesIndex['/footer'],
'Mentions légales',
'/mentions-legales',
self::TEMPLATE_LEGAL
);
// > 'Présentation > Contact' shortcut
$this->insertPage(
$organization,
$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(
$organization,
$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
);
// Build and update the domain
if ($mode == self::MODE_PROD) {
$domain = $organization->getSubDomain() . '.opentalent.fr';
} elseif ($mode == self::MODE_DEV) {
$domain = $organization->getSubDomain() . '/';
} else {
throw new RuntimeException('Unknown value for $mode: ' . $mode);
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_domain');
$queryBuilder->insert('sys_domain')
->values([
'pid' => $rootUid,
'domainName' => $domain
])
->execute();
// 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
$this->writeConfigFile($organizationId, $rootUid, $domain, true);
// Create the user_upload directory and update the sys_filemounts table
$uploadRelPath = "/user_upload/" . $organizationId;
$uploadDir = $_ENV['TYPO3_PATH_APP'] . "/public/fileadmin" . $uploadRelPath;
if (file_exists($uploadDir)) {
throw new \RuntimeException("A directory or file " . $uploadDir . " already exists. Abort.");
}
$defaultUploadDir = $uploadDir . '/images';
$formsDir = $uploadDir . '/Forms';
$this->mkDir($uploadDir);
$this->mkDir($defaultUploadDir);
$this->mkDir($formsDir);
// Insert the filemounts points (sys_filemounts)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder->insert('sys_filemounts')
->values([
'title' => 'Documents',
'path' => $defaultUploadDir,
'base' => 1
])
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_filemounts');
$queryBuilder->insert('sys_filemounts')
->values([
'title' => 'Forms_' . $organizationId,
'path' => $formsDir,
'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:" . $defaultUploadDir . "\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 commiting 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;
}
/**
* Update the `sys_template`.`constants` field of the given
* organization's website with the data fetched from the opentalent DB.
*
* @param int $organizationId
* @return int
* @throws NoSuchWebsiteException
*/
public function updateSiteConstantsAction(int $organizationId): int
{
$rootUid = $this->findRootUidFor($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);
$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();
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
*
* @param int $organizationId
* @param bool $hard
* @param int|null $redirectTo
* @return int
* @throws NoSuchWebsiteException
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Doctrine\DBAL\DBALException
*/
public function deleteSiteAction(int $organizationId, bool $hard=false, ?int $redirectTo=null) {
$rootUid = $this->findRootUidFor($organizationId);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$isDeleted = $queryBuilder
->select('deleted')
->from('pages')
->where($queryBuilder->expr()->eq('uid', $rootUid))
->execute()
->fetchColumn(0) == 1;
$confirm_file = $_ENV['TYPO3_PATH_APP'] . "/DEL" . $organizationId;
if ($hard && !file_exists($confirm_file)) {
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');
}
$originUrl = $this->getSiteInfosAction($organizationId)->getBaseUrl();
$targetUrl = $this->getSiteInfosAction($redirectTo)->getBaseUrl();
}
// 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);
if ($hard) {
// there is no 'deleted' field in sys_domain
$this->delete('sys_domain', 'pid', $rootUid, $hard);
}
// remove filemounts
$this->delete('sys_filemounts',
'path',
"'/user_upload/" . $organizationId . "/images'",
$hard);
$this->delete('sys_filemounts',
'path',
"'/user_upload/" . $organizationId . "/Forms'",
$hard);
$adminBeUserUid = $this->findAdminBeUserUid($rootUid);
if ($adminBeUserUid !== null) {
$this->delete('be_users', 'uid', $adminBeUserUid, $hard);
}
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid);
if ($editorsGroupUid !== null) {
$this->delete('be_groups', 'uid', $editorsGroupUid, $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 . '/';
} else {
$uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/deleted_' . $organizationId . '/';
}
// If hard deletion, verify that upload dirs are empty
if ($hard && is_dir($uploadDir)) {
foreach (scandir($uploadDir) as $subdir) {
if ($subdir == '.' or $subdir == '..') {
continue;
}
$subdir = $uploadDir . $subdir;
if (!is_dir($subdir)) {
throw new \RuntimeException(
'The directory ' . $uploadDir . ' 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))) {
rmdir(dirname($configYamlFile));
}
if (is_dir($uploadDir . 'images')) {
rmdir($uploadDir . 'images');
}
if (is_dir($uploadDir . 'Forms')) {
rmdir($uploadDir . 'Forms');
}
if (is_dir($uploadDir)) {
rmdir($uploadDir);
}
} else {
$renamed = [];
foreach ($toRename as $initialPath => $newPath) {
rename($initialPath, $newPath);
$renamed[$initialPath] = $newPath;
}
}
// Add the redirection
if ($redirectTo) {
$this->addRedirection($originUrl, $targetUrl);
}
// 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) {
$rootUid = $this->findRootUidFor($organizationId);
// 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) {
$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 . "/images'"))
->execute();
$queryBuilder
->update('sys_filemounts')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('path', "'/user_upload/" . $organizationId . "/Forms'"))
->execute();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
$queryBuilder
->update('be_users')
->set('deleted', 0)
->where($queryBuilder->expr()->eq('db_mountpoints', $rootUid))
->execute();
$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();
}
$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();
}
// 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 . '/';
$toRename = [];
if (is_file($configYamlFile)) {
$toRename[$configYamlFile] = dirname($configYamlFile) . '/config.yaml';
}
if (is_dir($uploadDir)) {
$toRename[$uploadDir] = dirname($uploadDir) . '/' . $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->findRootUidFor($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
*/
private function scanSite(int $organizationId, int $rootUid) {
$warnings = [];
// fetch pages and root page
$pages = $this->otPageRepository->getAllSitePages($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->findConfigFor($rootUid);
// Check site's title
if (trim($rootPage['title']) != trim($organization->getName())) {
$warnings[] = "Site'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
*/
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 \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
* @throws NoSuchWebsiteException
*/
public function setSiteDomainAction(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);
}
$infos = $this->getSiteInfosAction($organizationId);
$originUrl = $infos->getBaseUrl();
$rootUid = $infos->getRootUid();
if (preg_replace('/https?:\/\//', '', $originUrl) == 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($originUrl, $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->findRootUidFor($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->getAllSitePages($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;
}
/**
* CLI 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;
}
}
/**
* Retrieve the Organization object from the repository and then,
* from the Opentalent API
*
* @param $organizationId
* @return Organization
*/
private function fetchOrganization($organizationId): Organization
{
$organizationRepository = GeneralUtility::makeInstance(ObjectManager::class)->get(OrganizationRepository::class);
try {
return $organizationRepository->findById($organizationId);
} catch (ApiRequestException $e) {
throw new \RuntimeException('Unable to fetch the organization with id: ' . $organizationId);
}
}
/**
* Try to find the root page uid of the organization's website and return it.
* Throw a Opentalent\OtAdmin\NoSuchWebsiteException exception if the website does not exist.
*
* @param $organizationId
* @return int
* @throws NoSuchWebsiteException
*/
private function findRootUidFor($organizationId): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$rootUid = $queryBuilder
->select('uid')
->from('pages')
->where('is_siteroot=1')
->andWhere($queryBuilder->expr()->eq('tx_opentalent_structure_id', $organizationId))
->execute()
->fetchColumn(0);
if ($rootUid > 0) {
return $rootUid;
}
throw new NoSuchWebsiteException("No website found for organization " . $organizationId);
}
/**
* Try to find the id of the organization owning this website.
*
* @param $rootUid
* @return int
* @throws NoSuchWebsiteException
*/
private function findOrganizationIdUidFor($rootUid): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()->removeAll();
$organizationId = $queryBuilder
->select('tx_opentalent_structure_id')
->from('pages')
->where('is_siteroot=1')
->andWhere($queryBuilder->expr()->eq('uid', $rootUid))
->execute()
->fetchColumn(0);
if ($organizationId > 0) {
return $organizationId;
}
throw new NoSuchWebsiteException("No organization found for website " . $rootUid);
}
/**
* 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(Organization $organization,
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',
'tx_opentalent_structure_id' => $organization->getId()
];
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 Organization $organization
* @return int
*/
private function insertRootPage(Organization $organization): int
{
return $this->insertPage(
$organization,
$this->getParentFolderUid(),
$organization->getName(),
'/',
self::TEMPLATE_HOME,
[
'is_siteroot' => 1,
'TSconfig' => 'TCAdefaults.pages.tx_opentalent_structure_id =' . $organization->getId(),
'tx_opentalent_template' => self::DEFAULT_THEME,
'tx_opentalent_template_preferences' => '{"themeColor":"' . self::DEFAULT_COLOR . '","displayCarousel":"1"}'
]
);
}
/**
* 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.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, a.id, s.product
FROM opentalent.Person p
INNER JOIN Access a ON p.id = a.person_id
INNER JOIN 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" .
" }\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);
}
}
/**
* Try to find the config file of the website in the less resource-consuming way
* and parse it.
*
* @param int $rootUid
* @return array Path of the configuration file and parsed configuration of the website
*/
protected function findConfigFileAndContentFor(int $rootUid): array
{
$configs_directory = $_ENV['TYPO3_PATH_APP'] . "/config/sites/";
$candidates = array_filter(
scandir($configs_directory),
function ($x) { return $x != '.' && $x != '..'; }
);
// try to filter by directory name
foreach ($candidates as $subdir) {
if (preg_match('/\.*_' . $rootUid . '$/', $subdir)) {
$filename = $configs_directory . $subdir . '/config.yaml';
$yamlConfig = Yaml::parseFile($filename);
if ($yamlConfig['rootPageId'] === $rootUid) {
return [$filename, $yamlConfig];
}
}
}
// it wasn't found the easy way, let's look to each file... :(
foreach ($candidates as $subdir) {
$filename = $configs_directory . $subdir . '/config.yaml';
$yamlConfig = Yaml::parseFile($filename);
if ($yamlConfig['rootPageId'] === $rootUid) {
return [$filename, $yamlConfig];
}
}
return [null, []];
}
/**
* Similar to findConfigFileAndContentFor(), but only returns the parsed configuration
* @param int $rootUid
* @return array Configuration of the website
*/
protected function findConfigFor(int $rootUid): array
{
$pathAndConfig = $this->findConfigFileAndContentFor($rootUid);
return $pathAndConfig[1];
}
/**
* Similar to findConfigFileAndContentFor(), but only returns the config file path
* @param int $rootUid
* @return string Path of the config file of the given website
*/
protected function findConfigFilePathFor(int $rootUid): string
{
$pathAndConfig = $this->findConfigFileAndContentFor($rootUid);
return $pathAndConfig[0];
}
/**
* Create or update the .../sites/.../config.yaml file of the given site
*
* @param int $organizationId
* @param int $rootUid
* @param string $domain
* @param bool $forceRecreate
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
*/
private function writeConfigFile(int $organizationId, int $rootUid, string $domain, bool $forceRecreate = false): string
{
$existing = $this->findConfigFileAndContentFor($rootUid);
$configFilename = $existing[0];
$config = $existing[1];
$subdomain = explode('.', $domain)[0];
if (!$configFilename) {
$configDir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/" . $subdomain . '_' . $organizationId;
$configFilename = $configDir . "/config.yaml";
$isNew = true;
if (file_exists($configFilename)) {
throw new \RuntimeException("A file named " . $configFilename . " already exists. Abort.");
}
} else {
$configDir = dirname($configFilename);
$config['base'] = 'https://' . $domain;
$isNew = false;
}
if ($isNew || $forceRecreate) {
$config = ['base' => 'https://' . $domain,
'baseVariants' => [
['base' => $subdomain . '/',
'condition' => 'applicationContext == "Development"']
],
'errorHandling' => [
['errorCode' => '404',
'errorHandler' => 'PHP',
'errorPhpClassFQCN' => 'Opentalent\OtTemplating\Page\ErrorHandler'],
['errorCode' => '403',
'errorHandler' => 'PHP',
'errorPhpClassFQCN' => 'Opentalent\OtTemplating\Page\ErrorHandler'],
],
'flux_content_types' => '',
'flux_page_templates' => '',
'languages' => [[
'title' => 'Fr',
'enabled' => True,
'base' => '/',
'typo3Language' => 'fr',
'locale' => 'fr_FR',
'iso-639-1' => 'fr',
'navigationTitle' => 'Fr',
'hreflang' => 'fr-FR',
'direction' => 'ltr',
'flag' => 'fr',
'languageId' => '0',
]],
'rootPageId' => $rootUid,
'routes' => []
];
}
$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:
$cacheSystem = $this->cacheManager->getCache('cache_core');
$cacheSystem->remove('site-configuration');
$cacheSystem->remove('pseudo-sites');
return $configFilename;
}
/**
* Create the BE user for the website, then return its uid
* The user shall be already created in the Opentalent DB
* This is a minimal creation, that shall be completed by the ot_connect ext
*
* @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' => '', // inherited from the base AdminGroup
'db_mountpoints' => $rootUid,
'options' => 2,
'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) {
$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 . "/%'");
$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
* @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();
}
$beGroups = $queryBuilder
->select('uid', 'subgroup')
->from('be_groups')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
// ->andWhere($queryBuilder->expr()->eq('deleted', 0))
->execute()
->fetchAll();
$editorsGroupUid = null;
foreach ($beGroups as $beGroup) {
// the editor group shall be a subgroup of one of the Editors group, and have this website mounted
if (in_array($beGroup['subgroup'], $editorsGroups)) {
$editorsGroupUid = $beGroup['uid'];
}
}
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
* @return int
* @throws NoSuchRecordException
*/
protected function findAdminBeUserUid(int $rootUid, bool $withRestrictions=true): int {
$adminGroups = [
$this->getBaseBeGroupUid(self::BEGROUP_ADMIN_STANDARD),
$this->getBaseBeGroupUid(self::BEGROUP_ADMIN_PREMIUM)
];
$editorsGroupUid = $this->findEditorsBeGroupUid($rootUid);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
if (!$withRestrictions) {
$queryBuilder->getRestrictions()->removeAll();
}
$beUsers = $queryBuilder
->select('uid', 'usergroup')
->from('be_users')
->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
// ->andWhere($queryBuilder->expr()->eq('deleted', 0))
->execute()
->fetchAll();
$adminUid = null;
foreach ($beUsers as $beUser) {
// the admin shall be both in the website editors group and in the Admin group
$isAdmin = false;
$isInThisWebsiteGroup = false;
foreach (explode(',', $beUser['usergroup']) as $group) {
if (in_array($group, $adminGroups)) {
$isAdmin = true;
}
if ($group == $editorsGroupUid) {
$isInThisWebsiteGroup = true;
}
}
if ($isAdmin && $isInThisWebsiteGroup) {
$adminUid = $beUser['uid'];
}
}
// Try to find if there is a be_user who still is in the v8.7 architecture
if ($adminUid == null) {
$organizationId = $this->findOrganizationIdUidFor($rootUid);
$extraData = $this->fetchOrganizationExtraData($organizationId);
$expectedUsername = $extraData['admin']['username'];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('be_users');
$adminUid = $queryBuilder
->select('uid')
->from('be_users')
->where($queryBuilder->expr()->eq('username', $queryBuilder->expr()->literal($expectedUsername)))
->andWhere($queryBuilder->expr()->eq('db_mountpoints', $rootUid))
->andWhere($queryBuilder->expr()->eq('deleted', 0))
->execute()
->fetchColumn(0);
}
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);
}
}