@automattic/vip 3.3.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,6 +39,8 @@ services:
39
39
  command: nginx -g "daemon off;"
40
40
  environment:
41
41
  LANDO_NEEDS_EXEC: 1
42
+ LANDO_WEBROOT_USER: nginx
43
+ LANDO_WEBROOT_GROUP: nginx
42
44
  volumes:
43
45
  - ./nginx/extra.conf:/etc/nginx/conf.extra/extra.conf
44
46
  <% wpVolumes() %>
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External dependencies
5
+ */
6
+ "use strict";
7
+
8
+ exports.__esModule = true;
9
+ exports.appDeployValidateCmd = appDeployValidateCmd;
10
+ var _chalk = _interopRequireDefault(require("chalk"));
11
+ var _debug = _interopRequireDefault(require("debug"));
12
+ var _path = require("path");
13
+ var _command = _interopRequireDefault(require("../lib/cli/command"));
14
+ var _clientFileUploader = require("../lib/client-file-uploader");
15
+ var _customDeploy = require("../lib/custom-deploy/custom-deploy");
16
+ var _tracker = require("../lib/tracker");
17
+ var _customDeploy2 = require("../lib/validations/custom-deploy");
18
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ const debug = (0, _debug.default)('@automattic/vip:bin:vip-app-deploy-validate');
23
+ async function appDeployValidateCmd(arg = [], opts = {}) {
24
+ const app = opts.app;
25
+ const env = opts.env;
26
+ const [fileName] = arg;
27
+ const fileMeta = await (0, _clientFileUploader.getFileMeta)(fileName);
28
+ debug('Options: ', opts);
29
+ debug('Args: ', arg);
30
+ const track = _tracker.trackEventWithEnv.bind(null, app, env);
31
+ debug('Validating file...');
32
+ await (0, _customDeploy.validateFile)(app, env, fileMeta);
33
+ await track('deploy_validate_app_command_execute');
34
+ const ext = (0, _path.extname)(fileName);
35
+ if (ext === '.zip') {
36
+ (0, _customDeploy2.validateZipFile)(fileName);
37
+ } else {
38
+ await (0, _customDeploy2.validateTarFile)(fileName);
39
+ }
40
+ console.log(_chalk.default.green('✓ Compressed file has been successfully validated with no errors!'));
41
+ }
42
+
43
+ // Command examples for the `vip app deploy validate` help prompt
44
+ const examples = [{
45
+ usage: 'vip app @mysite.develop deploy validate <file.zip|file.tar.gz>',
46
+ description: 'Validate the compressed file to see if it can be deployed to your site'
47
+ }];
48
+ void (0, _command.default)({
49
+ requiredArgs: 1
50
+ }).examples(examples).option('app', 'The application name or ID').option('env', 'The environment name or ID').argv(process.argv, appDeployValidateCmd);
@@ -232,4 +232,4 @@ const examples = [
232
232
  }];
233
233
  void (0, _command.default)({
234
234
  requiredArgs: 1
235
- }).examples(examples).option('message', 'Custom message for deploy').option('skip-confirmation', 'Skip confirmation prompt').option('force', 'Skip confirmation prompt (deprecated)').option('app', 'The application name or ID').option('env', 'The environment name or ID').argv(process.argv, appDeployCmd);
235
+ }).command('validate', 'Validate a file before deploying in Custom Deployments').examples(examples).option('message', 'Custom message for deploy').option('skip-confirmation', 'Skip confirmation prompt').option('force', 'Skip confirmation prompt (deprecated)').option('app', 'The application name or ID').option('env', 'The environment name or ID').argv(process.argv, appDeployCmd);
@@ -11,12 +11,14 @@ var _tracker = require("../lib/tracker");
11
11
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
12
12
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
13
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ const usage = 'vip cache purge-url';
15
+ const exampleUsage = 'vip @example-app.develop cache purge-url';
14
16
  const examples = [{
15
- usage: 'vip cache purge-url <URL>',
16
- description: 'Purge a URL from page cache'
17
+ usage: `${exampleUsage} https://example-app-develop.go-vip.co/sample-page/` + '\n - Purged URL: https://example-app.develop.go-vip.co/sample-page/',
18
+ description: 'Purge the page cache for a single URL.'
17
19
  }, {
18
- usage: 'vip cache purge-url --from-file=/dev/vip/urls.txt',
19
- description: 'Purge multiple URLs from page cache'
20
+ usage: `${exampleUsage} --from-file=./urls.txt`,
21
+ description: 'Purge the page cache for multiple URLs, each listed on a single line in a local file.'
20
22
  }];
21
23
  async function cachePurgeCommand(urls = [], opt = {}) {
22
24
  const trackingParams = {
@@ -59,5 +61,5 @@ async function cachePurgeCommand(urls = [], opt = {}) {
59
61
  appQuery: _cachePurge.appQuery,
60
62
  envContext: true,
61
63
  wildcardCommand: true,
62
- usage: 'vip cache purge-url <URL>'
63
- }).option('from-file', 'Read URLs from file (useful to purge multiple URLs)').examples(examples).argv(process.argv, cachePurgeCommand);
64
+ usage
65
+ }).option('from-file', 'Read one or more URLs from a file, each listed on a single line.').examples(examples).argv(process.argv, cachePurgeCommand);
@@ -3,6 +3,16 @@
3
3
 
4
4
  var _command = _interopRequireDefault(require("../lib/cli/command"));
5
5
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
+ const usage = 'vip cache';
7
+ const exampleUsage = 'vip @example-app.develop cache';
8
+ const examples = [{
9
+ usage: `${exampleUsage} purge-url https://example-app-develop.go-vip.co/sample-page/` + '\n - Purged URL: https://example-app.develop.go-vip.co/sample-page/',
10
+ description: 'Purge the page cache for a single URL.'
11
+ }, {
12
+ usage: `${exampleUsage} purge-url --from-file=./urls.txt`,
13
+ description: 'Purge the page cache for multiple URLs, each listed on a single line in a local file.'
14
+ }];
6
15
  (0, _command.default)({
7
- requiredArgs: 1
8
- }).command('purge-url', 'Purge page cache').argv(process.argv);
16
+ requiredArgs: 1,
17
+ usage
18
+ }).command('purge-url', 'Purge page cache for one or more URLs.').examples(examples).argv(process.argv);
@@ -41,7 +41,7 @@ const cmd = (0, _command.default)({
41
41
  wildcardCommand: true,
42
42
  usage
43
43
  }).examples(examples);
44
- cmd.option('yes', 'Auto-confirm update');
44
+ cmd.option('yes', 'Skip the confirmation prompt and automatically submit "y".');
45
45
  cmd.argv(process.argv, async (arg, opt) => {
46
46
  const {
47
47
  app,
@@ -33,7 +33,7 @@ const examples = [{
33
33
  }];
34
34
  const cmd = (0, _command.default)({
35
35
  usage
36
- }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('title', 'A descriptive value for the WordPress Site Title. Default is "VIP Dev").').option('multisite', 'Create environment as a multisite. Accepts "y" for a subdomain multisite, "subdirectory" (recommended) for a subdirectory multisite, or "false". Default is "y".', undefined, _devEnvironmentCli.processStringOrBooleanOption);
36
+ }).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('title', 'A descriptive value for the WordPress Site Title. Default is "VIP Dev".').option('multisite', 'Create environment as a multisite. Accepts "y" for a subdomain multisite, "subdirectory" (recommended) for a subdirectory multisite, or "false". Default is "y".', undefined, _devEnvironmentCli.processStringOrBooleanOption);
37
37
  (0, _devEnvironmentCli.addDevEnvConfigurationOptions)(cmd);
38
38
  cmd.examples(examples);
39
39
  cmd.argv(process.argv, async (arg, opt) => {
@@ -82,10 +82,10 @@ const appQuery = `id,name,environments{
82
82
  let environment = application.environments.find(env => env.id === opts.env.id);
83
83
  if (syncing) {
84
84
  if (environment.syncProgress.status === 'running') {
85
- console.log(_chalk.default.yellow('Note:'), 'A data sync is already running');
85
+ console.log(_chalk.default.yellow('Note:'), 'A data sync is already running.');
86
86
  } else {
87
- console.log(_chalk.default.yellow('Note:'), 'Someone recently ran a data sync on this site');
88
- console.log(_chalk.default.yellow('Note:'), 'Please wait a few minutes before trying again');
87
+ console.log(_chalk.default.yellow('Note:'), 'Someone recently ran a data sync on this site.');
88
+ console.log(_chalk.default.yellow('Note:'), 'Please wait a few minutes before trying again.');
89
89
  }
90
90
  }
91
91
  console.log();
@@ -157,14 +157,14 @@ const appQuery = `id,name,environments{
157
157
  await (0, _tracker.trackEvent)('sync_command_error', {
158
158
  error: 'API returned `failed` status'
159
159
  });
160
- out.push(`${marks.failed} Data Sync is finished for ${opts.app.name}`);
160
+ out.push(`${marks.failed} Data Sync is finished for ${opts.app.name}.`);
161
161
  out.push('');
162
162
  break;
163
163
  case 'success':
164
164
  default:
165
165
  clearInterval(progress);
166
166
  await (0, _tracker.trackEvent)('sync_command_success');
167
- out.push(`${marks.success} Data Sync is finished for ${opts.app.name}`);
167
+ out.push(`${marks.success} Data Sync is finished for ${opts.app.name}.`);
168
168
  out.push('');
169
169
  break;
170
170
  }
package/dist/bin/vip.js CHANGED
@@ -22,7 +22,7 @@ const tokenURL = 'https://dashboard.wpvip.com/me/cli/token';
22
22
  const customDeployToken = process.env.WPVIP_DEPLOY_TOKEN;
23
23
  const runCmd = async function () {
24
24
  const cmd = (0, _command.default)();
25
- cmd.command('logout', 'Log out the current authenticated VIP-CLI user.').command('app', 'List and modify your VIP applications').command('backup', 'Generate a backup of an environment.').command('cache', 'Manage page cache for your VIP applications').command('config', 'Manage environment configurations.').command('dev-env', 'Create and manage VIP Local Development Environments.').command('export', 'Export a copy of data associated with an environment.').command('import', 'Import media or SQL files into your VIP applications').command('logs', 'Get logs from your VIP applications').command('search-replace', 'Perform search and replace tasks on files').command('slowlogs', 'Retrieve MySQL slow query logs from an environment.').command('db', "Access an environment's database.").command('sync', 'Sync production to a development environment').command('whoami', 'Retrieve details about the current authenticated VIP-CLI user.').command('validate', 'Validate your VIP application and environment').command('wp', 'Run WP CLI commands against an environment');
25
+ cmd.command('logout', 'Log out the current authenticated VIP-CLI user.').command('app', 'List and modify your VIP applications').command('backup', 'Generate a backup of an environment.').command('cache', 'Manage page cache for an environment.').command('config', 'Manage environment configurations.').command('dev-env', 'Create and manage VIP Local Development Environments.').command('export', 'Export a copy of data associated with an environment.').command('import', 'Import media or SQL files into your VIP applications').command('logs', 'Get logs from your VIP applications').command('search-replace', 'Perform search and replace tasks on files').command('slowlogs', 'Retrieve MySQL slow query logs from an environment.').command('db', "Access an environment's database.").command('sync', 'Sync production to a development environment').command('whoami', 'Retrieve details about the current authenticated VIP-CLI user.').command('validate', 'Validate your VIP application and environment').command('wp', 'Run WP CLI commands against an environment');
26
26
  cmd.argv(process.argv);
27
27
  };
28
28
 
@@ -426,7 +426,7 @@ _args.default.argv = async function (argv, cb) {
426
426
  });
427
427
  options.exportFileErrorsToJson = Object.hasOwn(options, 'exportFileErrorsToJson') && Boolean(options.exportFileErrorsToJson) && !['false', 'no'].includes(options.exportFileErrorsToJson);
428
428
  info.push({
429
- key: 'Export any file errors encountered to a JSON file instead of a plain text file',
429
+ key: 'Export any file errors encountered to a JSON file instead of a plain text file.',
430
430
  value: options.exportFileErrorsToJson ? '✅ Yes' : `${_chalk.default.red('x')} No`
431
431
  });
432
432
  break;
@@ -508,22 +508,22 @@ function _default(opts) {
508
508
  ...opts
509
509
  };
510
510
  if (_opts.appContext || _opts.requireConfirm) {
511
- _args.default.option('app', 'Specify the app');
511
+ _args.default.option('app', 'Target an application. Accepts a string value for the application name or an integer for the application ID.');
512
512
  }
513
513
  if (_opts.envContext || _opts.childEnvContext) {
514
- _args.default.option('env', 'Specify the environment');
514
+ _args.default.option('env', 'Target an environment. Accepts a string value for the environment type.');
515
515
  }
516
516
  if (_opts.requireConfirm) {
517
- _args.default.option('force', 'Skip confirmation', false);
517
+ _args.default.option('force', 'Skip confirmation.', false);
518
518
  }
519
519
  if (_opts.format) {
520
- _args.default.option('format', 'Format results', 'table');
520
+ _args.default.option('format', 'Render output in a particular format. Accepts "table" (default), "csv", and "json".', 'table');
521
521
  }
522
522
 
523
523
  // Add help and version to all subcommands
524
- _args.default.option('help', 'Output the help for the (sub)command');
525
- _args.default.option('version', 'Output the version number');
526
- _args.default.option('debug', 'Activate debug output');
524
+ _args.default.option('help', 'Retrieve a description, examples, and available options for a (sub)command.');
525
+ _args.default.option('version', 'Retrieve the version number of VIP-CLI currently installed on the local machine.');
526
+ _args.default.option('debug', 'Generate verbose output during command execution to help identify or fix errors or bugs.');
527
527
  return _args.default;
528
528
  }
529
529
  function getEnvIdentifier(env) {
@@ -512,7 +512,10 @@ async function promptForWordPress(defaultObject) {
512
512
 
513
513
  // image with selection
514
514
  const tagChoices = await getTagChoices();
515
- let option = defaultObject?.tag ?? tagChoices[0].value;
515
+ if (typeof defaultObject === 'string' && !tagChoices.find(choice => choice.value === defaultObject)) {
516
+ throw new Error(`Unknown or unsupported WordPress version: ${defaultObject}.`);
517
+ }
518
+ let option = typeof defaultObject === 'string' ? defaultObject : defaultObject?.tag ?? tagChoices[0].value;
516
519
  if (isStdinTTY) {
517
520
  const message = `${messagePrefix}Which version would you like`;
518
521
  const selectTag = new _enquirer.Select({
@@ -620,9 +623,10 @@ function processVersionOption(value) {
620
623
  }
621
624
  return value?.toString() ?? '';
622
625
  }
626
+ const phpVersionsSupported = Object.keys(_devEnvironment.DEV_ENVIRONMENT_PHP_VERSIONS).join(', ');
623
627
  function addDevEnvConfigurationOptions(command) {
624
628
  // We leave the third parameter to undefined on some because the defaults are handled in preProcessInstanceData()
625
- return command.option('wordpress', 'Use a specific WordPress version', undefined, processVersionOption).option(['u', 'mu-plugins'], 'Use a specific mu-plugins changeset or local directory').option('app-code', 'Use the application code from a local directory or use "demo" for VIP skeleton code').option('phpmyadmin', 'Enable PHPMyAdmin component. By default it is disabled', undefined, processBooleanOption).option('xdebug', 'Enable XDebug. By default it is disabled', undefined, processBooleanOption).option('xdebug_config', 'Extra configuration to pass to xdebug via XDEBUG_CONFIG environment variable').option('elasticsearch', 'Enable Elasticsearch (needed by Enterprise Search)', undefined, processBooleanOption).option(['r', 'media-redirect-domain'], 'Domain to redirect for missing media files. This can be used to still have images without the need to import them locally.', undefined, processMediaRedirectDomainOption).option('php', 'Explicitly choose PHP version to use', undefined, processVersionOption).option(['A', 'mailpit'], 'Enable Mailpit. By default it is disabled', undefined, processBooleanOption).option(['H', 'photon'], 'Enable Photon. By default it is disabled', undefined, processBooleanOption);
629
+ return command.option('wordpress', 'Manage the version of WordPress. Accepts a string value for major versions (6.x). Defaults to the most recent version of WordPress.', undefined, processVersionOption).option(['u', 'mu-plugins'], 'Manage the source for VIP MU plugins. Accepts "demo" (default) for a read-only image of the staging branch, or a path to a built copy of VIP MU plugins on the local machine.').option('app-code', 'Manage the source for application code. Accepts "demo" (default) for a read-only image of WordPress VIP skeleton application code, or a path to a VIP formatted application repo on the local machine.').option('phpmyadmin', 'Enable or disable phpMyAdmin, disabled by default. Accepts "y" (default value) to enable or "n" to disable. When enabled, refer to the value of "PHPMYADMIN URLS" in the information output for a local environment for the URL to access phpMyAdmin.', undefined, processBooleanOption).option('xdebug', 'Enable or disable XDebug, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option('xdebug_config', 'Override some default configuration settings for Xdebug. Accepts a string value that is assigned to the XDEBUG_CONFIG environment variable.').option('elasticsearch', 'Enable or disable Elasticsearch (required by Enterprise Search), disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['r', 'media-redirect-domain'], 'Configure media files to be proxied from a VIP Platform environment. Accepts a string value for the primary domain of the VIP Platform environment or "n" to disable the media proxy.', undefined, processMediaRedirectDomainOption).option('php', `Manage the version of PHP. Accepts a string value for minor versions: ${phpVersionsSupported}`, undefined, processVersionOption).option(['A', 'mailpit'], 'Enable or disable Mailpit, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['H', 'photon'], 'Enable or disable Photon, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption);
626
630
  }
627
631
 
628
632
  /**
@@ -577,7 +577,7 @@ async function maybeUpdateWordPressImage(lando, slug) {
577
577
  type: 'select',
578
578
  name: 'upgrade',
579
579
  message: 'Would you like to upgrade WordPress? ',
580
- choices: ['no', 'yes', "no (don't ask anymore)"]
580
+ choices: ['yes', 'no', "no (don't ask anymore)"]
581
581
  });
582
582
 
583
583
  // If the user takes the new WP version path
@@ -585,7 +585,7 @@ async function maybeUpdateWordPressImage(lando, slug) {
585
585
  console.log('Upgrading from: ' + _chalk.default.yellow(currentWordPressTag) + ' to:');
586
586
 
587
587
  // Select a new image
588
- const choice = await (0, _devEnvironmentCli.promptForWordPress)(null);
588
+ const choice = await (0, _devEnvironmentCli.promptForWordPress)(newestWordPressImage?.tag ?? null);
589
589
  const version = versions.find(({
590
590
  tag
591
591
  }) => tag.trim() === choice.tag.trim());
@@ -611,6 +611,7 @@ async function maybeUpdateVersion(lando, slug) {
611
611
  const currentVersion = envData.version;
612
612
  console.log('Current local environment version is: ' + _chalk.default.yellow(currentVersion));
613
613
  if (!currentVersion || _semver.default.lt(currentVersion, _devEnvironment.DEV_ENVIRONMENT_VERSION)) {
614
+ envData.pullAfter = undefined;
614
615
  await updateEnvironment(lando, envData);
615
616
  console.log('Local environment version updated to: ' + _chalk.default.green(_devEnvironment.DEV_ENVIRONMENT_VERSION));
616
617
  return true;
@@ -180,13 +180,11 @@ async function bootstrapLando() {
180
180
  registryResolvable = false;
181
181
  }
182
182
  const pull = registryResolvable && (instanceData.pullAfter ?? 0) < Date.now();
183
- console.log(pull);
184
183
  if (Array.isArray(data.opts.pullable) && Array.isArray(data.opts.local) && data.opts.local.length === 0 && !pull) {
185
184
  // Setting `data.opts.pullable` to an empty array prevents Lando from pulling images with `docker pull`.
186
185
  // Note that if some of the images are not available, they will still be pulled by `docker-compose`.
187
186
  data.opts.local = data.opts.pullable;
188
187
  data.opts.pullable = [];
189
- console.log(data.opts);
190
188
  }
191
189
  if (pull || !instanceData.pullAfter) {
192
190
  instanceData.pullAfter = Date.now() + 7 * 24 * 60 * 60 * 1000;
@@ -406,7 +404,8 @@ async function checkEnvHealth(lando, instancePath) {
406
404
  }
407
405
  if (urlsToScan.length) {
408
406
  scanResults = scanResults.concat(await app.scanUrls(urlsToScan, {
409
- max: 1
407
+ max: 1,
408
+ waitCodes: [502, 504]
410
409
  }));
411
410
  }
412
411
  const result = {};
@@ -3,11 +3,27 @@
3
3
  exports.__esModule = true;
4
4
  exports.validateDeployFileExt = validateDeployFileExt;
5
5
  exports.validateFilename = validateFilename;
6
+ exports.validateName = validateName;
7
+ exports.validateTarFile = validateTarFile;
8
+ exports.validateZipFile = validateZipFile;
9
+ var _admZip = _interopRequireDefault(require("adm-zip"));
10
+ var _nodeFs = require("node:fs");
6
11
  var _path = _interopRequireDefault(require("path"));
12
+ var tar = _interopRequireWildcard(require("tar"));
7
13
  var exit = _interopRequireWildcard(require("../../lib/cli/exit"));
8
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
9
15
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
10
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
+ const errorMessages = {
18
+ missingThemes: 'Missing `themes` directory from root folder!',
19
+ symlink: 'Symlink detected: ',
20
+ singleRootDir: 'The compressed file must contain a single root directory!',
21
+ invalidExt: 'Invalid file extension. Please provide a .zip, .tar.gz, or a .tgz file.',
22
+ invalidChars: (filename, invalidChars) => `Filename ${filename} contains disallowed characters: ${invalidChars}`
23
+ };
24
+ const symlinkIgnorePattern = /\/node_modules\/[^/]+\/\.bin\//;
25
+ const macosxDir = '__MACOSX';
26
+
11
27
  /**
12
28
  * Check if a file has a valid extension
13
29
  *
@@ -20,7 +36,7 @@ function validateDeployFileExt(filename) {
20
36
  ext = '.tar.gz';
21
37
  }
22
38
  if (!['.zip', '.tar.gz', '.tgz'].includes(ext)) {
23
- exit.withError('Invalid file extension. Please provide a .zip, .tar.gz, or a .tgz file.');
39
+ exit.withError(errorMessages.invalidExt);
24
40
  }
25
41
  }
26
42
 
@@ -31,9 +47,183 @@ function validateDeployFileExt(filename) {
31
47
  */
32
48
  function validateFilename(filename) {
33
49
  const re = /^[a-z0-9\-_.]+$/i;
34
-
35
- // Exits if filename contains anything outside a-z A-Z - _ .
36
50
  if (!re.test(filename)) {
37
- exit.withError('Error: The characters used in the name of a file for custom deploys are limited to [0-9,a-z,A-Z,-,_,.]');
51
+ exit.withError(errorMessages.invalidChars(filename, '[0-9,a-z,A-Z,-,_,.]'));
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate the name of a file for disallowed characters
57
+ *
58
+ * @param {string} name The name of the file
59
+ * @param {bool} isDirectory Whether the file is a directory
60
+ */
61
+ function validateName(name, isDirectory) {
62
+ if (name.startsWith('._')) {
63
+ return;
64
+ }
65
+ const invalidCharsPattern = isDirectory ? /[!:*?"<>|']|^\.\..*$/ : /[!/:*?"<>|']|^\.\..*$/;
66
+ const errorMessage = errorMessages.invalidChars(name, isDirectory ? '[!:*?"<>|\'/^..]+' : '[!/:*?"<>|\'/^..]+');
67
+ if (invalidCharsPattern.test(name)) {
68
+ exit.withError(errorMessage);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate the existence of a symlink in a zip file. Ignores symlinks in node_modules/.bin/
74
+ *
75
+ * @param {IZipEntry} entry The zip entry to validate
76
+ */
77
+ function validateZipSymlink(entry) {
78
+ if (symlinkIgnorePattern.test(entry.entryName)) {
79
+ return;
80
+ }
81
+ const madeBy = entry.header.made >> 8; // eslint-disable-line no-bitwise
82
+ const errorMsg = errorMessages.symlink + entry.name;
83
+
84
+ // DOS
85
+ /* eslint-disable no-bitwise */
86
+ if (madeBy === 0 && (entry.attr & 0x0400) === 0x0400) {
87
+ exit.withError(errorMsg);
88
+ }
89
+
90
+ // Unix
91
+ if (madeBy === 3 && (entry.attr >>> 16 & _nodeFs.constants.S_IFLNK) === _nodeFs.constants.S_IFLNK) {
92
+ /* eslint-enable no-bitwise */
93
+ exit.withError(errorMsg);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Validate a zip entry for disallowed characters and symlinks.
99
+ * Ignores __MACOSX directories.
100
+ *
101
+ * @param {IZipEntry} entry The zip entry to validate
102
+ */
103
+ function validateZipEntry(entry) {
104
+ if (entry.entryName.startsWith(macosxDir)) {
105
+ return;
106
+ }
107
+ validateName(entry.isDirectory ? entry.entryName : entry.name, entry.isDirectory);
108
+ validateZipSymlink(entry);
109
+ }
110
+
111
+ /**
112
+ * Validate the existence of a themes directory in the root folder.
113
+ *
114
+ * @param {IZipEntry[]} zipEntries The zip entries to validate
115
+ */
116
+ function validateZipThemes(rootFolder, zipEntries) {
117
+ const hasThemesDir = zipEntries.some(entry => entry.isDirectory && entry.entryName.startsWith(_path.default.join(rootFolder, 'themes/')));
118
+ if (!hasThemesDir) {
119
+ exit.withError(errorMessages.missingThemes);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Validate a zip file for Custom Deployments.
125
+ *
126
+ * @param {string} filePath The path to the zip file
127
+ */
128
+ function validateZipFile(filePath) {
129
+ try {
130
+ const zipFile = new _admZip.default(filePath);
131
+ const zipEntries = zipFile.getEntries();
132
+ const rootDirs = zipEntries.filter(entry => entry.isDirectory && !entry.entryName.startsWith(macosxDir) && (entry.entryName.match(/\//g) || []).length === 1);
133
+ if (rootDirs.length !== 1) {
134
+ exit.withError(errorMessages.singleRootDir);
135
+ }
136
+ const rootFolder = rootDirs[0].entryName;
137
+ validateZipThemes(rootFolder, zipEntries);
138
+ zipEntries.forEach(entry => validateZipEntry(entry));
139
+ } catch (error) {
140
+ const err = error;
141
+ exit.withError(`Error reading file: ${err.message}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Validate the existence of a themes directory in the root folder in a tar file.
147
+ *
148
+ * @param {string} rootFolder The root folder of the tar file
149
+ * @param {TarEntry[]} tarEntries The list of tar entries
150
+ */
151
+ function validateTarThemes(rootFolder, tarEntries) {
152
+ const themesFolderPath = _path.default.join(rootFolder, 'themes/');
153
+ const themesFolderExists = tarEntries.some(entry => entry.path === themesFolderPath && entry.type === 'Directory');
154
+ if (!themesFolderExists) {
155
+ exit.withError(errorMessages.missingThemes);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Validate a tar entry for disallowed characters and symlinks.
161
+ *
162
+ * @param {TarEntry} entry The tar entry to validate
163
+ */
164
+ function validateTarEntry(entry) {
165
+ if (entry.path.startsWith(macosxDir)) {
166
+ return;
167
+ }
168
+ validateTarSymlink(entry);
169
+ validateName(_path.default.basename(entry.path), entry.type === 'Directory');
170
+ }
171
+
172
+ /**
173
+ * Validate the existence of a symlink in a tar file. Ignores symlinks in node_modules/.bin/
174
+ *
175
+ * @param {TarEntry} entry The tar entry to validate for symlinks
176
+ */
177
+ function validateTarSymlink(entry) {
178
+ if (symlinkIgnorePattern.test(entry.path)) {
179
+ return;
180
+ }
181
+ if (entry.type === 'SymbolicLink') {
182
+ exit.withError(errorMessages.symlink + entry.path);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Validate a tar file for Custom Deployments.
188
+ *
189
+ * @param {string} filePath The path to the tar file
190
+ */
191
+ async function validateTarFile(filePath) {
192
+ const tarEntries = [];
193
+ let rootFolder = null;
194
+ try {
195
+ await tar.list({
196
+ file: filePath,
197
+ onReadEntry: entry => {
198
+ if (entry.path.startsWith(macosxDir)) {
199
+ return;
200
+ }
201
+ if (entry.type !== 'File' && entry.type !== 'Directory' && entry.type !== 'SymbolicLink') {
202
+ return;
203
+ }
204
+ const isRootFolder = entry.type === 'Directory' && entry.path.endsWith('/') && (entry.path.match(/\//g) || []).length === 1;
205
+ if (isRootFolder) {
206
+ if (rootFolder === null) {
207
+ rootFolder = entry.path;
208
+ } else if (rootFolder !== entry.path) {
209
+ exit.withError(errorMessages.singleRootDir);
210
+ }
211
+ }
212
+ const entryInfo = {
213
+ path: entry.path,
214
+ type: entry.type,
215
+ mode: entry.mode
216
+ };
217
+ validateTarEntry(entryInfo);
218
+ tarEntries.push(entryInfo);
219
+ }
220
+ });
221
+ if (!rootFolder) {
222
+ exit.withError(errorMessages.singleRootDir);
223
+ }
224
+ validateTarThemes(rootFolder, tarEntries);
225
+ } catch (error) {
226
+ const err = error;
227
+ exit.withError(err.message);
38
228
  }
39
229
  }
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## Changelog
2
2
 
3
+ ### 3.4.1
4
+
5
+ * Updating vip cache commands descriptions and examples to follow the VIP-CLI style guide
6
+ * Dev-env: Upgrade WordPress upgrade prompt to "yes" and pre-select latest version not trunk
7
+ * build(deps-dev): bump @types/node from 18.19.36 to 18.19.37
8
+ * fix(dev-env): retry healthcheck only on 502 and 504 status codes
9
+ * fix(dev-env): set the user/group for `nginx` container to `nginx:nginx`
10
+
11
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.4.0...3.4.1
12
+
13
+ ### 3.4.0
14
+
15
+ * Updating command option descriptions to follow the VIP-CLI style guide
16
+ * build(deps-dev): bump @types/node from 18.19.34 to 18.19.36
17
+ * Add `vip app deploy validate` command for Custom Deployments
18
+ * fix(dev-env): remove debug code
19
+ * fix(dev-env): force pull images on environment version update
20
+ * chore(deps): update `ws` to fix CVE-2024-37890
21
+
22
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.3.1...3.4.0
23
+
3
24
  ### 3.3.1
4
25
 
5
26
  * build(deps): bump step-security/harden-runner from 2.8.0 to 2.8.1