diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 32b5c1779..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,102 +0,0 @@ -version: 2 -aliases: - - &ssh_key_fingerprint "36:03:e3:ca:b3:0b:82:18:e2:e9:ae:5d:81:17:86:b1" - # Fingerprint of the SSH deploy key of the project used to pull code. - # The value can be found in CircleCI UI -> SSH Permissions. - - - &step_configure_git - run: - name: Configure git - command: | - git config --global user.email "$DEPLOY_USER_EMAIL" && git config --global user.name "$DEPLOY_USER_NAME" - - # Re-usable job to run different types of builds. - - &job-build - working_directory: &working-directory /app - docker: - - image: &builder-image singledigital/bay-ci-builder:5.x - environment: - INSTALL_NEW_SITE: 1 - LAGOON_ENVIRONMENT_TYPE: ci - steps: - - attach_workspace: - at: /workspace - - checkout - # Init environment for development. - - run: if [ -f "./dev-tools.sh" ] && [ ! "$DEV_TOOLS" ]; then ./dev-tools.sh; fi - - setup_remote_docker: - docker_layer_caching: true - - run: .circleci/build.sh - - run: .circleci/test.sh - - run: - name: Copy artifacts - command: .circleci/test-artifacts.sh - when: always - - store_artifacts: - path: /tmp/artifacts - - # Job to perform merge to reference branch after a merge to develop. - - &merge-to-reference - working_directory: *working-directory - docker: - - image: *builder-image - auth: - username: $DOCKERHUB_USERNAME - password: $DOCKERHUB_PASSWORD - environment: - LAGOON_ENVIRONMENT_TYPE: ci - SSH_KEY_FINGERPRINT: *ssh_key_fingerprint - DEPLOY_USER_EMAIL: sdp.devs@dpc.vic.gov.au - DEPLOY_USER_NAME: sdpdeploy - steps: - - attach_workspace: - at: /workspace - - checkout - - *step_configure_git - - setup_remote_docker: - docker_layer_caching: true - - add_ssh_keys: - fingerprints: - - *ssh_key_fingerprint - - run: - name: Merge to reference branch - command: .circleci/merge-to-reference.sh - no_output_timeout: 30m - - -jobs: - build: - <<: *job-build - - build_suggest: - <<: *job-build - docker: - - image: *builder-image - environment: - INSTALL_NEW_SITE: 1 - LAGOON_ENVIRONMENT_TYPE: ci - INSTALL_SUGGEST: 1 - BEHAT_PROFILE: "--profile=suggest" - - merge_to_reference: - <<: *merge-to-reference - - -workflows: - version: 2 - main: - jobs: - - build - - build_suggest: - filters: - branches: - ignore: - - reference - - mergetoreference: - jobs: - - merge_to_reference: - filters: - branches: - only: - - develop diff --git a/.circleci/merge-to-reference.sh b/.circleci/merge-to-reference.sh deleted file mode 100755 index db8b679ef..000000000 --- a/.circleci/merge-to-reference.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -## -# Merge develop to reference branch in CI. -# - -echo "==> Checking out reference branch" -git checkout reference -echo "==> Merging develop to reference branch" -git merge develop -git checkout develop -- composer.json -git add . -echo "==> Replacing composer require entries starting with dpc-sdp with value dev-reference" -cat composer.json | gojq '.require |= with_entries( - if (.key | test("dpc-sdp/tide")) - then .value = "dev-reference" end)' > composer.json.backup -mv -f composer.json.backup composer.json -echo "==> Add all changes" -git add . -git commit -m "Merge changes from develop." -echo "==> Push the changes to remote reference branch" -git push origin --force reference diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1caf8d6d..9eb80315a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,16 +5,6 @@ on: workflow_dispatch: jobs: - set_status_in_progress: - name: set_status_in_progress - if: always() - uses: dpc-sdp/github-actions/.github/workflows/set_status.yml@v1.2.0 - secrets: inherit - with: - context: 'tide_build' - description: 'Tide Build running...' - state: 'pending' - target_url: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }} tide_build: name: tide_build secrets: inherit @@ -22,17 +12,3 @@ jobs: with: module_build: true runner: biggy-tide - export_config: - name: export_config - secrets: inherit - uses: dpc-sdp/github-actions/.github/workflows/export_config.yml@v1.2.0 - set_status: - name: set_status - needs: [tide_build] - if: always() - uses: dpc-sdp/github-actions/.github/workflows/set_status.yml@v1.2.0 - secrets: inherit - with: - context: 'tide_build' - description: 'Tide Build' - target_url: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/pull-request-reviewer-reminder.yml b/.github/workflows/pull-request-reviewer-reminder.yml new file mode 100644 index 000000000..47188d8a2 --- /dev/null +++ b/.github/workflows/pull-request-reviewer-reminder.yml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + workflow_dispatch: + schedule: + - cron: '30 1 * * *' + +permissions: + contents: write # only for delete-branch option + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + exempt-pr-labels: "DO NOT MERGE" + stale-issue-message: 'This issue is stale because it has been open 28 days with no activity. Remove stale label or comment or this will be closed in 14 days.' + days-before-stale: 28 + days-before-close: 14 diff --git a/modules/tide_api/src/Plugin/jsonapi/FieldEnhancer/YamlEnhancer.php b/modules/tide_api/src/Plugin/jsonapi/FieldEnhancer/YamlEnhancer.php index 878d6c938..c663fd204 100644 --- a/modules/tide_api/src/Plugin/jsonapi/FieldEnhancer/YamlEnhancer.php +++ b/modules/tide_api/src/Plugin/jsonapi/FieldEnhancer/YamlEnhancer.php @@ -23,14 +23,12 @@ class YamlEnhancer extends ResourceFieldEnhancerBase { */ protected function doUndoTransform($data, Context $context) { $data = Yaml::decode($data); - $markup_text = $data['markup']['#markup']; - $processed_text = $data['processed_text']['#text']; - if (!empty($processed_text)) { - $data['processed_text']['#text'] = $this->processText($processed_text); + if (!empty($data['markup']['#markup'])) { + $data['processed_text']['#text'] = $this->processText($data['markup']['#markup']); } - if (!empty($markup_text)) { - $data['markup']['#markup'] = $this->processText($markup_text); + if (!empty($data['markup']['#markup'])) { + $data['markup']['#markup'] = $this->processText($data['markup']['#markup']); } return $data; diff --git a/modules/tide_landing_page/tide_landing_page.install b/modules/tide_landing_page/tide_landing_page.install index cc5ffe1c1..243e63264 100644 --- a/modules/tide_landing_page/tide_landing_page.install +++ b/modules/tide_landing_page/tide_landing_page.install @@ -34,7 +34,8 @@ function tide_landing_page_install() { */ function tide_landing_page_update_dependencies() { $dependencies = []; - $dependencies['tide_landing_page'][10102] = ['tide_core' => 10005]; + $dependencies['tide_landing_page'][10101] = ['tide_core' => 10005]; + $dependencies['tide_landing_page'][10106] = ['tide_core' => 10009]; return $dependencies; } diff --git a/modules/tide_publication/config/install/core.entity_view_display.node.publication_page.default.yml b/modules/tide_publication/config/install/core.entity_view_display.node.publication_page.default.yml index fc0836ba4..78929836f 100644 --- a/modules/tide_publication/config/install/core.entity_view_display.node.publication_page.default.yml +++ b/modules/tide_publication/config/install/core.entity_view_display.node.publication_page.default.yml @@ -2,6 +2,7 @@ langcode: en status: true dependencies: config: + - field.field.node.publication.field_content_category - field.field.node.publication_page.field_landing_page_component - field.field.node.publication_page.field_landing_page_contact - field.field.node.publication_page.field_landing_page_hero_image @@ -22,6 +23,7 @@ dependencies: - entity_reference_revisions - options - user + - term_reference_tree id: node.publication_page.default targetEntityType: node bundle: publication_page @@ -39,6 +41,17 @@ content: region: content settings: { } third_party_settings: { } + field_content_category: + type: term_reference_tree + weight: 5 + region: content + settings: + start_minimized: true + leaves_only: true + select_parents: false + cascading_selection: 0 + max_depth: 0 + third_party_settings: { } field_landing_page_component: weight: 1 label: above diff --git a/modules/tide_publication/config/install/field.field.node.publication_page.field_content_category.yml b/modules/tide_publication/config/install/field.field.node.publication_page.field_content_category.yml new file mode 100644 index 000000000..331b98e10 --- /dev/null +++ b/modules/tide_publication/config/install/field.field.node.publication_page.field_content_category.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_content_category + - node.type.publication_page + - taxonomy.vocabulary.content_category +id: node.publication_page.field_content_category +field_name: field_content_category +entity_type: node +bundle: publication_page +label: 'Content category' +description: 'Select the most relevant option from the list of content categories. This mandatory field will help with search and filtering on the website.' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:taxonomy_term' + handler_settings: + target_bundles: + content_category: content_category + sort: + field: name + direction: asc + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/modules/tide_publication/tests/behat/features/fields.feature b/modules/tide_publication/tests/behat/features/fields.feature index 98e8d921a..bf5d15cb7 100644 --- a/modules/tide_publication/tests/behat/features/fields.feature +++ b/modules/tide_publication/tests/behat/features/fields.feature @@ -179,3 +179,5 @@ Feature: Fields for Publication content type And I see field "Show content rating?" And I should see an "input#edit-field-show-content-rating-value" element And I should not see an "input#edit-field-show-content-rating-value.required" element + + And I should see text matching "Content category" diff --git a/modules/tide_publication/tide_publication.install b/modules/tide_publication/tide_publication.install index 6bb3fb8db..a07c4d800 100644 --- a/modules/tide_publication/tide_publication.install +++ b/modules/tide_publication/tide_publication.install @@ -130,3 +130,20 @@ function tide_publication_update_10004() { $tide_update_helper = \Drupal::service('tide_core.entity_update_helper'); $tide_update_helper->configMergeDeep('tide_landing_page', TideEntityUpdateHelper::INSTALL_DIR, $form_configs); } + +/** + * Imports publication_page.field_content_category for existing sites. + */ +function tide_publication_update_10005() { + \Drupal::moduleHandler()->loadInclude('tide_core', 'inc', 'includes/helpers'); + $config_location = [\Drupal::service('extension.list.module')->getPath('tide_publication') . '/config/install']; + $config_read = _tide_read_config('field.field.node.publication_page.field_content_category', $config_location, TRUE); + $storage = \Drupal::entityTypeManager()->getStorage('field_config'); + $id = $storage->getIDFromConfigName('field.field.node.publication_page.field_content_category', $storage->getEntityType()->getConfigPrefix()); + if ($storage->load($id) == NULL) { + $config_entity = $storage->createFromStorageRecord($config_read); + $config_entity->save(); + } + \Drupal::moduleHandler()->loadInclude('tide_core', 'inc', 'includes/updates'); + _tide_core_content_category_form_display('publication_page'); +} diff --git a/modules/tide_site/src/AliasManager.php b/modules/tide_site/src/AliasManager.php index b11506c88..7bd15d762 100644 --- a/modules/tide_site/src/AliasManager.php +++ b/modules/tide_site/src/AliasManager.php @@ -26,7 +26,7 @@ class AliasManager extends CoreAliasManager { /** * {@inheritdoc} */ - public function __construct(AliasRepositoryInterface $repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache, protected ?TimeInterface $time = NULL, AliasStorageHelper $alias_helper) { + public function __construct(AliasRepositoryInterface $repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache, protected ?TimeInterface $time, AliasStorageHelper $alias_helper) { parent::__construct($repository, $whitelist, $language_manager, $cache, $time); $this->aliasHelper = $alias_helper; } diff --git a/modules/tide_site/tide_site.install b/modules/tide_site/tide_site.install index 329f8110f..8b1bfdfaa 100644 --- a/modules/tide_site/tide_site.install +++ b/modules/tide_site/tide_site.install @@ -25,7 +25,7 @@ function tide_site_install() { */ function tide_site_update_10001() { \Drupal::moduleHandler()->loadInclude('tide_core', 'inc', 'includes/helpers'); - $config_location = [\Drupal::service('extension.list.module')->getPath('tide_site') . '/config/install']; + $config_location = [\Drupal::service('extension.list.module')->getPath('tide_core') . '/config/install']; $configs = [ 'field.storage.taxonomy_term.field_additional_comment' => 'field_storage_config', diff --git a/modules/tide_tfa/src/Controller/TideTfaUserController.php b/modules/tide_tfa/src/Controller/TideTfaUserController.php new file mode 100644 index 000000000..7be18ee78 --- /dev/null +++ b/modules/tide_tfa/src/Controller/TideTfaUserController.php @@ -0,0 +1,143 @@ +requestStack = $container->get('request_stack'); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function doResetPassLogin($uid, $timestamp, $hash, $request = NULL) { + // Ensure a valid request object. + if (!$request) { + $request = $this->requestStack->getCurrentRequest(); + } + + // Check if the PRLP module is enabled. + if (!\Drupal::moduleHandler()->moduleExists('prlp')) { + // If PRLP is not enabled, call the parent method. + return parent::doResetPassLogin($uid, $timestamp, $hash, $request); + } + + // Create an instance of PrlpController. + $prlp_controller = new PrlpController( + \Drupal::service('date.formatter'), + \Drupal::entityTypeManager()->getStorage('user'), + \Drupal::service('user.data'), + \Drupal::service('logger.factory')->get('prlp'), + \Drupal::service('flood'), + \Drupal::service('event_dispatcher') + ); + + /** @var \Drupal\user\UserInterface $user */ + $user = $this->userStorage->load($uid); + $this->setUser($user); + + // Let Drupal core deal with the one-time login, + // if TFA is not enabled or + // current user can skip TFA while resetting password. + if ($this->isTfaDisabled() || $this->canSkipPassReset()) { + // Use PRLP's resetPassLogin instead of the core function. + return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash); + } + + // Whether the TFA Validation Plugin is set and ready for use. + $tfa_ready = $this->isReady(); + + // Check for authentication plugin. + if ($tfa_ready && $this->pluginAllowsLogin()) { + $this->messenger()->addStatus($this->t('You have logged in on a trusted browser.')); + return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash); + } + + // Borrow the following codes from the core function: + $current = \Drupal::time()->getRequestTime(); + + // Verify that the user exists and is active. + if ($user === NULL || !$user->isActive()) { + throw new AccessDeniedHttpException(); + } + + // Time out, in seconds, until login URL expires. + $timeout = $this->config('user.settings')->get('password_reset_timeout'); + if ($user->getLastLoginTime() && $current - $timestamp > $timeout) { + $this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); + return $this->redirect('user.pass'); + } + elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && hash_equals($hash, user_pass_rehash($user, $timestamp))) { + if ($tfa_ready) { + $this->session->migrate(); + $token = Crypt::randomBytesBase64(55); + $request->getSession()->set('pass_reset_' . $uid, $token); + + $this->logger->notice('User %name used one-time login link at time %timestamp.', [ + '%name' => $user->getDisplayName(), + '%timestamp' => $timestamp, + ]); + + $this->tempStoreUid($user->id()); + + return $this->redirect('tfa.entry', [ + 'uid' => $uid, + 'hash' => $this->getLoginHash($user), + ], [ + 'query' => ['pass-reset-token' => $token], + 'absolute' => TRUE, + ]); + } + else { + if ($this->canLoginWithoutTfa($this->getLogger('tfa'))) { + return $this->redirectToUserForm($user, $request, $timestamp); + } + else { + return $this->redirect(''); + } + } + } + + // Use PRLP's resetPassLogin instead of the core function. + return $prlp_controller->prlpResetPassLogin($request, $uid, $timestamp, $hash); + } + + /** + * Determines if the user can skip tfa on password reset. + * + * This function checks the TFA settings to see if the option to skip TFA + * during password reset is enabled. If enabled, users will not be required + * to complete two-factor authentication when resetting their password. + * + * @return bool + * TRUE if the user can skip TFA on password reset, FALSE otherwise. + */ + public function canSkipPassReset() { + return $this->tfaSettings->get('reset_pass_skip_enabled'); + } + +} diff --git a/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php new file mode 100644 index 000000000..fbf1d9760 --- /dev/null +++ b/modules/tide_tfa/src/Routing/TideTfaRouteSubscriber.php @@ -0,0 +1,30 @@ +get('user.reset.login')) { + $route->setDefault('_controller', '\Drupal\tide_tfa\Controller\TideTfaUserController::doResetPassLogin'); + } + } + +} diff --git a/modules/tide_tfa/src/TideTfaOperation.php b/modules/tide_tfa/src/TideTfaOperation.php index 922721979..281c7f2c8 100644 --- a/modules/tide_tfa/src/TideTfaOperation.php +++ b/modules/tide_tfa/src/TideTfaOperation.php @@ -56,11 +56,17 @@ public static function setupTfaSettings() { $roles = Role::loadMultiple(); // Initialize the $tfa_required_roles array. $tfa_required_roles = []; - // Iterate through the roles and map the role IDs. - foreach ($roles as $role) { - if ($role->id() !== 'authenticated') { - // Map the role ID to itself. - $tfa_required_roles[$role->id()] = $role->id(); + // Define the roles to exclude in a variable. + $excluded_roles = ['authenticated', 'previewer', 'secure_file_user']; + + if (!empty($roles)) { + // Iterate through the roles and map the role IDs. + foreach ($roles as $role) { + // Check if the current role is not in the excluded roles. + if (!in_array($role->id(), $excluded_roles)) { + // Map the role ID to itself. + $tfa_required_roles[$role->id()] = $role->id(); + } } } diff --git a/modules/tide_tfa/tide_tfa.module b/modules/tide_tfa/tide_tfa.module index e894ea473..f9771b950 100644 --- a/modules/tide_tfa/tide_tfa.module +++ b/modules/tide_tfa/tide_tfa.module @@ -11,6 +11,12 @@ use Drupal\Core\Form\FormStateInterface; * Implements hook_form_alter(). */ function tide_tfa_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // [SD-375] Bypass tfa during reset pass for all users. + if ($form_id == 'tfa_settings_form') { + if (isset($form['reset_pass_skip_enabled'])) { + $form['reset_pass_skip_enabled']['#description'] = t('Allow TFA to be bypassed during password reset by the authenticated user.'); + } + } if ($form_id == 'tfa_entry_form') { // [SD-294] Change the label of the 'Send' button. if (isset($form['actions']['send'])) { diff --git a/modules/tide_tfa/tide_tfa.services.yml b/modules/tide_tfa/tide_tfa.services.yml new file mode 100644 index 000000000..fa90d9713 --- /dev/null +++ b/modules/tide_tfa/tide_tfa.services.yml @@ -0,0 +1,5 @@ +services: + tide_tfa.route_subscriber: + class: Drupal\tide_tfa\Routing\TideTfaRouteSubscriber + tags: + - { name: event_subscriber } diff --git a/modules/tide_webform/tide_webform.module b/modules/tide_webform/tide_webform.module index 4755aedc1..976647998 100644 --- a/modules/tide_webform/tide_webform.module +++ b/modules/tide_webform/tide_webform.module @@ -43,7 +43,7 @@ function tide_webform_form_alter(&$form, FormStateInterface $form_state, $form_i if ($form_id == 'webform_ui_element_form') { $form['#after_build'][] = 'tide_webform_webform_ui_element_form_after_build'; $form['#attached']['library'][] = 'tide_webform/webform'; - if (isset($form['properties']['form']['length_container']['maxlength']) && array_key_exists('#default_value', $form['properties']['form']['length_container']['maxlength'])) { + if (isset($form['properties']['form']['length_container']['maxlength']) && array_key_exists('#default_value', $form['properties']['form']['length_container']['maxlength']) && $form['properties']['type']['#value'] === 'textfield') { $default_value = NestedArray::getValue($form, [ 'properties', diff --git a/src/TideSystemInfoService.php b/src/TideSystemInfoService.php index 13a42b944..fed2f4fa4 100644 --- a/src/TideSystemInfoService.php +++ b/src/TideSystemInfoService.php @@ -226,10 +226,12 @@ function ($entityType) { */ public function getPackageVersion($packageName) { if (empty($packageName)) { - $result = $this->sensorRunner->runSensors([SensorConfig::load('tide_times')]); - $value = $result[0]->getValue(); - $decodedValue = json_decode($value, TRUE); - return $decodedValue; + if ($this->sensorRunner) { + $result = $this->sensorRunner->runSensors([SensorConfig::load('tide_times')]); + $value = $result[0]->getValue(); + $decodedValue = json_decode($value, TRUE); + return $decodedValue; + } } if (strtolower($packageName) === 'php') { diff --git a/tide_core.install b/tide_core.install index b249beb52..394aa131a 100644 --- a/tide_core.install +++ b/tide_core.install @@ -62,6 +62,16 @@ function tide_core_install() { $tideCoreOperation->alterParagraphsLibrary(); } +/** + * Implements hook_update_dependencies(). + */ +function tide_core_update_dependencies() { + $dependencies = []; + $dependencies['tide_core'][10007] = ['bay_platform_dependencies' => 10003]; + + return $dependencies; +} + /** * Increase character limit of URLs. */ diff --git a/tide_core.services.yml b/tide_core.services.yml index c332995ba..49d7fb1d1 100644 --- a/tide_core.services.yml +++ b/tide_core.services.yml @@ -34,4 +34,4 @@ services: - '@entity_field.manager' - '@logger.factory' - '@file_system' - - '@monitoring.sensor_runner' + - '@?monitoring.sensor_runner'