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 }