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