Skip to content

Instantly share code, notes, and snippets.

@spoonerWeb
Last active June 23, 2021 09:51
Show Gist options
  • Select an option

  • Save spoonerWeb/0313b75be9aebd16f6bd91bdcda6b401 to your computer and use it in GitHub Desktop.

Select an option

Save spoonerWeb/0313b75be9aebd16f6bd91bdcda6b401 to your computer and use it in GitHub Desktop.

Revisions

  1. spoonerWeb revised this gist Jun 23, 2021. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -13,6 +13,12 @@
    * Add and adapt the attached command
    * Make "Contao fileadmin" (tl_files) accessible into fileadmin (`fileadmin/tl_files/`)

    ## Extensions

    I used these extensions:

    * `blog` for migrating articles (news) into it

    ## Additional SQL commands

    In my case I needed to add or change some things via SQL query. For this I created several SQL files in folder `migrations` for adjusting the records after migration.
  2. spoonerWeb created this gist Jun 23, 2021.
    534 changes: 534 additions & 0 deletions Classes_Command_MigrationCommand.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,534 @@
    <?php
    declare(strict_types = 1);
    namespace Vendor\Extension\Command;

    use Symfony\Component\Console\Command\Command;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;
    use TYPO3\CMS\Core\Core\Environment;
    use TYPO3\CMS\Core\Database\Connection;
    use TYPO3\CMS\Core\Database\ConnectionPool;
    use TYPO3\CMS\Core\Resource\ResourceFactory;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Frontend\Page\PageRepository;

    class Migration extends Command
    {
    protected Connection $connectionContao;

    protected Connection $connectionTypo3;

    protected array $linkCaches = [];

    const IMAGE_ORIENTATION_MAPPING = [
    '' => 0,
    'right' => 25,
    'left' => 26,
    'below' => 8,
    'above' => 0,
    ];

    protected function configure()
    {
    parent::configure(); // TODO: Change the autogenerated stub
    }

    protected function initialize(InputInterface $input, OutputInterface $output)
    {
    parent::initialize($input, $output);
    $this->connectionTypo3 = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
    $this->connectionContao = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionByName('Contao');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
    $output->writeln('Starting migration...');

    $output->writeln('Checking for migration db...');
    if ($this->connectionContao) {
    $output->writeln('Connection to migration db successful!');
    } else {
    $output->writeln('Connection to migration db not possible!');
    return self::FAILURE;
    }

    $pagesMigrated = $this->migratePages();
    $output->writeln('Migrated ' . $pagesMigrated . ' pages.');
    $contentMigrated = $this->migrateContent();
    foreach ($contentMigrated as $type => $count) {
    $output->writeln('Migrated ' . $count . ' elements of type "' . $type . '".');
    }
    $backendUserMigrated = $this->migrateBackendUser();
    $output->writeln('Migrated ' . $backendUserMigrated . ' backend users.');
    $frontendUserMigrated = $this->migrateFrontendUser();
    $output->writeln('Migrated ' . $frontendUserMigrated['groups'] . ' frontend groups.');
    $output->writeln('Migrated ' . $frontendUserMigrated['users'] . ' frontend users.');

    $news = $this->migrateNews();
    $output->writeln('Migrated ' . $news . ' news articles.');

    $this->finalizeMigration();

    return self::SUCCESS;
    }

    private function migrateFrontendUser(): array
    {
    $this->connectionTypo3->truncate('fe_users');
    $usersToMigrate = $this->connectionContao->select(['*'], 'tl_member')->fetchAll();

    $this->connectionTypo3->insert(
    'pages',
    [
    'pid' => 1,
    'doktype' => PageRepository::DOKTYPE_SYSFOLDER,
    'title' => 'Benutzer'
    ]
    );
    $storagePid = $this->connectionTypo3->lastInsertId('pages');

    $this->connectionTypo3->truncate('fe_groups');
    $groups = $this->connectionContao->select(['*'], 'tl_member_group')->fetchAll();

    foreach ($groups as $group) {
    $newGroup = [
    'pid' => $storagePid,
    'uid' => $group['id'],
    'tstamp' => $group['tstamp'],
    'crdate' => $group['tstamp'],
    'title' => $group['name'],
    'felogin_redirectPid' => $group['jumpTo'],
    ];

    $this->connectionTypo3->insert('fe_groups', $newGroup);
    }

    foreach ($usersToMigrate as $user) {
    $zip = '';
    if (preg_match('/([\d]{4,5})/', $user['postal'], $matches)) {
    $zip = $matches[0];
    }
    $newUser = [
    'uid' => $user['id'],
    'pid' => $storagePid,
    'tstamp' => $user['tstamp'],
    'first_name' => $user['firstname'],
    'last_name' => $user['lastname'],
    'company' => $user['company'],
    'address' => $user['street'],
    'zip' => $zip,
    'city' => $user['city'],
    'telephone' => $user['phone'],
    'email' => $user['email'],
    'www' => $user['website'],
    'username' => $user['username'],
    'password' => $user['password'],
    'crdate' => $user['dateAdded'],
    'lastlogin' => $user['lastLogin'],
    'title' => $user['jobtitle'],
    'date_of_birth' => $user['dateOfBirth'] ?: 0,
    'favourite_club' => $user['campaign'],
    'referer' => $user['business_connection'],
    'allow_email' => $user['allowEmail'] === 'email_member' ? 1 : 0,
    'gender' => $user['gender'] === 'female' ? 1 : 0,
    'usergroup' => implode(',', unserialize($user['groups']))
    ];
    if ($user['avatar']) {
    $fileIdentifier = '/' . $user['avatar'];
    if (file_exists(Environment::getPublicPath() . '/fileadmin' . $fileIdentifier)) {
    $image = ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier(1, $fileIdentifier);

    $this->connectionTypo3->insert(
    'sys_file_reference',
    [
    'uid_local' => $image->getUid(),
    'uid_foreign' => $user['id'],
    'tstamp' => time(),
    'crdate' => time(),
    'tablenames' => 'fe_users',
    'fieldname' => 'image',
    'title' => $user['firstname'] . ' ' . $user['lastname'],
    ]
    );

    $newUser['image'] = 1;
    }
    }

    $this->connectionTypo3->insert('fe_users', $newUser);
    }

    return [
    'users' => count($usersToMigrate),
    'groups' => count($groups)
    ];
    }

    private function migrateBackendUser(): int
    {
    $this->connectionTypo3->truncate('be_users');
    $usersToMigrate = $this->connectionContao->select(['*'], 'tl_user')->fetchAll();

    foreach ($usersToMigrate as $user) {
    $newUser = [
    'uid' => $user['id'],
    'tstamp' => $user['tstamp'],
    'username' => $user['username'],
    'realName' => $user['name'],
    'email' => $user['email'],
    'password' => $user['password'],
    'admin' => $user['admin'] ? 1 : 0,
    'lastlogin' => 0,
    ];

    $this->connectionTypo3->insert('be_users', $newUser);
    }

    return count($usersToMigrate);
    }

    private function migratePages(): int
    {
    $this->connectionTypo3->truncate('pages');
    $pagesToMigrate = $this->connectionContao
    ->select(
    ['*'],
    'tl_page'
    )
    ->fetchAll();

    foreach ($pagesToMigrate as $pageToMigrate) {
    $noIndex = str_contains($pageToMigrate['robots'], 'noindex');
    $noFollow = str_contains($pageToMigrate['robots'], 'nofollow');
    $newPage = [
    'uid' => $pageToMigrate['id'],
    'pid' => $pageToMigrate['pid'] ?: 0,
    'sorting' => $pageToMigrate['sorting'],
    'tstamp' => $pageToMigrate['tstamp'],
    'crdate' => $pageToMigrate['tstamp'],
    'title' => $pageToMigrate['title'],
    'slug' => '/' . $pageToMigrate['alias'],
    'is_siteroot' => $pageToMigrate['type'] === 'root' ? 1 : 0,
    'fe_group' => $pageToMigrate['protected'] ? -2 : 0,
    'no_search' => $pageToMigrate['noSearch'] ?: 0,
    'doktype' => 1,
    'nav_hide' => $pageToMigrate['hide'] ? 1 : 0,
    'hidden' => $pageToMigrate['published'] ? 0 : 1,
    'no_index' => $noIndex === true ? 1 : 0,
    'no_follow' => $noFollow === true ? 1 : 0,
    ];

    if ($pageToMigrate['type'] === 'forward') {
    $newPage['doktype'] = PageRepository::DOKTYPE_SHORTCUT;
    $newPage['shortcut'] = $pageToMigrate['jumpTo'];
    }

    if ($pageToMigrate['type'] === 'root') {
    $newPage['slug'] = '/';
    $this->connectionTypo3->truncate('sys_template');
    $this->connectionTypo3
    ->insert(
    'sys_template',
    [
    'title' => $pageToMigrate['title'],
    'pid' => $pageToMigrate['id'],
    'root' => 1,
    'clear' => 3
    ]
    );
    }

    if (!GeneralUtility::inList('root,regular,forward', $pageToMigrate['type'])) {
    $newPage['nav_hide'] = 1;
    }

    $this->connectionTypo3->insert('pages', $newPage);
    }

    return count($pagesToMigrate);
    }

    private function migrateContent(): array
    {
    $containerConfiguration = [
    '2cols3366' => [
    'children' => [
    201,
    202
    ]
    ],
    '3cols' => [
    'children' => [
    301,
    302,
    303
    ]
    ],
    ];
    $migrated = [];
    $this->connectionTypo3->truncate('tt_content');
    $this->connectionTypo3->truncate('sys_file_reference');

    $articles = $this->connectionContao->select(['*'], 'tl_article')->fetchAll();

    foreach ($articles as $article) {
    $contentElements = $this->connectionContao->select(
    ['*'],
    'tl_content',
    ['pid' => $article['id']],
    [],
    ['sorting' => 'ASC']
    )->fetchAll();

    $column2With3366 = false;
    $column3 = false;
    $containerUid = 0;

    foreach ($contentElements as $key => $contentElement) {
    $newContentElement = [];
    if ($key === 0 && str_contains($contentElement['cssID'], 'grid_1 alpha')) {
    $column2With3366 = true;
    $containerUid = $contentElement['id'] + 1000;
    $newContainerElement = [
    'uid' => $containerUid,
    'pid' => $article['pid'],
    'tstamp' => $contentElement['tstamp'],
    'crdate' => $contentElement['tstamp'],
    'CType' => '2cols-33-66',
    'colPos' => 0,
    ];
    $this->connectionTypo3->insert(
    'tt_content',
    $newContainerElement
    );
    }
    if ($key % 3 === 0 && str_contains($contentElement['cssID'], 'grid_1 omega')) {
    $column3 = true;
    $containerUid = $contentElement['id'] + 1000;
    $newContainerElement = [
    'uid' => $containerUid,
    'pid' => $article['pid'],
    'tstamp' => $contentElement['tstamp'],
    'crdate' => $contentElement['tstamp'],
    'sorting' => $contentElement['sorting'],
    'CType' => '3cols',
    'colPos' => 0,
    ];
    $this->connectionTypo3->insert(
    'tt_content',
    $newContainerElement
    );
    }
    switch ($contentElement['type']) {
    case 'text':
    case 'image':
    case 'headline':
    $header = unserialize($contentElement['headline']);
    $bodytext = $this->cleanText($contentElement['text']);
    $bodytext = $this->migrateLinks($bodytext);
    $colPos = 0;
    if ($column2With3366) {
    $colPos = $containerConfiguration['2cols3366']['children'][$key];
    }
    if ($column3) {
    $column = $key % 3;
    $colPos = $containerConfiguration['3cols']['children'][$column];
    }
    $newContentElement = [
    'uid' => $contentElement['id'],
    'pid' => $article['pid'],
    'sorting' => $contentElement['sorting'],
    'tstamp' => $contentElement['tstamp'],
    'crdate' => $contentElement['tstamp'],
    'hidden' => $contentElement['invisible'] ? 1 : 0,
    'CType' => 'text',
    'colPos' => $colPos,
    'bodytext' => $bodytext,
    'header' => $header['value'],
    'header_layout' => (int)substr($header['unit'], 1, 1),
    'tx_container_parent' => $containerUid
    ];

    if ($contentElement['singleSRC'] && !str_contains($contentElement['cssID'], 'grid_1 omega')) {
    $newContentElement['CType'] = 'textmedia';
    $fileIdentifier = '/' . $contentElement['singleSRC'];
    if (file_exists(Environment::getPublicPath() . '/fileadmin' . $fileIdentifier)) {
    $image = ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier(1, $fileIdentifier);

    $this->connectionTypo3->insert(
    'sys_file_reference',
    [
    'uid_local' => $image->getUid(),
    'uid_foreign' => $contentElement['id'],
    'tstamp' => time(),
    'crdate' => time(),
    'tablenames' => 'tt_content',
    'fieldname' => 'assets',
    'title' => $contentElement['title'],
    'alternative' => $contentElement['alt'],
    'description' => $contentElement['caption'],
    ]
    );

    $newContentElement['assets'] = 1;
    $newContentElement['imagecols'] = 1;
    $newContentElement['imageorient'] = self::IMAGE_ORIENTATION_MAPPING[$contentElement['floating']];
    }
    }
    }
    if ($newContentElement) {
    $migrated[$newContentElement['CType']]++;
    $this->connectionTypo3->insert(
    'tt_content',
    $newContentElement
    );
    }
    }

    }

    return $migrated;
    }

    public function migrateNews(): int
    {
    $articles = $this->connectionContao->select(['*'], 'tl_news')->fetchAll();

    foreach ($articles as $article) {
    $this->connectionTypo3->insert(
    'pages',
    [
    'title' => $article['headline'],
    'pid' => $article['pid'],
    'slug' => $article['alias'] ?? '',
    'publish_date' => $article['date'],
    'crdate' => $article['time'],
    'tstamp' => $article['time'],
    'doktype' => \T3G\AgencyPack\Blog\Constants::DOKTYPE_BLOG_POST
    ]
    );
    $pageUid = $this->connectionTypo3->lastInsertId('pages');

    $this->connectionTypo3->insert(
    'tt_content',
    [
    'pid' => $pageUid,
    'crdate' => $article['time'],
    'tstamp' => $article['time'],
    'CType' => 'text',
    'colPos' => 0,
    'bodytext' => $this->cleanText($article['teaser']),
    'sorting' => 1,
    ]
    );

    $newContentElement = [
    'pid' => $pageUid,
    'crdate' => $article['time'],
    'tstamp' => $article['time'],
    'CType' => 'textmedia',
    'colPos' => 0,
    'bodytext' => $this->cleanText($article['text']),
    'sorting' => 2,
    ];

    $this->connectionTypo3->insert(
    'tt_content',
    $newContentElement
    );
    $newContentElement['uid'] = $this->connectionTypo3->lastInsertId('tt_content');

    if ($article['singleSRC']) {
    $fileIdentifier = '/' . $article['singleSRC'];
    if (file_exists(Environment::getPublicPath() . '/fileadmin' . $fileIdentifier)) {
    $image = ResourceFactory::getInstance()->getFileObjectByStorageAndIdentifier(1, $fileIdentifier);

    $this->connectionTypo3->insert(
    'sys_file_reference',
    [
    'uid_local' => $image->getUid(),
    'uid_foreign' => $newContentElement['uid'],
    'tstamp' => time(),
    'crdate' => time(),
    'tablenames' => 'tt_content',
    'fieldname' => 'assets',
    'description' => $article['caption'],
    ]
    );

    $updateContentElement['assets'] = 1;
    $updateContentElement['imagecols'] = 1;
    $updateContentElement['imageorient'] = self::IMAGE_ORIENTATION_MAPPING[$article['floating']];

    $this->connectionTypo3->update(
    'tt_content',
    $updateContentElement,
    ['uid' => $newContentElement['uid']]
    );
    }
    }
    }

    return count($articles);
    }

    private function cleanText(?string $text): string
    {
    if (is_null($text)) {
    return '';
    }
    $remove = [
    '[nbsp]',
    '<p>[nbsp]</p>',
    ];
    $text = str_replace($remove, '', $text);
    $text = preg_replace('/style="[^\"]*"/', '', $text);
    $text = trim($text);

    return $text;
    }

    private function migrateLinks(string $text): string
    {
    $linksFound = preg_match_all('/<a\s+(?:[^>]*?\s+)?href=(["\'])(.*?)\1/', $text, $links);
    if ($linksFound) {
    foreach ($links[2] as $originalLink) {
    if (!isset($this->linkCaches[$originalLink])) {
    $newLink = $originalLink;
    preg_match('/\{\{link_url::(\d+)\}\}/', $originalLink, $directTargetPage);
    if ($directTargetPage) {
    $newLink = 't3://page?uid=' . $directTargetPage[1];
    } else {
    $path = parse_url($originalLink, PHP_URL_PATH);
    $slug = str_replace('.html', '', $path);
    $slug = $slug[0] !== '/' ? '/' . $slug : $slug;

    $targetPage = $this->connectionTypo3->select(['uid'], 'pages', ['slug' => $slug])->fetch();
    if ($targetPage) {
    $newLink = 't3://page?uid=' . $targetPage['uid'];
    }
    }
    $this->linkCaches[$originalLink] = $newLink;
    } else {
    $newLink = $this->linkCaches[$originalLink];
    }

    $text = str_replace($originalLink, $newLink, $text);
    }
    }

    return $text;
    }

    private function finalizeMigration(): void
    {
    foreach (glob(Environment::getProjectPath() . '/migrations/*.sql') as $file) {
    if (file_exists($file)) {
    $stmt = $this->connectionTypo3->prepare(file_get_contents($file));
    $stmt->execute();
    }
    }
    }
    }
    13 changes: 13 additions & 0 deletions Configuration_Services.yaml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    services:
    _defaults:
    autowire: true
    autoconfigure: true
    public: false

    Vendor\Extension\:
    resource: '../Classes/*'

    Vendor\Extension\Command\Migration:
    tags:
    - name: 'console.command'
    command: 'vendor:migrate'
    22 changes: 22 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    # Migration of a Contao page to TYPO3

    ## Prerequisites

    * DB dump of Contao system
    * Setup basic TYPO3 v10
    * Second database for Contao dump

    ## Usage

    * Use a sitepackage extension
    * Configure Contao DB as second database in LocalConfiguration.php
    * Add and adapt the attached command
    * Make "Contao fileadmin" (tl_files) accessible into fileadmin (`fileadmin/tl_files/`)

    ## Additional SQL commands

    In my case I needed to add or change some things via SQL query. For this I created several SQL files in folder `migrations` for adjusting the records after migration.

    ## Questions and improvements?

    Just ping me via comment.