github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/drupal.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "github.com/drud/ddev/pkg/dockerutil" 6 "github.com/drud/ddev/pkg/nodeps" 7 "github.com/drud/ddev/pkg/output" 8 "github.com/drud/ddev/pkg/util" 9 10 "os" 11 "path" 12 "path/filepath" 13 "text/template" 14 15 "github.com/drud/ddev/pkg/fileutil" 16 17 "github.com/drud/ddev/pkg/archive" 18 ) 19 20 // DrupalSettings encapsulates all the configurations for a Drupal site. 21 type DrupalSettings struct { 22 DeployName string 23 DeployURL string 24 DatabaseName string 25 DatabaseUsername string 26 DatabasePassword string 27 DatabaseHost string 28 DatabaseDriver string 29 DatabasePort string 30 DatabasePrefix string 31 HashSalt string 32 Signature string 33 SitePath string 34 SiteSettings string 35 SiteSettingsDdev string 36 SyncDir string 37 DockerIP string 38 DBPublishedPort int 39 } 40 41 // NewDrupalSettings produces a DrupalSettings object with default. 42 func NewDrupalSettings(app *DdevApp) *DrupalSettings { 43 dockerIP, _ := dockerutil.GetDockerIP() 44 dbPublishedPort, _ := app.GetPublishedPort("db") 45 46 settings := &DrupalSettings{ 47 DatabaseName: "db", 48 DatabaseUsername: "db", 49 DatabasePassword: "db", 50 DatabaseHost: "db", 51 DatabaseDriver: "mysql", 52 DatabasePort: GetExposedPort(app, "db"), 53 DatabasePrefix: "", 54 HashSalt: util.RandString(64), 55 Signature: nodeps.DdevFileSignature, 56 SitePath: path.Join("sites", "default"), 57 SiteSettings: "settings.php", 58 SiteSettingsDdev: "settings.ddev.php", 59 SyncDir: path.Join("files", "sync"), 60 DockerIP: dockerIP, 61 DBPublishedPort: dbPublishedPort, 62 } 63 if app.Type == "drupal6" { 64 settings.DatabaseDriver = "mysqli" 65 } 66 if app.Database.Type == nodeps.Postgres { 67 settings.DatabaseDriver = "pgsql" 68 } 69 return settings 70 } 71 72 // settingsIncludeStanza defines the template that will be appended to 73 // a project's settings.php in the event that the file already exists. 74 const settingsIncludeStanza = ` 75 // Automatically generated include for settings managed by ddev. 76 $ddev_settings = dirname(__FILE__) . '/settings.ddev.php'; 77 if (getenv('IS_DDEV_PROJECT') == 'true' && is_readable($ddev_settings)) { 78 require $ddev_settings; 79 } 80 ` 81 82 // manageDrupalSettingsFile will direct inspecting and writing of settings.php. 83 func manageDrupalSettingsFile(app *DdevApp, drupalConfig *DrupalSettings, appType string) error { 84 // We'll be writing/appending to the settings files and parent directory, make sure we have permissions to do so 85 if err := drupalEnsureWritePerms(app); err != nil { 86 return err 87 } 88 89 if !fileutil.FileExists(app.SiteSettingsPath) { 90 output.UserOut.Printf("No %s file exists, creating one", drupalConfig.SiteSettings) 91 92 if err := writeDrupalSettingsPHP(app.SiteSettingsPath, appType); err != nil { 93 return fmt.Errorf("failed to write: %v", err) 94 } 95 } 96 97 included, err := settingsHasInclude(drupalConfig, app.SiteSettingsPath) 98 if err != nil { 99 return fmt.Errorf("failed to check for include: %v", err) 100 } 101 102 if included { 103 util.Debug("Existing %s file includes %s", drupalConfig.SiteSettings, drupalConfig.SiteSettingsDdev) 104 } else { 105 util.Debug("Existing %s file does not include %s, modifying to include ddev settings", drupalConfig.SiteSettings, drupalConfig.SiteSettingsDdev) 106 107 if err := appendIncludeToDrupalSettingsFile(app.SiteSettingsPath, app.Type); err != nil { 108 return fmt.Errorf("failed to include %s in %s: %v", drupalConfig.SiteSettingsDdev, drupalConfig.SiteSettings, err) 109 } 110 } 111 112 return nil 113 } 114 115 // writeDrupalSettingsPHP creates the project's settings.php if it doesn't exist 116 func writeDrupalSettingsPHP(filePath string, appType string) error { 117 content, err := bundledAssets.ReadFile(path.Join("drupal", appType, "settings.php")) 118 if err != nil { 119 return err 120 } 121 122 // Ensure target directory exists and is writable 123 dir := filepath.Dir(filePath) 124 if err = os.Chmod(dir, 0755); os.IsNotExist(err) { 125 if err = os.MkdirAll(dir, 0755); err != nil { 126 return err 127 } 128 } else if err != nil { 129 return err 130 } 131 132 // Create file 133 err = os.WriteFile(filePath, content, 0755) 134 if err != nil { 135 return err 136 } 137 138 return nil 139 } 140 141 // createDrupalSettingsPHP manages creation and modification of settings.php and settings.ddev.php. 142 // If a settings.php file already exists, it will be modified to ensure that it includes 143 // settings.ddev.php, which contains ddev-specific configuration. 144 func createDrupalSettingsPHP(app *DdevApp) (string, error) { 145 // Currently there isn't any customization done for the drupal config, but 146 // we may want to do some kind of customization in the future. 147 drupalConfig := NewDrupalSettings(app) 148 149 if err := manageDrupalSettingsFile(app, drupalConfig, app.Type); err != nil { 150 return "", err 151 } 152 153 if err := writeDrupalSettingsDdevPhp(drupalConfig, app.SiteDdevSettingsFile, app); err != nil { 154 return "", fmt.Errorf("`failed to write` Drupal settings file %s: %v", app.SiteDdevSettingsFile, err) 155 } 156 157 return app.SiteDdevSettingsFile, nil 158 } 159 160 // writeDrupalSettingsDdevPhp dynamically produces valid settings.ddev.php file by combining a configuration 161 // object with a data-driven template. 162 func writeDrupalSettingsDdevPhp(settings *DrupalSettings, filePath string, app *DdevApp) error { 163 if fileutil.FileExists(filePath) { 164 // Check if the file is managed by ddev. 165 signatureFound, err := fileutil.FgrepStringInFile(filePath, nodeps.DdevFileSignature) 166 if err != nil { 167 return err 168 } 169 170 // If the signature wasn't found, warn the user and return. 171 if !signatureFound { 172 util.Warning("%s already exists and is managed by the user.", filepath.Base(filePath)) 173 return nil 174 } 175 } 176 177 t, err := template.New("settings.ddev.php").ParseFS(bundledAssets, path.Join("drupal", app.Type, "settings.ddev.php")) 178 if err != nil { 179 return err 180 } 181 182 // Ensure target directory exists and is writable 183 dir := filepath.Dir(filePath) 184 if err = os.Chmod(dir, 0755); os.IsNotExist(err) { 185 if err = os.MkdirAll(dir, 0755); err != nil { 186 return err 187 } 188 } else if err != nil { 189 return err 190 } 191 192 file, err := os.Create(filePath) 193 if err != nil { 194 return err 195 } 196 err = t.Execute(file, settings) 197 if err != nil { 198 return err 199 } 200 util.CheckClose(file) 201 return nil 202 } 203 204 // WriteDrushrc writes out drushrc.php based on passed-in values. 205 // This works on Drupal 6 and Drupal 7 or with drush8 and older 206 func WriteDrushrc(app *DdevApp, filePath string) error { 207 if fileutil.FileExists(filePath) { 208 // Check if the file is managed by ddev. 209 signatureFound, err := fileutil.FgrepStringInFile(filePath, nodeps.DdevFileSignature) 210 if err != nil { 211 return err 212 } 213 214 // If the signature wasn't found, warn the user and return. 215 if !signatureFound { 216 util.Warning("%s already exists and is managed by the user.", filepath.Base(filePath)) 217 return nil 218 } 219 } 220 221 uri := app.GetPrimaryURL() 222 drushContents := []byte(`<?php 223 224 /** 225 * @file 226 * ` + nodeps.DdevFileSignature + `: Automatically generated drushrc.php file (for Drush 8) 227 * ddev manages this file and may delete or overwrite the file unless this comment is removed. 228 * Remove this comment if you don't want ddev to manage this file. 229 */ 230 231 if (getenv('IS_DDEV_PROJECT') == 'true') { 232 $options['l'] = "` + uri + `"; 233 } 234 `) 235 236 // Ensure target directory exists and is writable 237 dir := filepath.Dir(filePath) 238 if err := os.Chmod(dir, 0755); os.IsNotExist(err) { 239 if err = os.MkdirAll(dir, 0755); err != nil { 240 return err 241 } 242 } else if err != nil { 243 return err 244 } 245 246 err := os.WriteFile(filePath, drushContents, 0666) 247 if err != nil { 248 return err 249 } 250 251 return nil 252 } 253 254 // getDrupalUploadDir will return a custom upload dir if defined, returning a default path if not. 255 func getDrupalUploadDir(app *DdevApp) string { 256 if app.UploadDir == "" { 257 return "sites/default/files" 258 } 259 260 return app.UploadDir 261 } 262 263 // Drupal8Hooks adds a d8-specific hooks example for post-import-db 264 const Drupal8Hooks = `# post-import-db: 265 # - exec: drush cr 266 # - exec: drush updb 267 ` 268 269 // Drupal7Hooks adds a d7-specific hooks example for post-import-db 270 const Drupal7Hooks = `# post-import-db: 271 # - exec: drush cc all 272 ` 273 274 // getDrupal7Hooks for appending as byte array 275 func getDrupal7Hooks() []byte { 276 return []byte(Drupal7Hooks) 277 } 278 279 // getDrupal6Hooks for appending as byte array 280 func getDrupal6Hooks() []byte { 281 // We don't have anything new to add yet, so just use Drupal7 version 282 return []byte(Drupal7Hooks) 283 } 284 285 // getDrupal8Hooks for appending as byte array 286 func getDrupal8Hooks() []byte { 287 return []byte(Drupal8Hooks) 288 } 289 290 // setDrupalSiteSettingsPaths sets the paths to settings.php/settings.ddev.php 291 // for templating. 292 func setDrupalSiteSettingsPaths(app *DdevApp) { 293 drupalConfig := NewDrupalSettings(app) 294 settingsFileBasePath := filepath.Join(app.AppRoot, app.Docroot) 295 app.SiteSettingsPath = filepath.Join(settingsFileBasePath, drupalConfig.SitePath, drupalConfig.SiteSettings) 296 app.SiteDdevSettingsFile = filepath.Join(settingsFileBasePath, drupalConfig.SitePath, drupalConfig.SiteSettingsDdev) 297 } 298 299 // isDrupal7App returns true if the app is of type drupal7 300 func isDrupal7App(app *DdevApp) bool { 301 if _, err := os.Stat(filepath.Join(app.AppRoot, app.Docroot, "misc/ajax.js")); err == nil { 302 return true 303 } 304 return false 305 } 306 307 // isDrupal8App returns true if the app is of type drupal8 308 func isDrupal8App(app *DdevApp) bool { 309 isD8, err := fileutil.FgrepStringInFile(filepath.Join(app.AppRoot, app.Docroot, "core/lib/Drupal.php"), `const VERSION = '8`) 310 if err == nil && isD8 { 311 return true 312 } 313 return false 314 } 315 316 // isDrupal9App returns true if the app is of type drupal9 317 func isDrupal9App(app *DdevApp) bool { 318 isD9, err := fileutil.FgrepStringInFile(filepath.Join(app.AppRoot, app.Docroot, "core/lib/Drupal.php"), `const VERSION = '9`) 319 if err == nil && isD9 { 320 return true 321 } 322 return false 323 } 324 325 // isDrupal10App returns true if the app is of type drupal10 326 func isDrupal10App(app *DdevApp) bool { 327 isD10, err := fileutil.FgrepStringInFile(filepath.Join(app.AppRoot, app.Docroot, "core/lib/Drupal.php"), `const VERSION = '10`) 328 if err == nil && isD10 { 329 return true 330 } 331 return false 332 } 333 334 // isDrupal6App returns true if the app is of type Drupal6 335 func isDrupal6App(app *DdevApp) bool { 336 if _, err := os.Stat(filepath.Join(app.AppRoot, app.Docroot, "misc/ahah.js")); err == nil { 337 return true 338 } 339 return false 340 } 341 342 // drupal6ConfigOverrideAction overrides php_version for D6, since it is incompatible 343 // with php7+ 344 func drupal6ConfigOverrideAction(app *DdevApp) error { 345 app.PHPVersion = nodeps.PHP56 346 return nil 347 } 348 349 func drupal8ConfigOverrideAction(app *DdevApp) error { 350 app.PHPVersion = nodeps.PHP74 351 return nil 352 } 353 354 // drupal0ConfigOverrideAction overrides php_version for D10, requires PHP8.0 355 //func drupal9ConfigOverrideAction(app *DdevApp) error { 356 // app.PHPVersion = nodeps.PHP80 357 // return nil 358 //} 359 360 // drupal10ConfigOverrideAction overrides php_version for D10, requires PHP8.0 361 func drupal10ConfigOverrideAction(app *DdevApp) error { 362 app.PHPVersion = nodeps.PHP81 363 return nil 364 } 365 366 // drupal8PostStartAction handles default post-start actions for D8 apps, like ensuring 367 // useful permissions settings on sites/default. 368 func drupal8PostStartAction(app *DdevApp) error { 369 // Return early because we aren't expected to manage settings. 370 if app.DisableSettingsManagement { 371 return nil 372 } 373 if err := createDrupal8SyncDir(app); err != nil { 374 return err 375 } 376 377 //nolint: revive 378 if err := drupalEnsureWritePerms(app); err != nil { 379 return err 380 } 381 return nil 382 } 383 384 func drupalPostStartAction(app *DdevApp) error { 385 if isDrupal9App(app) || isDrupal10App(app) { 386 // pg_trm extension is required in Drupal9.5+ 387 if app.Database.Type == nodeps.Postgres { 388 stdout, stderr, err := app.Exec(&ExecOpts{ 389 Service: "db", 390 Cmd: `psql -q -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" 2>/dev/null`, 391 NoCapture: false, 392 }) 393 if err != nil { 394 util.Warning("unable to CREATE EXTENSION pg_trm: stdout='%s', stderr='%s', err=%v", stdout, stderr, err) 395 } 396 } 397 // SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED required in Drupal 9.5+ 398 if app.Database.Type == nodeps.MariaDB || app.Database.Type == nodeps.MySQL { 399 stdout, stderr, err := app.Exec(&ExecOpts{ 400 Service: "db", 401 Cmd: `mysql -e "SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;" 2>/dev/null`, 402 NoCapture: false, 403 }) 404 if err != nil { 405 util.Warning("unable to SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED: stdout='%s', stderr='%s', err=%v", stdout, stderr, err) 406 } 407 } 408 } 409 // Return early because we aren't expected to manage settings. 410 if app.DisableSettingsManagement { 411 return nil 412 } 413 if err := createDrupal8SyncDir(app); err != nil { 414 return err 415 } 416 417 //nolint: revive 418 if err := drupalEnsureWritePerms(app); err != nil { 419 return err 420 } 421 return nil 422 } 423 424 // drupal7PostStartAction handles default post-start actions for D7 apps, like ensuring 425 // useful permissions settings on sites/default. 426 func drupal7PostStartAction(app *DdevApp) error { 427 // Return early because we aren't expected to manage settings. 428 if app.DisableSettingsManagement { 429 return nil 430 } 431 if err := drupalEnsureWritePerms(app); err != nil { 432 return err 433 } 434 435 err := WriteDrushrc(app, filepath.Join(filepath.Dir(app.SiteSettingsPath), "drushrc.php")) 436 if err != nil { 437 util.Warning("Failed to WriteDrushrc: %v", err) 438 } 439 440 return nil 441 } 442 443 // drupal6PostStartAction handles default post-start actions for D6 apps, like ensuring 444 // useful permissions settings on sites/default. 445 func drupal6PostStartAction(app *DdevApp) error { 446 // Return early because we aren't expected to manage settings. 447 if app.DisableSettingsManagement { 448 return nil 449 } 450 451 if err := drupalEnsureWritePerms(app); err != nil { 452 return err 453 } 454 455 err := WriteDrushrc(app, filepath.Join(filepath.Dir(app.SiteSettingsPath), "drushrc.php")) 456 if err != nil { 457 util.Warning("Failed to WriteDrushrc: %v", err) 458 } 459 return nil 460 } 461 462 // drupalEnsureWritePerms will ensure sites/default and sites/default/settings.php will 463 // have the appropriate permissions for development. 464 func drupalEnsureWritePerms(app *DdevApp) error { 465 util.Debug("Ensuring write permissions for %s", app.GetName()) 466 var writePerms os.FileMode = 0200 467 468 settingsDir := path.Dir(app.SiteSettingsPath) 469 makeWritable := []string{ 470 settingsDir, 471 app.SiteSettingsPath, 472 app.SiteDdevSettingsFile, 473 path.Join(settingsDir, "services.yml"), 474 } 475 476 for _, o := range makeWritable { 477 stat, err := os.Stat(o) 478 if err != nil { 479 if !os.IsNotExist(err) { 480 util.Warning("Unable to ensure write permissions: %v", err) 481 } 482 483 continue 484 } 485 486 if err := os.Chmod(o, stat.Mode()|writePerms); err != nil { 487 // Warn the user, but continue. 488 util.Warning("Unable to set permissions: %v", err) 489 } 490 } 491 492 return nil 493 } 494 495 // createDrupal8SyncDir creates a Drupal 8 app's sync directory 496 func createDrupal8SyncDir(app *DdevApp) error { 497 // Currently there isn't any customization done for the drupal config, but 498 // we may want to do some kind of customization in the future. 499 drupalConfig := NewDrupalSettings(app) 500 501 syncDirPath := path.Join(app.GetAppRoot(), app.GetDocroot(), "sites/default", drupalConfig.SyncDir) 502 if fileutil.FileExists(syncDirPath) { 503 return nil 504 } 505 506 if err := os.MkdirAll(syncDirPath, 0755); err != nil { 507 return fmt.Errorf("failed to create sync directory (%s): %v", syncDirPath, err) 508 } 509 510 return nil 511 } 512 513 // settingsHasInclude determines if the settings.php or equivalent includes settings.ddev.php or equivalent. 514 // This is done by looking for the ddev settings file (settings.ddev.php) in settings.php. 515 func settingsHasInclude(drupalConfig *DrupalSettings, siteSettingsPath string) (bool, error) { 516 included, err := fileutil.FgrepStringInFile(siteSettingsPath, drupalConfig.SiteSettingsDdev) 517 if err != nil { 518 return false, err 519 } 520 521 return included, nil 522 } 523 524 // appendIncludeToDrupalSettingsFile modifies the settings.php file to include the settings.ddev.php 525 // file, which contains ddev-specific configuration. 526 func appendIncludeToDrupalSettingsFile(siteSettingsPath string, appType string) error { 527 // Check if file is empty 528 contents, err := os.ReadFile(siteSettingsPath) 529 if err != nil { 530 return err 531 } 532 533 // If the file is empty, write the complete settings file and return 534 if len(contents) == 0 { 535 return writeDrupalSettingsPHP(siteSettingsPath, appType) 536 } 537 538 // The file is not empty, open it for appending 539 file, err := os.OpenFile(siteSettingsPath, os.O_RDWR|os.O_APPEND, 0644) 540 if err != nil { 541 return err 542 } 543 defer util.CheckClose(file) 544 545 _, err = file.Write([]byte(settingsIncludeStanza)) 546 if err != nil { 547 return err 548 } 549 return nil 550 } 551 552 // drupalImportFilesAction defines the Drupal workflow for importing project files. 553 func drupalImportFilesAction(app *DdevApp, importPath, extPath string) error { 554 destPath := app.GetHostUploadDirFullPath() 555 556 // parent of destination dir should exist 557 if !fileutil.FileExists(filepath.Dir(destPath)) { 558 return fmt.Errorf("unable to import to %s: parent directory does not exist", destPath) 559 } 560 561 // parent of destination dir should be writable. 562 if err := os.Chmod(filepath.Dir(destPath), 0755); err != nil { 563 return err 564 } 565 566 // If the destination path exists, remove it as was warned 567 if fileutil.FileExists(destPath) { 568 if err := os.RemoveAll(destPath); err != nil { 569 return fmt.Errorf("failed to cleanup %s before import: %v", destPath, err) 570 } 571 } 572 573 if isTar(importPath) { 574 if err := archive.Untar(importPath, destPath, extPath); err != nil { 575 return fmt.Errorf("failed to extract provided archive: %v", err) 576 } 577 578 return nil 579 } 580 581 if isZip(importPath) { 582 if err := archive.Unzip(importPath, destPath, extPath); err != nil { 583 return fmt.Errorf("failed to extract provided archive: %v", err) 584 } 585 586 return nil 587 } 588 589 //nolint: revive 590 if err := fileutil.CopyDir(importPath, destPath); err != nil { 591 return err 592 } 593 594 return nil 595 }