@webship.co/workspace-drupal-cms 0.0.0-stage → 1.0.7

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.
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Drupal CMS for the Webship Workspace: build a Drupal CMS site from a site template, apply add-on
4
+ # recipes, run Drush on it, add its default accounts, and wire its step pack into the testing setup.
5
+ #
6
+ # Placed by `plugins.sh --install drupal-cms` in core/scripts/plugins/drupal-cms/functions/, where
7
+ # bootstrap sources it after the core's own functions. Everything runs in the project's container
8
+ # (DDEV by default: `ddev composer …`, `ddev drush …`), never on the host. The container tool
9
+ # writes the database settings (settings.ddev.php), so nothing here writes a $databases block.
10
+ #
11
+ # The plugin is the whole of the platform: it builds with its own functions (drupal_cms_*) and
12
+ # calls only the workspace's general helpers (trim_project_name, drop_database, ddev_up,
13
+ # ddev_nodejs_version). It reads none of the Drupal functions the workspace base still carries,
14
+ # and none of the Drupal plugin's: either plugin works without the other.
15
+ #
16
+ # Four names are the ones the core's tools look for, under the name `drupal_cms`:
17
+ # set_drupal_cms_users the default accounts (cmd-tools-add-users.sh <project> drupal_cms)
18
+ # add_drupal_cms_users create them with Drush (the same tool, and --add-users of the builder)
19
+ # cancel_drupal_cms_users delete them (cmd-tools-cancel-users.sh <project> drupal_cms)
20
+ # testing_configure_drupal_cms the step pack in the testing stack (cmd-tools-testing.sh <project>)
21
+
22
+ # The step pack a Drupal CMS project is tested with, the core pack it loads by itself, and the
23
+ # release line both are required at.
24
+ readonly DRUPAL_CMS_STEPS_PACKAGE='@webship-js/drupal-cms-steps';
25
+ readonly DRUPAL_CMS_STEPS_VERSION='^2.2.0';
26
+ readonly DRUPAL_CMS_CORE_STEPS_PACKAGE='@webship-js/drupal-core-steps';
27
+ readonly DRUPAL_CMS_CORE_STEPS_VERSION='^2.2.0';
28
+
29
+ # The project template every Drupal CMS site is created from.
30
+ readonly DRUPAL_CMS_PROJECT_TEMPLATE='drupal/cms';
31
+
32
+ # The default accounts: an administrator, a content editor (the role Drupal CMS creates, with no
33
+ # account of its own) and an account with no role, the three a scenario logs in as. Accounts are
34
+ # configuration, created once on the site; a test suite never creates or deletes one. `_none_` is
35
+ # an account with no role.
36
+ function drupal_cms_default_users() {
37
+ users=(administrator content_editor authenticated);
38
+ user_administrator_name="administrator";
39
+ user_administrator_mail="administrator@example.com";
40
+ user_administrator_password="dD.123123ddd";
41
+ user_administrator_role="administrator";
42
+ user_content_editor_name="content_editor";
43
+ user_content_editor_mail="content_editor@example.com";
44
+ user_content_editor_password="dD.123123ddd";
45
+ user_content_editor_role="content_editor";
46
+ user_authenticated_name="authenticated";
47
+ user_authenticated_mail="authenticated@example.com";
48
+ user_authenticated_password="dD.123123ddd";
49
+ user_authenticated_role="_none_";
50
+ }
51
+
52
+ # The name the core's tools ask for the list by.
53
+ function set_drupal_cms_users() {
54
+ drupal_cms_default_users ;
55
+ }
56
+
57
+ # Is this folder a Drupal CMS project? Its DDEV project type is Drupal's and its composer.json
58
+ # carries the Drupal CMS packages.
59
+ function drupal_cms_is_project() {
60
+ local project_path="$1";
61
+ grep -qE '^type:[[:space:]]*"?drupal[0-9]*"?[[:space:]]*$' "${project_path}/.ddev/config.yaml" 2>/dev/null || return 1 ;
62
+ grep -qE '"drupal/(cms|drupal_cms_[a-z0-9_]+)"' "${project_path}/composer.json" 2>/dev/null ;
63
+ }
64
+
65
+ # Go to the project a tool was asked about, or say why not.
66
+ function drupal_cms_enter_project() {
67
+ local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}";
68
+ if [ ! -d "${project_path}" ] ; then
69
+ echo "No project ${PROJECT_NAME} in ${WORKSPACE_ROOT}/${doc_name}." ;
70
+ return 1 ;
71
+ fi
72
+ if ! drupal_cms_is_project "${project_path}" ; then
73
+ echo "${doc_name}/${PROJECT_NAME} is not a Drupal CMS project (its composer.json does not carry the Drupal CMS packages)." ;
74
+ return 1 ;
75
+ fi
76
+ cd "${project_path}" || return 1 ;
77
+ }
78
+
79
+ # What a version asks for: `2.x` (or `2`) is the latest release of that line, `2.2.9` is that
80
+ # release, `2.x-dev` is the development branch. Sets drupal_cms_constraint (what composer
81
+ # create-project is given) and drupal_cms_stability ('' is composer's own: stable).
82
+ function drupal_cms_resolve_version() {
83
+ local version="${1:-2.x}" ;
84
+ drupal_cms_constraint="" ; drupal_cms_stability="" ;
85
+ if [[ "${version}" =~ ^([0-9]+)(\.x)?$ ]] ; then
86
+ drupal_cms_constraint="^${BASH_REMATCH[1]}" ;
87
+ elif [[ "${version}" =~ ^[0-9]+\.x-dev$ ]] ; then
88
+ drupal_cms_constraint="${version}" ;
89
+ drupal_cms_stability="dev" ;
90
+ elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]*)?$ ]] ; then
91
+ drupal_cms_constraint="${version}" ;
92
+ drupal_cms_stability="${BASH_REMATCH[2]}" ;
93
+ [ "${drupal_cms_stability}" == 'rc' ] && drupal_cms_stability="RC" ;
94
+ else
95
+ echo "--version ${version} is not a version this builder reads: a release line (2.x), a release (2.2.9) or a development branch (2.x-dev)." ;
96
+ return 1 ;
97
+ fi
98
+ return 0 ;
99
+ }
100
+
101
+ # The add-on recipe a short name stands for: `forms` is drupal_cms_forms, `seo_tools` is
102
+ # drupal_cms_seo_tools. A name that already says drupal_cms_ is taken as it is.
103
+ function drupal_cms_recipe_name() {
104
+ local name="$1" ;
105
+ case "${name}" in
106
+ drupal_cms_*) printf '%s' "${name}" ;;
107
+ *) printf 'drupal_cms_%s' "${name}" ;;
108
+ esac
109
+ }
110
+
111
+ # The site template a name stands for: `starter` and `blank` are the two Drupal CMS ships or
112
+ # names itself (drupal_cms_starter, drupal_cms_blank); any other name is the recipe folder of
113
+ # that template, as it is.
114
+ function drupal_cms_template_name() {
115
+ local name="$1" ;
116
+ case "${name}" in
117
+ starter|blank) printf 'drupal_cms_%s' "${name}" ;;
118
+ *) printf '%s' "${name}" ;;
119
+ esac
120
+ }
121
+
122
+ # The recipes --recipes asked for, as one name per word: "forms,search" is
123
+ # "drupal_cms_forms drupal_cms_search". Nothing for the default.
124
+ function drupal_cms_recipe_list() {
125
+ local given="$1" name list="" ;
126
+ [ "${given}" == '_none_' ] && return 0 ;
127
+ for name in ${given//,/ } ; do
128
+ list="${list}${list:+ }$(drupal_cms_recipe_name "${name}")" ;
129
+ done
130
+ printf '%s' "${list}" ;
131
+ }
132
+
133
+ # composer install, once more if it fails: the first install of a fresh project can fail on a
134
+ # network or cache hiccup and pass on the second try. A real dependency problem fails both times.
135
+ function drupal_cms_composer_install_retry() {
136
+ ddev composer install --no-interaction && return 0 ;
137
+ echo "composer install failed once, trying it again before giving up." ;
138
+ ddev composer install --no-interaction ;
139
+ }
140
+
141
+ # composer create-project in the current project folder, which holds only its .ddev directory.
142
+ function drupal_cms_composer_create_project() {
143
+ local package="$1" stability="$2" ;
144
+ if ddev composer create-project "${package}" --no-interaction ${stability:+--stability=${stability}} ; then
145
+ return 0 ;
146
+ fi
147
+ # When the project reached the install step, composer.json is already there: install once more.
148
+ if [ -f composer.json ] && drupal_cms_composer_install_retry ; then
149
+ echo "composer create-project needed a second install, which worked." ;
150
+ return 0 ;
151
+ fi
152
+ return 1 ;
153
+ }
154
+
155
+ # The project template ships a .ddev/config.yaml of its own, which replaces the one the build
156
+ # started with: no project name, a database of its choice, sometimes a container tool newer than
157
+ # the one on this machine. Put the project's own values back, keep the database the build already
158
+ # started, and start again.
159
+ function drupal_cms_align_container() {
160
+ local database="$1" php_version="$2" ;
161
+ local settings=(--project-name=${PROJECT_NAME} --project-type=drupal --docroot=web --nodejs-version=$(ddev_nodejs_version)) ;
162
+ [ -n "${database}" ] && settings+=(--database=${database}) ;
163
+ [ -n "${php_version}" ] && settings+=(--php-version=${php_version}) ;
164
+ if ! ddev config "${settings[@]}" ; then
165
+ if grep -q '^ddev_version_constraint:' .ddev/config.yaml 2>/dev/null ; then
166
+ echo "The project template asks for a newer DDEV than this machine has ($(grep '^ddev_version_constraint:' .ddev/config.yaml))." ;
167
+ echo "The line is taken out so this DDEV runs the project. Update DDEV when you can." ;
168
+ sed -i '/^ddev_version_constraint:/d' .ddev/config.yaml ;
169
+ ddev config "${settings[@]}" || return 1 ;
170
+ else
171
+ return 1 ;
172
+ fi
173
+ fi
174
+ drupal_cms_telemetry_off ;
175
+ ddev_up restart ;
176
+ }
177
+
178
+ # A site built here is a local one: keep it out of the install statistics Drupal CMS reports.
179
+ function drupal_cms_telemetry_off() {
180
+ ddev config --web-environment-add=DRUPAL_CMS_TELEMETRY=0 > /dev/null ;
181
+ }
182
+
183
+ # `ddev drush` answering. DDEV rewrites its shared command directory whenever any project on the
184
+ # machine starts, and `ddev drush` is missing from it for a moment: wait for it, instead of
185
+ # failing the install on its first line. Drupal CMS brings Drush with it; an older release that
186
+ # does not gets it here.
187
+ function drupal_cms_wait_for_drush() {
188
+ local attempt ;
189
+ if [ ! -d vendor/drush/drush ] ; then
190
+ ddev composer require drush/drush --no-interaction || return 1 ;
191
+ fi
192
+ for attempt in 1 2 3 4 5 6 7 8 9 10 ; do
193
+ ddev drush --version > /dev/null 2>&1 && return 0 ;
194
+ echo "ddev drush is not available yet (attempt ${attempt} of 10), waiting." ;
195
+ sleep 6 ;
196
+ done
197
+ echo "ddev drush is still not available." ;
198
+ return 1 ;
199
+ }
200
+
201
+ # composer require, once more if it fails: a site template brings some sixty packages, and one
202
+ # download that times out fails the whole command. A package that does not exist fails both times.
203
+ function drupal_cms_composer_require_retry() {
204
+ ddev composer require "$@" --with-all-dependencies --no-interaction && return 0 ;
205
+ echo "composer require failed once, trying it again before giving up." ;
206
+ sleep 20 ;
207
+ ddev composer require "$@" --with-all-dependencies --no-interaction ;
208
+ }
209
+
210
+ # A recipe in recipes/, required with composer when it is not there yet. The project template
211
+ # brings the installer and no site template, so the Starter template is required here too. The
212
+ # package is drupal/<recipe> unless one is named.
213
+ function drupal_cms_require_recipe() {
214
+ local recipe="$1" package="${2:-}" ;
215
+ # What a package ships is unpacked into recipes/ by composer; asking again is harmless.
216
+ ddev composer drupal:recipe-unpack --no-interaction > /dev/null 2>&1 || true ;
217
+ [ -d "recipes/${recipe}" ] && return 0 ;
218
+ package="${package:-drupal/${recipe}}" ;
219
+ echo "The ${recipe} recipe is not in recipes/: requiring ${package}." ;
220
+ drupal_cms_composer_require_retry "${package}" || return 1 ;
221
+ ddev composer drupal:recipe-unpack --no-interaction > /dev/null 2>&1 || true ;
222
+ if [ ! -d "recipes/${recipe}" ] ; then
223
+ echo "The ${recipe} recipe is still not in recipes/ after requiring ${package}." ;
224
+ return 1 ;
225
+ fi
226
+ }
227
+
228
+ # Apply add-on recipes to an installed site, one after the other.
229
+ function drupal_cms_apply_recipes() {
230
+ local recipe ;
231
+ for recipe in "$@" ; do
232
+ drupal_cms_require_recipe "${recipe}" || return 1 ;
233
+ echo "Applying the ${recipe} recipe." ;
234
+ ddev drush cache:rebuild ;
235
+ ddev drush recipe "/var/www/html/recipes/${recipe}" || { echo "The ${recipe} recipe could not be applied." ; return 1 ; } ;
236
+ done
237
+ }
238
+
239
+ # What the site was built from, kept where the test suite reads it: the environment of the
240
+ # project's container (DRUPAL_SITE_TEMPLATE, DRUPAL_CMS_RECIPES). The recipes are separated by
241
+ # spaces there, because the container tool splits its own list on commas.
242
+ function drupal_cms_record_build() {
243
+ local template="$1" recipes="$2" ;
244
+ [ -n "${template}" ] && ddev config --web-environment-add="DRUPAL_SITE_TEMPLATE=${template}" > /dev/null ;
245
+ [ -n "${recipes}" ] && ddev config --web-environment-add="DRUPAL_CMS_RECIPES=${recipes}" > /dev/null ;
246
+ return 0 ;
247
+ }
248
+
249
+ # The recipes recorded for the project, one per word.
250
+ function drupal_cms_recorded_recipes() {
251
+ local project_path="$1" ;
252
+ grep -E '^[[:space:]]*-[[:space:]]*"?DRUPAL_CMS_RECIPES=' "${project_path}/.ddev/config.yaml" 2>/dev/null | head -1 \
253
+ | sed -E 's/^[[:space:]]*-[[:space:]]*"?DRUPAL_CMS_RECIPES=//; s/"?[[:space:]]*$//' ;
254
+ }
255
+
256
+ # The first account of the site is the workspace account. The platform's installer names it and
257
+ # sets its password by itself on some releases, whatever the install command was given, so the
258
+ # name, the password and the address are set again here. They are handed over on standard input,
259
+ # so no character in them is read as code.
260
+ function drupal_cms_set_owner() {
261
+ printf '%s\n%s\n%s' "${account_name}" "${account_pass}" "${account_mail}" | ddev drush php:eval '
262
+ $given = explode("\n", stream_get_contents(STDIN));
263
+ $owner = \Drupal\user\Entity\User::load(1);
264
+ if ($owner && count($given) === 3 && $given[0] !== "") {
265
+ $owner->setUsername($given[0])->setPassword($given[1])->setEmail($given[2])->save();
266
+ print "The first account of the site is " . $owner->getAccountName() . ".\n";
267
+ }' ;
268
+ }
269
+
270
+ # Is the site installed and answering Drush?
271
+ function drupal_cms_is_installed() {
272
+ ddev drush status --field=bootstrap 2>/dev/null | grep -qi successful ;
273
+ }
274
+
275
+ # Does a role exist on the site?
276
+ function drupal_cms_role_exists() {
277
+ ddev drush config:get "user.role.$1" id > /dev/null 2>&1 ;
278
+ }
279
+
280
+ # Does an account exist on the site?
281
+ function drupal_cms_user_exists() {
282
+ ddev drush user:information "$1" > /dev/null 2>&1 ;
283
+ }
284
+
285
+ # Build a Drupal CMS site from nothing: the container, the code, and with --install the site
286
+ # itself, from the site template and with the add-on recipes that were asked for.
287
+ function drupal_cms_build() {
288
+ trim_project_name ;
289
+ local base_url="https://${PROJECT_NAME}.ddev.site" ;
290
+ local version="${VERSION:-${site_version:-2.x}}" ;
291
+ local php_version="${PHP_VERSION:-}" ;
292
+
293
+ drupal_cms_resolve_version "${version}" || exit 1 ;
294
+ local package="${DRUPAL_CMS_PROJECT_TEMPLATE}:${drupal_cms_constraint}" ;
295
+ local template ; template="$(drupal_cms_template_name "${TEMPLATE:-starter}")" ;
296
+ local template_package="${TEMPLATE_PACKAGE:-}" ;
297
+ local recipes ; recipes="$(drupal_cms_recipe_list "${RECIPES:-_none_}")" ;
298
+
299
+ echo "Drupal CMS for ${doc_name}/${PROJECT_NAME}:" ;
300
+ echo " code ${package}${drupal_cms_stability:+ (stability ${drupal_cms_stability})}" ;
301
+ echo " container project type drupal, PHP ${php_version:-the one the project template names}, Node.js $(ddev_nodejs_version)" ;
302
+ echo " site template ${template}${template_package:+ (from ${template_package})}" ;
303
+ echo " recipes ${recipes:-none}" ;
304
+ echo " install $([ "${INSTALL}" == 'yes' ] && echo 'yes' || echo 'no')$([ "${ADD_USERS}" == 'yes' ] && echo ', with the default accounts')" ;
305
+ echo " address ${base_url}" ;
306
+ if [ "${DRY_RUN}" == 'yes' ] ; then
307
+ echo "A dry run: nothing was built." ;
308
+ return 0 ;
309
+ fi
310
+
311
+ cd ${WORKSPACE_ROOT}/${doc_name} || exit 1 ;
312
+
313
+ drop_database ;
314
+ rm -rf "${PROJECT_NAME}" ;
315
+ mkdir -p "${PROJECT_NAME}" ;
316
+ cd ${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME} || exit 1 ;
317
+
318
+ ddev config --project-type=drupal --docroot=web --project-name=${PROJECT_NAME} \
319
+ --nodejs-version=$(ddev_nodejs_version) ${php_version:+--php-version=${php_version}} --auto || { echo "The container could not be configured." ; exit 1 ; } ;
320
+ drupal_cms_telemetry_off ;
321
+ ddev_up start -y || exit 1 ;
322
+
323
+ # The database the container started with, before the project template can name another.
324
+ local database ;
325
+ database=$(ddev describe -j 2>/dev/null | python3 -c 'import json, sys
326
+ try:
327
+ raw = json.load(sys.stdin)["raw"]
328
+ print("%s:%s" % (raw["database_type"], raw["database_version"]))
329
+ except Exception:
330
+ pass' 2>/dev/null) ;
331
+
332
+ if ! drupal_cms_composer_create_project "${package}" "${drupal_cms_stability}" ; then
333
+ echo "composer create-project failed for ${package}: the build stops here, nothing was installed." ;
334
+ exit 1 ;
335
+ fi
336
+
337
+ drupal_cms_align_container "${database}" "${php_version}" || { echo "The container could not be started again with the project template's configuration." ; exit 1 ; } ;
338
+
339
+ if [ "${INSTALL}" == 'yes' ] ; then
340
+ drupal_cms_wait_for_drush || { echo "Drush does not answer: the build stops here." ; exit 1 ; } ;
341
+ drupal_cms_require_recipe "${template}" "${template_package}" || { echo "The site template is not in the code: the build stops here." ; exit 1 ; } ;
342
+
343
+ # The installer applies the site template: the form value is the folder name of its recipe.
344
+ if ! ddev drush site:install drupal_cms_installer "installer_site_template_form.add_ons=${template}" --yes \
345
+ --site-name="${doc_name} ${PROJECT_NAME}" \
346
+ --account-name="${account_name}" --account-pass="${account_pass}" --account-mail="${account_mail}" ; then
347
+ echo "The install of ${doc_name} ${PROJECT_NAME} failed: drush site:install did not finish." ;
348
+ echo "The project is kept for a look: ${base_url} (ddev logs, ddev drush status)." ;
349
+ exit 1 ;
350
+ fi
351
+ drupal_cms_set_owner ;
352
+ if ! drupal_cms_is_installed ; then
353
+ echo "${doc_name} ${PROJECT_NAME} was installed, but the site does not bootstrap. Run: ddev drush status ; ddev logs." ;
354
+ exit 1 ;
355
+ fi
356
+
357
+ if [ -n "${recipes}" ] ; then
358
+ drupal_cms_apply_recipes ${recipes} || { echo "The site is installed; a recipe was not applied. Apply it again with cmd-tools-drupal-cms-cli.sh ${PROJECT_NAME} --recipe <name>." ; exit 1 ; } ;
359
+ fi
360
+ drupal_cms_record_build "${template}" "${recipes}" ;
361
+
362
+ if [ "${ADD_USERS}" == 'yes' ] ; then
363
+ add_drupal_cms_users ;
364
+ cd ${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME} || exit 1 ;
365
+ fi
366
+ ddev drush cache:rebuild ;
367
+
368
+ echo "${doc_name} ${PROJECT_NAME} has been installed: Drupal CMS on Drupal $(ddev drush status --field=drupal-version 2>/dev/null), the ${template} site template${recipes:+, with ${recipes}}.";
369
+ echo "Go to ${base_url}/user/login and log in as ${account_name}";
370
+ else
371
+ [ "${ADD_USERS}" == 'yes' ] && echo "--add-users needs --install: there is no site to add accounts to yet." ;
372
+ [ -n "${recipes}" ] && echo "--recipes needs --install: a recipe is applied to an installed site." ;
373
+ echo "${doc_name} ${PROJECT_NAME} is ready to install.";
374
+ echo "Go to ${base_url}";
375
+ fi
376
+
377
+ if [ "${LAUNCH}" == 'yes' ] ; then
378
+ ddev launch ;
379
+ fi
380
+
381
+ cd ${WORKSPACE_ROOT}/${doc_name};
382
+ }
383
+
384
+ # Create the default accounts with Drush. An account that is already there is left as it is, and
385
+ # a role the site does not have is said and skipped.
386
+ #
387
+ # The list is always this plugin's own: the workspace base still carries an older list under the
388
+ # same name, and its tools load that one just before they call this function.
389
+ function add_drupal_cms_users() {
390
+ drupal_cms_enter_project || return 1 ;
391
+ drupal_cms_default_users ;
392
+ local user user_name user_mail user_password user_role ;
393
+ for user in "${users[@]}" ; do
394
+ user_name="user_${user}_name"; user_mail="user_${user}_mail";
395
+ user_password="user_${user}_password"; user_role="user_${user}_role";
396
+
397
+ echo " ---------------------------------------------------------------- ";
398
+ echo " User name: ${!user_name}";
399
+ echo " User mail: ${!user_mail}";
400
+ echo " User password: ${!user_password}";
401
+ echo " User role: ${!user_role}";
402
+ echo " ================================================================= ";
403
+
404
+ if drupal_cms_user_exists "${!user_name}" ; then
405
+ echo " ${!user_name} is already there." ;
406
+ else
407
+ ddev drush user:create "${!user_name}" --mail="${!user_mail}" --password="${!user_password}" ;
408
+ fi
409
+ if [ "${!user_role}" == '_none_' ] ; then
410
+ echo " No role for this account." ;
411
+ elif drupal_cms_role_exists "${!user_role}" ; then
412
+ ddev drush user:role:add "${!user_role}" "${!user_name}" ;
413
+ else
414
+ echo " The site has no ${!user_role} role: ${!user_name} is left without one." ;
415
+ fi
416
+ done
417
+ cd ${WORKSPACE_ROOT}/${doc_name};
418
+ }
419
+
420
+ # Delete the default accounts, and what they wrote.
421
+ function cancel_drupal_cms_users() {
422
+ drupal_cms_enter_project || return 1 ;
423
+ drupal_cms_default_users ;
424
+ local user user_name ;
425
+ for user in "${users[@]}" ; do
426
+ user_name="user_${user}_name";
427
+ echo " ---------------------------------------------------------------- ";
428
+ echo " User name: ${!user_name}";
429
+ echo " ================================================================= ";
430
+ if drupal_cms_user_exists "${!user_name}" ; then
431
+ ddev drush user:cancel --delete-content "${!user_name}" --yes ;
432
+ else
433
+ echo " ${!user_name} is not there." ;
434
+ fi
435
+ done
436
+ cd ${WORKSPACE_ROOT}/${doc_name};
437
+ }
438
+
439
+ # What cmd-tools-drupal-cms-cli.sh does: the first of --run, --recipe, --update, --cache-rebuild,
440
+ # --login, --add-users, --cancel-users that was asked for; with none of them, what the site is.
441
+ function drupal_cms_cli_tool() {
442
+ trim_project_name ;
443
+ drupal_cms_enter_project || exit 1 ;
444
+ local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}" ;
445
+
446
+ if ! ddev describe -j 2>/dev/null | grep -q '"status":"running"' ; then
447
+ ddev start -y || exit 1 ;
448
+ fi
449
+
450
+ if [ ! "${RUN}" == '_none_' ] ; then
451
+ # Handed to the shell of the container as one line, so an argument in quotes stays one
452
+ # argument: --run "sql:query 'select name from users_field_data'".
453
+ ddev exec "drush ${RUN}" ;
454
+ exit $? ;
455
+ fi
456
+
457
+ if [ ! "${RECIPE}" == '_none_' ] ; then
458
+ local asked recorded recipe ;
459
+ asked="$(drupal_cms_recipe_list "${RECIPE}")" ;
460
+ drupal_cms_apply_recipes ${asked} || exit 1 ;
461
+ recorded="$(drupal_cms_recorded_recipes "${project_path}")" ;
462
+ for recipe in ${asked} ; do
463
+ case " ${recorded} " in *" ${recipe} "*) : ;; *) recorded="${recorded}${recorded:+ }${recipe}" ;; esac
464
+ done
465
+ drupal_cms_record_build "" "${recorded}" ;
466
+ ddev drush cache:rebuild ;
467
+ echo "Applied: ${asked}. The test suite reads the recipes of the site from the container: ddev restart before the next run." ;
468
+ elif [ "${UPDATE}" == 'yes' ] ; then
469
+ ddev composer update --with-all-dependencies --no-interaction || exit 1 ;
470
+ ddev drush updatedb --yes ;
471
+ ddev drush cache:rebuild ;
472
+ elif [ "${CACHE_REBUILD}" == 'yes' ] ; then
473
+ ddev drush cache:rebuild ;
474
+ elif [ "${LOGIN}" == 'yes' ] ; then
475
+ echo "A one-time login link for the site's first account:" ;
476
+ ddev drush user:login --no-browser ;
477
+ elif [ "${ADD_USERS}" == 'yes' ] ; then
478
+ add_drupal_cms_users ;
479
+ elif [ "${CANCEL_USERS}" == 'yes' ] ; then
480
+ cancel_drupal_cms_users ;
481
+ else
482
+ echo "${doc_name}/${PROJECT_NAME}:" ;
483
+ ddev drush status --fields=drupal-version,uri,install-profile,theme,admin-theme,php-version,db-driver,bootstrap ;
484
+ echo "" ; echo "Site template: $(grep -E 'DRUPAL_SITE_TEMPLATE=' .ddev/config.yaml 2>/dev/null | head -1 | sed -E 's/.*DRUPAL_SITE_TEMPLATE=//; s/"?[[:space:]]*$//')" ;
485
+ echo "Recipes applied with these tools: $(drupal_cms_recorded_recipes "${project_path}")" ;
486
+ echo "" ; echo "Recipes in the code:" ; ls recipes 2>/dev/null | tr '\n' ' ' ; echo "" ;
487
+ echo "" ; echo "Accounts:" ;
488
+ ddev drush sql:query "SELECT u.name, u.mail, COALESCE(GROUP_CONCAT(r.roles_target_id), '') AS roles FROM users_field_data u LEFT JOIN user__roles r ON r.entity_id = u.uid WHERE u.uid > 0 GROUP BY u.uid, u.name, u.mail ORDER BY u.uid" ;
489
+ echo "" ; echo "Pending database updates:" ;
490
+ ddev drush updatedb:status ;
491
+ fi
492
+ cd ${WORKSPACE_ROOT}/${doc_name};
493
+ }
494
+
495
+ # The step pack in the testing stack of a Drupal CMS project. The core's testing_configure calls
496
+ # this in the project folder once webship-js is scaffolded and before the dependencies are
497
+ # installed; for a project that is not Drupal CMS it changes nothing.
498
+ #
499
+ # It adds the pack (and the Drupal core pack it depends on) to package.json, lists its step
500
+ # definitions in cucumber.js after webship-js's own — the pack loads the core pack by itself, so
501
+ # that one is not listed — and gives cucumber.js what the pack reads: the users registry of the
502
+ # default accounts (a handle a scenario reads, mapped to an account; never the workspace account),
503
+ # the site template and the recipes of the site, the cookie consent and the third-party requests.
504
+ # It writes a first feature when the project has none of its own. Every part is skipped when it is
505
+ # already there, so a second run adds what the first missed and never resets a suite.
506
+ function testing_configure_drupal_cms() {
507
+ local project_path="$1";
508
+ drupal_cms_is_project "${project_path}" || return 0 ;
509
+ if [ ! -f "${project_path}/cucumber.js" ] || [ ! -f "${project_path}/package.json" ] ; then
510
+ echo " Drupal CMS: cucumber.js is not here yet — run the testing setup again once webship-js is scaffolded." ;
511
+ return 1 ;
512
+ fi
513
+
514
+ echo "Adding the Drupal CMS step pack (${DRUPAL_CMS_STEPS_PACKAGE} ${DRUPAL_CMS_STEPS_VERSION}) and the Drupal core pack it uses.";
515
+ drupal_cms_default_users ;
516
+ # handle | the name of its environment variables | account | password | mail | role
517
+ local registry="" user user_name user_password user_mail user_role handle env ;
518
+ for user in "${users[@]}" ; do
519
+ user_name="user_${user}_name"; user_password="user_${user}_password";
520
+ user_mail="user_${user}_mail"; user_role="user_${user}_role";
521
+ # The pack's features call the accounts "admin", "content editor" and "authenticated".
522
+ case "${user}" in
523
+ administrator) handle="admin" ; env="ADMIN" ;;
524
+ content_editor) handle="content editor" ; env="EDITOR" ;;
525
+ *) handle="${user}" ; env="${user^^}" ;;
526
+ esac
527
+ registry="${registry}${handle}|${env}|${!user_name}|${!user_password}|${!user_mail}|${!user_role}"$'\n' ;
528
+ done
529
+
530
+ python3 - "${project_path}" "${DRUPAL_CMS_STEPS_PACKAGE}" "${DRUPAL_CMS_STEPS_VERSION}" \
531
+ "${DRUPAL_CMS_CORE_STEPS_PACKAGE}" "${DRUPAL_CMS_CORE_STEPS_VERSION}" "${registry}" <<'PYEOF' || return 1
532
+ import json, re, sys
533
+
534
+ project, pack, version, core_pack, core_version, registry = sys.argv[1:7]
535
+
536
+ # package.json: the pack and the core pack it uses as dev dependencies, at the release lines this
537
+ # plugin was proven on.
538
+ pkg_file = project + '/package.json'
539
+ pkg = json.load(open(pkg_file))
540
+ changed = False
541
+ for name, wanted in ((pack, version), (core_pack, core_version)):
542
+ if name not in pkg.get('dependencies', {}) and name not in pkg.get('devDependencies', {}):
543
+ pkg.setdefault('devDependencies', {})[name] = wanted
544
+ changed = True
545
+ print(' package.json: %s %s' % (name, wanted))
546
+ if changed:
547
+ open(pkg_file, 'w').write(json.dumps(pkg, indent=2) + '\n')
548
+
549
+ # cucumber.js: the pack's step definitions after webship-js's own, and what the pack reads.
550
+ cucumber_file = project + '/cucumber.js'
551
+ src = open(cucumber_file).read()
552
+ before = src
553
+
554
+ if ('node_modules/%s/' % pack) not in src:
555
+ line = "'node_modules/%s/tests/step-definitions/**/*.js'," % pack
556
+ core = re.search(r"^([ \t]*)'node_modules/webship-js/tests/step-definitions/[^']*',.*$", src, re.M)
557
+ opening = re.search(r"^([ \t]*)require:[ \t]*\[[ \t]*$", src, re.M)
558
+ if core:
559
+ src = src[:core.end()] + '\n' + core.group(1) + line + src[core.end():]
560
+ elif opening:
561
+ src = src[:opening.end()] + '\n' + opening.group(1) + ' ' + line + src[opening.end():]
562
+ else:
563
+ sys.exit(" cucumber.js has no require list this can add to. Add this line to it:\n " + line)
564
+ print(' cucumber.js: the step definitions of %s' % pack)
565
+
566
+ world = re.search(r"^([ \t]*)worldParameters:[ \t]*\{[ \t]*$", src, re.M)
567
+ block = []
568
+ if world:
569
+ pad = world.group(1) + ' '
570
+ if not re.search(r"^[ \t]*siteTemplate:", src, re.M):
571
+ block += [
572
+ pad + "// The site template the site was installed with and the add-on recipes applied to it. The builder",
573
+ pad + "// keeps both in the environment of the project's container; a scenario that needs another",
574
+ pad + "// template or recipe is skipped.",
575
+ pad + "siteTemplate: process.env.DRUPAL_SITE_TEMPLATE || 'drupal_cms_starter',",
576
+ pad + "recipes: (process.env.DRUPAL_CMS_RECIPES || '').split(/[\\s,]+/).filter(Boolean),",
577
+ pad + "// Requests to other hosts are stopped, so a run does not wait for a third party.",
578
+ pad + "blockThirdPartyRequests: process.env.ALLOW_THIRD_PARTY_REQUESTS !== '1',",
579
+ pad + "// The cookie consent is accepted before every scenario, so the notice never covers what a",
580
+ pad + "// scenario clicks; a scenario about the consent resets it with a step.",
581
+ pad + "acceptCookieConsent: process.env.KEEP_COOKIE_CONSENT !== '1',",
582
+ ]
583
+ print(' cucumber.js: the site template, the recipes and the cookie consent')
584
+ if not re.search(r"^[ \t]*users:[ \t]*\{", src, re.M):
585
+ rows = [r.split('|') for r in registry.splitlines() if r.strip()]
586
+ block += [
587
+ pad + "// The users registry: a handle a scenario reads, mapped to an account of this site (the default",
588
+ pad + "// accounts: cmd-tools-add-users.sh). Override one with DRUPAL_<NAME>_USER, DRUPAL_<NAME>_PASS and",
589
+ pad + "// DRUPAL_<NAME>_MAIL (ADMIN, EDITOR, AUTHENTICATED).",
590
+ pad + "users: {",
591
+ ]
592
+ for handle, env, name, password, mail, role in rows:
593
+ key = handle if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', handle) else "'%s'" % handle
594
+ roles = '[]' if role == '_none_' else "['%s']" % role
595
+ block.append(pad + " %s: { username: process.env.DRUPAL_%s_USER || '%s', password: process.env.DRUPAL_%s_PASS || '%s', email: process.env.DRUPAL_%s_MAIL || '%s', roles: %s },"
596
+ % (key, env, name, env, password, env, mail, roles))
597
+ block.append(pad + "},")
598
+ print(' cucumber.js: the users registry (%s)' % ', '.join(r[0] for r in rows))
599
+ if block:
600
+ src = src[:world.end()] + '\n' + '\n'.join(block) + src[world.end():]
601
+ elif not re.search(r"^[ \t]*users:[ \t]*\{", src, re.M):
602
+ sys.exit(" cucumber.js has no worldParameters block to add the users registry to.")
603
+
604
+ if src != before:
605
+ open(cucumber_file, 'w').write(src)
606
+ PYEOF
607
+
608
+ # A first feature, when the project has none for the site yet: the front page, the login page,
609
+ # each default account, the dashboard, a Canvas page and the cookie consent.
610
+ local feature="${WORKSPACE_SCRIPTS}/plugins/drupal-cms/site.feature" ;
611
+ if [ -f "${feature}" ] && [ ! -f "${project_path}/tests/features/site.feature" ] ; then
612
+ mkdir -p "${project_path}/tests/features" && cp "${feature}" "${project_path}/tests/features/site.feature" \
613
+ && echo " tests/features/site.feature: a first feature for the site" ;
614
+ fi
615
+
616
+ # The scenarios log in as the default accounts: make sure an installed site has them.
617
+ if drupal_cms_is_installed ; then
618
+ ( add_drupal_cms_users > /dev/null ) && echo " the default accounts are on the site (${users[*]})" ;
619
+ fi
620
+ }