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  }