github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/wordpress.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "strings" 8 "text/template" 9 10 "github.com/ddev/ddev/pkg/archive" 11 "github.com/ddev/ddev/pkg/fileutil" 12 "github.com/ddev/ddev/pkg/nodeps" 13 "github.com/ddev/ddev/pkg/util" 14 ) 15 16 // WordpressConfig encapsulates all the configurations for a WordPress site. 17 type WordpressConfig struct { 18 WPGeneric bool 19 DeployName string 20 DeployURL string 21 DatabaseName string 22 DatabaseUsername string 23 DatabasePassword string 24 DatabaseHost string 25 AuthKey string 26 SecureAuthKey string 27 LoggedInKey string 28 NonceKey string 29 AuthSalt string 30 SecureAuthSalt string 31 LoggedInSalt string 32 NonceSalt string 33 Docroot string 34 TablePrefix string 35 Signature string 36 SiteSettings string 37 SiteSettingsDdev string 38 AbsPath string 39 DbCharset string 40 DbCollate string 41 } 42 43 // NewWordpressConfig produces a WordpressConfig object with defaults. 44 func NewWordpressConfig(app *DdevApp, absPath string) *WordpressConfig { 45 return &WordpressConfig{ 46 WPGeneric: false, 47 DatabaseName: "db", 48 DatabaseUsername: "db", 49 DatabasePassword: "db", 50 DatabaseHost: "ddev-" + app.Name + "-db", 51 DeployURL: app.GetPrimaryURL(), 52 Docroot: "/var/www/html/docroot", 53 TablePrefix: "wp_", 54 AuthKey: util.RandString(64), 55 AuthSalt: util.RandString(64), 56 LoggedInKey: util.RandString(64), 57 LoggedInSalt: util.RandString(64), 58 NonceKey: util.RandString(64), 59 NonceSalt: util.RandString(64), 60 SecureAuthKey: util.RandString(64), 61 SecureAuthSalt: util.RandString(64), 62 Signature: nodeps.DdevFileSignature, 63 SiteSettings: "wp-config.php", 64 SiteSettingsDdev: "wp-config-ddev.php", 65 AbsPath: absPath, 66 DbCharset: "utf8", 67 DbCollate: "", 68 } 69 } 70 71 // wordPressHooks adds a wp-specific hooks example for post-start 72 const wordPressHooks = `# Un-comment to emit the WP CLI version after ddev start. 73 # post-start: 74 # - exec: wp cli version 75 ` 76 77 // getWordpressHooks for appending as byte array 78 func getWordpressHooks() []byte { 79 return []byte(wordPressHooks) 80 } 81 82 // getWordpressUploadDirs will return the default paths. 83 func getWordpressUploadDirs(_ *DdevApp) []string { 84 return []string{"wp-content/uploads"} 85 } 86 87 const wordpressConfigInstructions = ` 88 An existing user-managed wp-config.php file has been detected! 89 Project DDEV settings have been written to: 90 91 %s 92 93 Please comment out any database connection settings in your wp-config.php and 94 add the following snippet to your wp-config.php, near the bottom of the file 95 and before the include of wp-settings.php: 96 97 // Include for ddev-managed settings in wp-config-ddev.php. 98 $ddev_settings = dirname(__FILE__) . '/wp-config-ddev.php'; 99 if (is_readable($ddev_settings) && !defined('DB_USER')) { 100 require_once($ddev_settings); 101 } 102 103 If you don't care about those settings, or config is managed in a .env 104 file, etc, then you can eliminate this message by putting a line that says 105 // wp-config-ddev.php not needed 106 in your wp-config.php 107 ` 108 109 // createWordpressSettingsFile creates a Wordpress settings file from a 110 // template. Returns full path to location of file + err 111 func createWordpressSettingsFile(app *DdevApp) (string, error) { 112 absPath, err := wordpressGetRelativeAbsPath(app) 113 if err != nil { 114 if strings.Contains(err.Error(), "multiple") { 115 util.Warning("Unable to determine ABSPATH: %v", err) 116 } 117 } 118 119 config := NewWordpressConfig(app, absPath) 120 121 // Write DDEV settings file 122 if err := writeWordpressDdevSettingsFile(config, app.SiteDdevSettingsFile); err != nil { 123 return "", err 124 } 125 126 // Check if an existing WordPress settings file exists 127 if fileutil.FileExists(app.SiteSettingsPath) { 128 // Check if existing WordPress settings file is ddev-managed 129 sigExists, err := fileutil.FgrepStringInFile(app.SiteSettingsPath, nodeps.DdevFileSignature) 130 if err != nil { 131 return "", err 132 } 133 134 if sigExists { 135 // Settings file is ddev-managed, overwriting is safe 136 if err := writeWordpressSettingsFile(config, app.SiteSettingsPath); err != nil { 137 return "", err 138 } 139 } else { 140 // Settings file exists and is not ddev-managed, alert the user to the location 141 // of the generated DDEV settings file 142 includeExists, err := fileutil.FgrepStringInFile(app.SiteSettingsPath, "wp-config-ddev.php") 143 if err != nil { 144 util.Warning("Unable to check that the DDEV settings file has been included: %v", err) 145 } 146 147 if includeExists { 148 util.Success("Include of %s found in %s", app.SiteDdevSettingsFile, app.SiteSettingsPath) 149 } else { 150 util.Warning(wordpressConfigInstructions, app.SiteDdevSettingsFile) 151 } 152 } 153 } else { 154 // If settings file does not exist, write basic settings file including it 155 if err := writeWordpressSettingsFile(config, app.SiteSettingsPath); err != nil { 156 return "", err 157 } 158 } 159 160 return app.SiteDdevSettingsFile, nil 161 } 162 163 // writeWordpressSettingsFile dynamically produces valid wp-config.php file by combining a configuration 164 // object with a data-driven template. 165 func writeWordpressSettingsFile(wordpressConfig *WordpressConfig, filePath string) error { 166 t, err := template.New("wp-config.php").ParseFS(bundledAssets, "wordpress/wp-config.php") 167 if err != nil { 168 return err 169 } 170 171 // Ensure target directory exists and is writable 172 dir := filepath.Dir(filePath) 173 if err = os.Chmod(dir, 0755); os.IsNotExist(err) { 174 if err = os.MkdirAll(dir, 0755); err != nil { 175 return err 176 } 177 } else if err != nil { 178 return err 179 } 180 181 file, err := os.Create(filePath) 182 if err != nil { 183 return err 184 } 185 defer util.CheckClose(file) 186 187 //nolint: revive 188 if err = t.Execute(file, wordpressConfig); err != nil { 189 return err 190 } 191 192 return nil 193 } 194 195 // writeWordpressDdevSettingsFile unconditionally creates the file that contains ddev-specific settings. 196 func writeWordpressDdevSettingsFile(config *WordpressConfig, filePath string) error { 197 if fileutil.FileExists(filePath) { 198 // Check if the file is managed by ddev. 199 signatureFound, err := fileutil.FgrepStringInFile(filePath, nodeps.DdevFileSignature) 200 if err != nil { 201 return err 202 } 203 204 // If the signature wasn't found, warn the user and return. 205 if !signatureFound { 206 util.Warning("%s already exists and is managed by the user.", filepath.Base(filePath)) 207 return nil 208 } 209 } 210 211 t, err := template.New("wp-config-ddev.php").ParseFS(bundledAssets, "wordpress/wp-config-ddev.php") 212 if err != nil { 213 return err 214 } 215 216 // Ensure target directory exists and is writable 217 dir := filepath.Dir(filePath) 218 if err = os.Chmod(dir, 0755); os.IsNotExist(err) { 219 if err = os.MkdirAll(dir, 0755); err != nil { 220 return err 221 } 222 } else if err != nil { 223 return err 224 } 225 226 file, err := os.Create(filePath) 227 if err != nil { 228 return err 229 } 230 defer util.CheckClose(file) 231 232 err = t.Execute(file, config) 233 return err 234 } 235 236 // setWordpressSiteSettingsPaths sets the expected settings files paths for 237 // a WordPress site. 238 func setWordpressSiteSettingsPaths(app *DdevApp) { 239 config := NewWordpressConfig(app, "") 240 241 settingsFileBasePath := filepath.Join(app.AppRoot, app.Docroot) 242 app.SiteSettingsPath = filepath.Join(settingsFileBasePath, config.SiteSettings) 243 app.SiteDdevSettingsFile = filepath.Join(settingsFileBasePath, config.SiteSettingsDdev) 244 } 245 246 // isWordpressApp returns true if the app of of type wordpress 247 func isWordpressApp(app *DdevApp) bool { 248 _, err := wordpressGetRelativeAbsPath(app) 249 if err != nil { 250 // Multiple abspath candidates is an issue, but is still a valid 251 // indicator that this is a WordPress app 252 if strings.Contains(err.Error(), "multiple") { 253 return true 254 } 255 256 return false 257 } 258 259 return true 260 } 261 262 // wordpressImportFilesAction defines the Wordpress workflow for importing project files. 263 // The Wordpress workflow is currently identical to the Drupal import-files workflow. 264 func wordpressImportFilesAction(app *DdevApp, target, importPath, extPath string) error { 265 destPath := app.calculateHostUploadDirFullPath(target) 266 267 // Parent of destination dir should exist 268 if !fileutil.FileExists(filepath.Dir(destPath)) { 269 return fmt.Errorf("unable to import to %s: parent directory does not exist", destPath) 270 } 271 272 // Parent of destination dir should be writable. 273 if err := os.Chmod(filepath.Dir(destPath), 0755); err != nil { 274 return err 275 } 276 277 // If the destination path exists, remove it as was warned 278 if fileutil.FileExists(destPath) { 279 if err := os.RemoveAll(destPath); err != nil { 280 return fmt.Errorf("failed to cleanup %s before import: %v", destPath, err) 281 } 282 } 283 284 if isTar(importPath) { 285 if err := archive.Untar(importPath, destPath, extPath); err != nil { 286 return fmt.Errorf("failed to extract provided archive: %v", err) 287 } 288 289 return nil 290 } 291 292 if isZip(importPath) { 293 if err := archive.Unzip(importPath, destPath, extPath); err != nil { 294 return fmt.Errorf("failed to extract provided archive: %v", err) 295 } 296 297 return nil 298 } 299 300 //nolint: revive 301 if err := fileutil.CopyDir(importPath, destPath); err != nil { 302 return err 303 } 304 305 return nil 306 } 307 308 // wordpressGetRelativeAbsPath returns the portion of the ABSPATH value that will come after "/" in wp-config.php - 309 // this is done by searching (at a max depth of one directory from the docroot) for wp-settings.php, the 310 // file we're using as a signal to indicate that this is a WordPress project. 311 func wordpressGetRelativeAbsPath(app *DdevApp) (string, error) { 312 needle := "wp-settings.php" 313 314 curDirMatches, err := filepath.Glob(filepath.Join(app.AppRoot, app.Docroot, needle)) 315 if err != nil { 316 return "", err 317 } 318 319 if len(curDirMatches) > 0 { 320 return "", nil 321 } 322 323 subDirMatches, err := filepath.Glob(filepath.Join(app.AppRoot, app.Docroot, "*", needle)) 324 if err != nil { 325 return "", err 326 } 327 328 if len(subDirMatches) == 0 { 329 return "", fmt.Errorf("unable to find %s in subdirectories", needle) 330 } 331 332 if len(subDirMatches) > 1 { 333 return "", fmt.Errorf("multiple subdirectories contain %s", needle) 334 } 335 336 absPath := filepath.Base(filepath.Dir(subDirMatches[0])) 337 338 return absPath, nil 339 }