github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/typo3.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"text/template"
     9  
    10  	"github.com/Masterminds/semver/v3"
    11  	"github.com/ddev/ddev/pkg/archive"
    12  	"github.com/ddev/ddev/pkg/composer"
    13  	"github.com/ddev/ddev/pkg/fileutil"
    14  	"github.com/ddev/ddev/pkg/nodeps"
    15  	"github.com/ddev/ddev/pkg/output"
    16  	"github.com/ddev/ddev/pkg/util"
    17  )
    18  
    19  // createTypo3SettingsFile creates the app's LocalConfiguration.php and
    20  // AdditionalConfiguration.php, adding things like database host, name, and
    21  // password. Returns the fullpath to settings file and error
    22  func createTypo3SettingsFile(app *DdevApp) (string, error) {
    23  	if filepath.Dir(app.SiteDdevSettingsFile) == app.AppRoot {
    24  		// As long as the final settings folder is not defined, early return
    25  		return app.SiteDdevSettingsFile, nil
    26  	}
    27  
    28  	if !fileutil.FileExists(app.SiteSettingsPath) {
    29  		util.Warning("TYPO3 does not seem to have been set up yet, missing %s (%s)", filepath.Base(app.SiteSettingsPath), app.SiteSettingsPath)
    30  	}
    31  
    32  	// TYPO3 DDEV settings file will be AdditionalConfiguration.php (app.SiteDdevSettingsFile).
    33  	// Check if the file already exists.
    34  	if fileutil.FileExists(app.SiteDdevSettingsFile) {
    35  		// Check if the file is managed by ddev.
    36  		signatureFound, err := fileutil.FgrepStringInFile(app.SiteDdevSettingsFile, nodeps.DdevFileSignature)
    37  		if err != nil {
    38  			return "", err
    39  		}
    40  
    41  		// If the signature wasn't found, warn the user and return.
    42  		if !signatureFound {
    43  			util.Warning("%s already exists and is managed by the user.", filepath.Base(app.SiteDdevSettingsFile))
    44  			return app.SiteDdevSettingsFile, nil
    45  		}
    46  	}
    47  
    48  	output.UserOut.Printf("Generating %s file for database connection.", filepath.Base(app.SiteDdevSettingsFile))
    49  	if err := writeTypo3SettingsFile(app); err != nil {
    50  		return "", fmt.Errorf("failed to write TYPO3 AdditionalConfiguration.php file: %v", err.Error())
    51  	}
    52  
    53  	return app.SiteDdevSettingsFile, nil
    54  }
    55  
    56  // writeTypo3SettingsFile produces AdditionalConfiguration.php file
    57  // It's assumed that the LocalConfiguration.php already exists, and we're
    58  // overriding the db config values in it. The typo3conf/ directory will
    59  // be created if it does not yet exist.
    60  func writeTypo3SettingsFile(app *DdevApp) error {
    61  	filePath := app.SiteDdevSettingsFile
    62  
    63  	// Ensure target directory is writable.
    64  	dir := filepath.Dir(filePath)
    65  	var perms os.FileMode = 0755
    66  	if err := os.Chmod(dir, perms); err != nil {
    67  		if !os.IsNotExist(err) {
    68  			// The directory exists, but chmod failed.
    69  			return err
    70  		}
    71  
    72  		// The directory doesn't exist, create it with the appropriate permissions.
    73  		if err := os.MkdirAll(dir, perms); err != nil {
    74  			return err
    75  		}
    76  	}
    77  	dbDriver := "mysqli" // mysqli is the driver used in default LocalConfiguration.php
    78  	if app.Database.Type == nodeps.Postgres {
    79  		dbDriver = "pdo_pgsql"
    80  	}
    81  	settings := map[string]interface{}{"DBHostname": "db", "DBDriver": dbDriver, "DBPort": GetExposedPort(app, "db")}
    82  
    83  	// Ensure target directory exists and is writable
    84  	if err := os.Chmod(dir, 0755); os.IsNotExist(err) {
    85  		if err = os.MkdirAll(dir, 0755); err != nil {
    86  			return err
    87  		}
    88  	} else if err != nil {
    89  		return err
    90  	}
    91  
    92  	f, err := os.Create(filePath)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	t, err := template.New("AdditionalConfiguration.php").ParseFS(bundledAssets, "typo3/AdditionalConfiguration.php")
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	if err = t.Execute(f, settings); err != nil {
   103  		return err
   104  	}
   105  	if err != nil {
   106  		return err
   107  	}
   108  	return nil
   109  }
   110  
   111  // getTypo3UploadDirs will return the default paths.
   112  func getTypo3UploadDirs(_ *DdevApp) []string {
   113  	return []string{"fileadmin"}
   114  }
   115  
   116  // Typo3Hooks adds a TYPO3-specific hooks example for post-import-db
   117  const Typo3Hooks = `#  post-start:
   118  #    - exec: composer install -d /var/www/html
   119  `
   120  
   121  // getTypo3Hooks for appending as byte array
   122  func getTypo3Hooks() []byte {
   123  	// We don't have anything new to add yet.
   124  	return []byte(Typo3Hooks)
   125  }
   126  
   127  // setTypo3SiteSettingsPaths sets the paths to settings files for templating
   128  func setTypo3SiteSettingsPaths(app *DdevApp) {
   129  	var settingsFilePath, localSettingsFilePath string
   130  
   131  	if isTypo3v12OrHigher(app) {
   132  		settingsFileBasePath := filepath.Join(app.AppRoot, app.ComposerRoot)
   133  		settingsFilePath = filepath.Join(settingsFileBasePath, "config", "system", "settings.php")
   134  		localSettingsFilePath = filepath.Join(settingsFileBasePath, "config", "system", "additional.php")
   135  	} else if isTypo3App(app) {
   136  		settingsFileBasePath := filepath.Join(app.AppRoot, app.Docroot)
   137  		settingsFilePath = filepath.Join(settingsFileBasePath, "typo3conf", "LocalConfiguration.php")
   138  		localSettingsFilePath = filepath.Join(settingsFileBasePath, "typo3conf", "AdditionalConfiguration.php")
   139  	} else {
   140  		// As long as TYPO3 is not installed, the file paths are set to the
   141  		// AppRoot to avoid the creation of the .gitignore in the wrong location.
   142  		settingsFilePath = filepath.Join(app.AppRoot, "LocalConfiguration.php")
   143  		localSettingsFilePath = filepath.Join(app.AppRoot, "AdditionalConfiguration.php")
   144  	}
   145  
   146  	// Update file paths
   147  	app.SiteSettingsPath = settingsFilePath
   148  	app.SiteDdevSettingsFile = localSettingsFilePath
   149  }
   150  
   151  // isTypoApp returns true if the app is of type typo3
   152  func isTypo3App(app *DdevApp) bool {
   153  	typo3Folder := filepath.Join(app.AppRoot, app.Docroot, "typo3")
   154  
   155  	// Check if the folder exists, fails if a symlink target does not exist.
   156  	if _, err := os.Stat(typo3Folder); !os.IsNotExist(err) {
   157  		return true
   158  	}
   159  
   160  	// Check if a symlink exists, succeeds even if the target does not exist.
   161  	if _, err := os.Lstat(typo3Folder); !os.IsNotExist(err) {
   162  		return true
   163  	}
   164  
   165  	return false
   166  }
   167  
   168  // typo3ImportFilesAction defines the TYPO3 workflow for importing project files.
   169  // The TYPO3 import-files workflow is currently identical to the Drupal workflow.
   170  func typo3ImportFilesAction(app *DdevApp, uploadDir, importPath, extPath string) error {
   171  	destPath := app.calculateHostUploadDirFullPath(uploadDir)
   172  
   173  	// parent of destination dir should exist
   174  	if !fileutil.FileExists(filepath.Dir(destPath)) {
   175  		return fmt.Errorf("unable to import to %s: parent directory does not exist", destPath)
   176  	}
   177  
   178  	// parent of destination dir should be writable.
   179  	if err := os.Chmod(filepath.Dir(destPath), 0755); err != nil {
   180  		return err
   181  	}
   182  
   183  	// If the destination path exists, remove it as was warned
   184  	if fileutil.FileExists(destPath) {
   185  		if err := os.RemoveAll(destPath); err != nil {
   186  			return fmt.Errorf("failed to cleanup %s before import: %v", destPath, err)
   187  		}
   188  	}
   189  
   190  	if isTar(importPath) {
   191  		if err := archive.Untar(importPath, destPath, extPath); err != nil {
   192  			return fmt.Errorf("failed to extract provided archive: %v", err)
   193  		}
   194  
   195  		return nil
   196  	}
   197  
   198  	if isZip(importPath) {
   199  		if err := archive.Unzip(importPath, destPath, extPath); err != nil {
   200  			return fmt.Errorf("failed to extract provided archive: %v", err)
   201  		}
   202  
   203  		return nil
   204  	}
   205  
   206  	//nolint: revive
   207  	if err := fileutil.CopyDir(importPath, destPath); err != nil {
   208  		return err
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  // isTypo3v12OrHigher returns true if the TYPO3 version is 12 or higher.
   215  func isTypo3v12OrHigher(app *DdevApp) bool {
   216  	composerManifest, _ := composer.NewManifest(filepath.Join(app.AppRoot, app.ComposerRoot, "composer.json"))
   217  	vendorDir := "vendor"
   218  	if composerManifest != nil {
   219  		vendorDir = composerManifest.GetVendorDir()
   220  	}
   221  
   222  	versionFilePath := filepath.Join(app.AppRoot, app.ComposerRoot, vendorDir, "typo3", "cms-core", "Classes", "Information", "Typo3Version.php")
   223  	versionFile, err := fileutil.ReadFileIntoString(versionFilePath)
   224  
   225  	// Typo3Version class exists since v10.3.0. Before v11.5.0 the core was always
   226  	// installed into the folder public/typo3 so we can early return if the file
   227  	// is not found in the vendor folder.
   228  	if err != nil {
   229  		util.Debug("TYPO3 version class not found in '%s' for project %s, installed version is assumed to be older than 11.5.0: %v", versionFilePath, app.Name, err)
   230  		return false
   231  	}
   232  
   233  	// We may have a TYPO3 version 11 or higher and therefor have to parse the
   234  	// class file to properly detect the version.
   235  	re := regexp.MustCompile(`const\s+VERSION\s*=\s*'([^']+)`)
   236  
   237  	matches := re.FindStringSubmatch(versionFile)
   238  
   239  	if len(matches) < 2 {
   240  		util.Warning("Unexpected Typo3Version found for project %s in %v.", app.Name, versionFile)
   241  		return false
   242  	}
   243  
   244  	version, err := semver.NewVersion(matches[1])
   245  	if err != nil {
   246  		// This case never should happen
   247  		util.Warning("Unexpected error while parsing TYPO3 version ('%s') for project %s: %v.", matches[1], app.Name, err)
   248  		return false
   249  	}
   250  
   251  	util.Debug("Found TYPO3 version %v for project %s.", version.Original(), app.Name)
   252  
   253  	return version.Major() >= 12
   254  }