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  }