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