Problem/Motivation

Through the Update module, Drupal core informs administrators about currently available security releases that affect their site. But it does NOT display any information about upcoming security releases. This means that site admins must check outside of Drupal to know whether a security release is forthcoming. Also, Drupal core does not indicate the severity of the security releases that it shows.

Very rarely, there are highly critical security releases with the potential to affect so many sites that it would be good to notify site admins from inside Drupal.

Proposed resolution

As part of the Automatic Update initiative (see #3039120: Create initial feature to display relevant PSA data in Drupal), the Automatic Updates contrib module displayed public service announcement (PSA) and security advisory (SA) messages, fetched from https://updates.drupal.org/psa.json, inside Drupal itself. This feed, created in #3068539: Add psa.json API endpoint to support automatic updates, is used by the security team for critical security announcements and was intended for consumption by Drupal core. The feed is documented at https://www.drupal.org/docs/8/update/automatic-updates#s-public-service-.... We propose to add functionality in the System module to notify site admins of PSAs and security advisories that appear in the feed.

In #3196368: [policy, no patch] Determine to which module the new security advisory functionality should be added, the System module was chosen to host this functionality instead of the Update module.

Sites admins will ultimately be notified in 3 ways:

  1. On the status report page at admin/reports/status.
  2. On certain other administration pages (for users who have the 'administer site configuration' permission).
  3. Via e-mail, which will be implemented in #3197333: Email site admins for security advisories displayed in Drupal.

We follow this logic to determine if a feed item is relevant:

  1. If it's about an extension that is present on the site (not necessarily installed), or about core itself. For PSAs, this criterion alone makes the item relevant. Otherwise...
  2. If the extension's exact version is known, and listed in the item's insecure field.
  3. Or, failing that, if the site's version of the extension is a development snapshot that specifies a major and minor version and both these match for a version in the insecure list.
  4. Or, failing that, if the site's version of the extension is a development snapshot that specifies only a major version and it matches for a version in theinsecure list.

In other words: for a given extension, we'll try to match on the exact version, then fall back to just the major and minor, then fall back to just the major version. If the item is not a PSA and we can't determine the version at all, we don't consider it relevant.

See https://git.drupalcode.org/project/drupal/-/merge_requests/284/diffs#bb5... for where this logic is implemented in code.

Remaining Tasks

Documentation

  1. Main documentation, needs review: Highly Critical Security Advisories within Drupal
  2. Needs to be updated to mention the fetching of advisories: PHP OpenSSL requirements

Questions & Answers

Do we need a way to turn this off?
Yes, but it's not something you can change in the UI (like the contrib module has). It should be very rare for anything to appear in the feed, so most of the time (we'd guess 95%), if a site admin disabled this feature, they'd see no change in their site and it wouldn't be clear what the setting did or why they'd want to toggle it. But the consequences could be very bad if they turned it off and missed a critical security update. That said, there are many cases where security updates are monitored outside of Drupal, so it makes sense to have some developer-facing way to turn it off (we have implemented this with a config setting).

Should we follow the same logic as the contrib module?
We show all PSAs that refer to a project which is present on the site. It does not have to be installed; the admin should be made aware of the risk of installing, and certain risks don't require the module to be installed. For non-PSAs, we also need to match a version that is listed in the item's insecure field, according to the logic detailed above.

The contrib module currently displays all PSAs in the feed where the project is core, or installed on the site. For non-PSAs, the project's version must also be listed in the item's insecure field.

So we're pretty much doing what the contrib module does.

How might this change in the future?
In #3068539: Add psa.json API endpoint to support automatic updates @drumm said:

The planned use of this API is only for highly critical updates which we hope as many Drupal sites as possible set to automatically update. It does not include all PSAs.

We should get clarity if the feed could change later to include anything other than highly critical updates. If it could, then versions of core that were still in use before core changed its logic to handle this would start to show other updates.

Currently there is nothing in the feed that specifies security risk level. We could ask for this to be added to the feed properties. Then we could filter for it now even though no other updates should be in the feed. This way if they were added later they would still be filtered out.

Should we show PSA's for projects that are not installed?
Yes. The contrib module does not, but we want to, because:

  1. Certain remote code execution vulnerabilities might not require the module to be installed.
  2. A module could be installed at any point. A site admin should probably know that a critical security release is forthcoming in the next week so they can make an informed decision if they want to install a module or maybe wait until the release.

Follow ups

  1. User guide issue: #3203748: Add information about highly critical security advisories within Drupal
  2. #3197333: Email site admins for security advisories displayed in Drupal
  3. Make a list of improvements and bugs found here that should be backported to the contrib module.
    See #3201183: Consider fixing these issues found when porting PSA functionality to Drupal core
    1. After this is released in core update the contrib module to not display PSA's if the core version install will display the PSAs. This may just meaning checking for the existence of the services added in this patch
    2. 🔥🔥🔥 Critical Assumes extension name and project name are the same. See #42.1
    3. change getPublicServiceMessages() to throw exceptions rather return error messages as PSA messages are returned. change the callers to react to the exception and change what is displayed for an error.
    4. Change to handle invalid versions of installed modules see #37.7(edge case)
    5. Edge case: handle individual insecure versions that don't parse #37.6
    6. Throw validation error if notifications are set for PSA and no email is provided for the update module(contrib module doesn't have email setting of it's own), @see #43.2
    7. Don't send the same PSA email multiple times @see #43.3
    8. Added tests for invalid json
    9. SecurityAnnouncement class with extra validation and getters
    10. enable_psa setting in the UI is described in contrib module as "Show Public service announcements on administrative pages", but if unchecked it will also stop emails and the status report.

API changes

  1. There's a new service in town, system.sa_fetcher. This is a final class. Code can call to retrieve the advisories that are relevant for the current site. See #137 for why the class is final.
  2. New config settings object for the System module system.advisories with 2 settings
    • enabled: This controls whether the advisories will automatically be fetched during cron.
    • interval_hours: This determines how often advisories will be fetched

User interface changes

Critical security advisories and public service announcements will be displayed on the status report page and certain admin pages to users who have 'administer site configuration' permission.

Status report page:

Messages on admin pages

1 PSA: displayed as warning because you can't update to specific version to remove this message

2 PSA: less likely there would be 2 items but this is what it would look like

1 non- PSA: displayed as a error because there is release to update to right now

Data model changes

None

Release notes snippet

Certain highly critical security advisories (SAs) and public service announcements (PSAs) will be displayed on the status report page and certain admin pages to users who have the 'administer site configuration' permission. Examples of past announcements that would have been included in this new feed:

While it is recommended to leave this functionality enabled, sites that do not want these advisories to be fetched from Drupal.org can add this line in settings.php:
$config['system.advisories']['enabled'] = FALSE;

CommentFileSizeAuthor
#206 advisory-1-non-psa.png20.85 KBtedbow
#206 advisories-2-psa.png18.59 KBtedbow
#206 advisories-1-psa.png19.16 KBtedbow
#203 help-page.png239.32 KBwebchick
#203 errors-on-errors.png195.25 KBwebchick
#187 hook_help.png35.79 KBtedbow
#187 fail_message.png21.6 KBtedbow
#180 daqrume-a6063f76-7c40-4f0a-a725-eca156263217.png28.35 KBphenaproxima
#152 3041885-152.patch94.99 KBtedbow
#152 interdiff-150-152.txt1.46 KBtedbow
#152 advisories_with_status_messages.png23.07 KBtedbow
#152 advisories.png18.62 KBtedbow
#150 3041885-150.patch95.06 KBtedbow
#150 interdiff-149-150.txt3.37 KBtedbow
#149 3041885-149.patch92.74 KBtedbow
#149 interdiff-147-149.txt2.06 KBtedbow
#147 3041885-147.patch92.48 KBtedbow
#147 interdiff-146-147.txt8.49 KBtedbow
#146 3041885-146.patch87.96 KBtedbow
#146 interdiff-145-146.txt10 KBtedbow
#145 3041885-145.patch88.69 KBtedbow
#145 interdiff-142-145.txt9.16 KBtedbow
#144 3041885-144-1.png40.05 KBphenaproxima
#142 3041885-142.patch87.07 KBtedbow
#142 interdiff-138-142.txt1.2 KBtedbow
#138 3041885-138.patch87.02 KBtedbow
#138 interdiff-137-138.txt4.48 KBtedbow
#137 3041885-137.patch86.47 KBtedbow
#137 interdiff-136-137.txt9.81 KBtedbow
#136 3041885-136.patch86.51 KBtedbow
#136 interdiff-134-136.txt22.27 KBtedbow
#134 3041885-134.patch85.38 KBtedbow
#134 interdiff-131-134.txt4.04 KBtedbow
#131 3041885-130.patch84.86 KBtedbow
#131 interdiff-128-130.txt2.39 KBtedbow
#128 3041885-128.patch82.47 KBtedbow
#128 interdiff-126-128.txt14.38 KBtedbow
#127 3041885-126.patch82.18 KBtedbow
#127 interdiff-122-126.txt6.81 KBtedbow
#122 3041885-122.patch82.39 KBtedbow
#121 3041885.120_121.interdiff.txt486 bytesdww
#121 3041885-121.patch82.29 KBdww
#120 3041885-120.patch82.42 KBtedbow
#120 interdiff-118-120.txt2.08 KBtedbow
#118 3041885-118.patch82.35 KBtedbow
#118 interdiff-113-118.txt4.58 KBtedbow
#113 3041885-113.patch82.49 KBtedbow
#113 interdiff-107-113.txt12.2 KBtedbow
#107 3041885-107.patch81.86 KBtedbow
#107 interdiff-106-107.txt4.72 KBtedbow
#106 3041885-106.patch81.64 KBayushmishra206
#106 interdiff_101-106.txt5.65 KBayushmishra206
#101 3041885-101.patch81.6 KBtedbow
#101 interdiff-91-101.txt3.1 KBtedbow
#91 3041885-91.patch80.76 KBtedbow
#91 interdiff-90-91.txt15.98 KBtedbow
#90 3041885.86_90.interdiff.txt11.09 KBdww
#90 3041885-90.patch80.17 KBdww
#86 3041885-86.patch79.94 KBtedbow
#86 interdiff-85-86.txt13.82 KBtedbow
#85 3041885-85.patch68.65 KBtedbow
#85 interdiff-82-85.txt10.1 KBtedbow
#82 3041885-82.patch63.4 KBtedbow
#82 interdiff-81-82.txt20.59 KBtedbow
#81 3041885-81.patch63.06 KBtedbow
#81 interdiff-80-81.txt26.35 KBtedbow
#80 3041885-80.patch62.23 KBtedbow
#80 interdiff-79-80.txt11.71 KBtedbow
#79 3041885-79.patch64.18 KBtedbow
#79 interdiff-77-79.txt13.57 KBtedbow
#77 3041885-77.patch58.21 KBtedbow
#77 interdiff-75-77.txt476 bytestedbow
#75 3041885-75.patch58.21 KBtedbow
#75 interdiff-74-75.txt442 bytestedbow
#74 3041885-74.patch58.19 KBtedbow
#74 interdiff-73-74.txt7.24 KBtedbow
#73 3041885-73.patch52.57 KBtedbow
#73 interdiff-73-73.txt3.39 KBtedbow
#72 3041885-72.patch53.34 KBtedbow
#72 interdiff-66-72.txt12.67 KBtedbow
#66 3041885-65.patch53.32 KBtedbow
#66 interdiff-63-65.txt6.88 KBtedbow
#66 status_report2.png44.49 KBtedbow
#66 admin_screens2.png117.34 KBtedbow
#66 settings_form_change.png69.32 KBtedbow
#63 3041885-63.patch53.06 KBtedbow
#63 interdiff-62-63.txt6.44 KBtedbow
#63 status_report.png16.59 KBtedbow
#63 admin_people.png27.71 KBtedbow
#63 admin_updates.png71.57 KBtedbow
#62 3041885-62.patch55.03 KBtedbow
#62 interdiff-61-62.txt5.06 KBtedbow
#61 3041885-61.patch55.63 KBtedbow
#61 interdiff-60-61.txt15.18 KBtedbow
#60 3041885-60.patch51.85 KBtedbow
#60 interdiff-58-60.txt8.92 KBtedbow
#59 interdiff-51-58.txt33.46 KBtedbow
#58 3041885-58.patch54.42 KBtedbow
#51 3041885-51.patch54.37 KBtedbow
#51 interdiff-49-51.txt3.37 KBtedbow
#49 3041885-49.patch56.09 KBtedbow
#49 interdiff-46-49.txt17.59 KBtedbow
#46 3041885-46.patch56.04 KBtedbow
#46 interdiff-43-46.txt10.43 KBtedbow
#43 3041885-43.patch55.19 KBtedbow
#43 interdiff-42-43.txt11.34 KBtedbow
#42 3041885-42.patch51.32 KBtedbow
#42 interdiff-41-42.txt21.23 KBtedbow
#41 3041885-41.patch53.21 KBtedbow
#41 interdiff-39-41.txt9.25 KBtedbow
#39 3041885-39.patch52.55 KBtedbow
#39 interdiff-38-39.txt15.35 KBtedbow
#38 3041885-38.patch49.82 KBtedbow
#38 interdiff-37-38.txt10.12 KBtedbow
#37 3041885-37.patch50.05 KBtedbow
#37 interdiff-36-37.txt17.28 KBtedbow
#36 3041885-36.patch45.41 KBtedbow
#36 interdiff-34-36.txt1.31 KBtedbow
#34 3041885-34.patch45.44 KBtedbow
#26 interdiff-19-26.txt37.12 KBtedbow
#34 interdiff-31-34.txt6.44 KBtedbow
#24 interdiff_5-14.txt30.26 KBbeautifulmind
#31 3041885-31.patch45.38 KBtedbow
#19 core-display-relevant-PSA-data-3041885-19.patch30.29 KBbeautifulmind
#31 interdiff-26-31.txt7.11 KBtedbow
#14 core-display-relevant-PSA-data-3041885-14.patch30.27 KBbeautifulmind
#26 3041885-26.patch42.29 KBtedbow
#6 core-display-relevant-PSA-data-3041885-5.patch23.62 KBbeautifulmind

Issue fork drupal-3041885

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

heddn created an issue. See original summary.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

heddn’s picture

gábor hojtsy’s picture

My understanding is that https://www.drupal.org/u/beautifulmind is working on this patch. I cannot assign because they are not a maintainer.

beautifulmind’s picture

Changes have been committed and here is the patch. Please review and confirm.

dww’s picture

Status: Active » Needs review

Updating status based on the patch being uploaded. Will review when I can.

Thanks!
-Derek

longwave’s picture

+++ b/core/modules/update/src/Services/Notify.php
@@ -0,0 +1,167 @@
+  protected function doSend($to, array $params) {
+    $users = $this->entityTypeManager->getStorage('user')
+      ->loadByProperties(['mail' => $to]);
+    foreach ($users as $user) {
+      $to_user = reset($users);
+      $params['langcode'] = $to_user->getPreferredLangcode();
+      $this->mailManager->mail('update_psa', 'notify', $to, $params['langcode'], $params);
+    }
+  }

$to comes from config, but this won't send a mail if the email address doesn't have an account.

Also, the loop over $users isn't right, $user isn't even used inside the loop.

Finally, the first parameter to $this->mailManager->mail() should be the module name, "update_psa" isn't a module.

Status: Needs review » Needs work

The last submitted patch, 6: core-display-relevant-PSA-data-3041885-5.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

beautifulmind’s picture

Thank you for the review. I will do another iteration by fixing all the issues and will submit for the community review.

Regards.

tedbow’s picture

@beautifulmind thanks for getting this started!

Not a full review but a few things

  1. --- /dev/null
    +++ b/core/modules/update/config/install/update_psa.settings.yml
    
    +++ b/core/modules/update/config/install/update_psa.settings.yml
    @@ -0,0 +1,14 @@
    
    @@ -0,0 +1,14 @@
    +psa_endpoint: 'https://updates.drupal.org/psa.json'
    +enable_psa: true
    +notify: true
    +check_frequency: 43200
    +enable_readiness_checks: true
    +hashes_uri: 'https://updates.drupal.org/release-hashes'
    +ignored_paths: "modules/*\nthemes/*\nprofiles/*"
    +download_uri: 'https://www.drupal.org/in-place-updates'
    +enable_cron_updates: false
    +enable_cron_security_updates: false
    +database_update_handling:
    +  - maintenance_mode_activate
    +  - execute_updates
    +  - maintenance_mode_disactivate
    

    I think these settings can just live in core/modules/update/config/install/update.settings.yml

    But we actually don't need most of these. We only need the settings that relate to the actual PSA functionality. These are all the settings for the contrib module but we are not adding all of the functionality here.

  2. +++ b/core/modules/update/config/schema/update.schema.yml
    @@ -40,3 +40,43 @@ update.settings:
    +update_psa.settings:
    +  type: config_object
    +  label: 'Automatic updates settings'
    +  mapping:
    +    psa_endpoint:
    +      type: string
    +      label: 'Endpoint URI for PSAs'
    +    enable_psa:
    +      type: boolean
    +      label: 'Enable PSA notices'
    +    notify:
    +      type: boolean
    +      label: 'Notify when PSAs are available'
    +    check_frequency:
    +      type: integer
    +      label: 'Frequency to check for PSAs, defaults to 12 hours'
    +    enable_readiness_checks:
    +      type: boolean
    +      label: 'Enable readiness checks'
    +    hashes_uri:
    +      type: string
    +      label: 'Endpoint URI for file hashes'
    +    ignored_paths:
    +      type: string
    +      label: 'List of files paths to ignore when running readiness checks'
    +    download_uri:
    +      type: string
    +      label: 'URI for downloading in-place update assets'
    +    enable_cron_updates:
    +      type: boolean
    +      label: 'Enable automatic updates via cron'
    +    enable_cron_security_updates:
    +      type: boolean
    +      label: 'Enable automatic updates for security releases via cron'
    +    database_update_handling:
    +      type: sequence
    +      label: 'Database update handling'
    +      sequence:
    +        type: string
    +        label: 'Tagged service to handle database updates'
    \ No newline at end of file
    

    Same here. We only need the schema for PSA functionality. probably just the first 4 items. I think we can nest these for under the existing schema in the file under their own collectoin "psa"

  3. +++ b/core/modules/update/src/Services/AutomaticUpdatesPsa.php
    @@ -0,0 +1,252 @@
    +use Psr\Log\LoggerInterface;
    

    Not used. Don't need a use statement if only referenced in a comment.

  4. +++ b/core/modules/update/src/Services/AutomaticUpdatesPsa.php
    @@ -0,0 +1,252 @@
    +/**
    + * Class AutomaticUpdatesPsa.
    + */
    +class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
    

    The class name should just be UpdatesPsa because we are adding this to the Update module directly.

    Also the class needs description of what it does instead of the current comment

  5. +++ b/core/modules/update/src/Services/AutomaticUpdatesPsa.php
    @@ -0,0 +1,252 @@
    +    $this->profile = $profile;
    +    $this->theme = $theme;
    ...
    +    return $this->{$extension_type}->exists($project_name) && !empty($this->{$extension_type}->getAllAvailableInfo()[$project_name]['version']);
    

    This dynamic accessing of the properties was fixed in #3157857: Do not accessed Extensionlist class variables in AutomaticUpdatesPsa dynamically

    @beautifulmind were working from the 8.x-1.x branch of the contrib module or 8.x-2.x just before this was commited? either is fine because I don't think there has been that many changes to this functionality.

    but I think we should bring in the changes from that issue.

  6. +++ b/core/modules/update/src/Services/AutomaticUpdatesPsaInterface.php
    @@ -0,0 +1,18 @@
    diff --git a/core/modules/update/src/Services/Notify.php b/core/modules/update/src/Services/Notify.php
    
    diff --git a/core/modules/update/src/Services/Notify.php b/core/modules/update/src/Services/Notify.php
    new file mode 100644
    
    new file mode 100644
    index 0000000000..66b9355f1c
    
    index 0000000000..66b9355f1c
    --- /dev/null
    
    --- /dev/null
    +++ b/core/modules/update/src/Services/Notify.php
    

    I think this patch would be easier to review if we did the mailing in a follow up.

    but it is probably a product/release management question as to whether we can have the notification only displayed on the screen as first step.

  7. +++ b/core/modules/update/src/Services/Notify.php
    @@ -0,0 +1,167 @@
    + * Class EmailNotify.
    

    Need a class description

  8. +++ b/core/modules/update/src/Services/NotifyInterface.php
    @@ -0,0 +1,15 @@
    + * Interface NotifyInterface.
    

    Need an actual description

  9. +++ b/core/modules/update/update.install
    @@ -177,3 +178,31 @@ function _update_requirement_check($project, $type) {
    +  $requirements['update_psa'] = [
    +    'title' => t('<a href="@link">Public service announcements</a>', ['@link' => 'https://www.drupal.org/docs/8/update/automatic-updates#psas']),
    

    Should change to :link instead of @link because it is in html.

    But actually this shouldn't be link because it links the contrib module docs.

  10. +++ b/core/modules/update/update.module
    @@ -19,6 +19,8 @@
    +use Symfony\Component\Process\PhpExecutableFinder;
    +use Symfony\Comopenent\Process\Process;
    

    Not used

  11. +++ b/core/modules/update/update.module
    @@ -173,6 +185,11 @@ function update_cron() {
    +  ¶
    

    trailing space

  12. +++ b/core/modules/update/update.services.yml
    @@ -22,3 +22,24 @@ services:
    +      - '@string_translation'  ¶
    

    trailing space

aaronmchale’s picture

Would someone who is "in the know here" be able to provide a concise and clear summary of what's trying to be accomplished in this issue? I'm struggling to understand how exactly this will differ from what's already in Core (as in, Core already has update status screens + update status emails).

Thanks,
-Aaron

beautifulmind’s picture

Hello @tedbow
Thank you for the detailed review. I noticed those points and will make enhancements as well.

Initially, I wanted to combine those settings but for the sack of clarity, I created a separate file under /install!

In the next iteration, most probably by Monday evening or Tuesday morning, I will enhance the code further and submit a patch for community review.

Once again, I really appreicate your detailed review & feedback. Thank you. :)

Regards.

beautifulmind’s picture

All the issues have been fixed, logger added, PSA Notification message template added. Fixed all the Coding standards issues. Please review the patch.

Regards.

beautifulmind’s picture

Issue summary: View changes
xjm’s picture

Status: Needs work » Needs review

Marking NR for #14.

beautifulmind’s picture

I just cleaned the files for white spaces. Should I upload the cleaned patch with same file name? Or should revise the file name with target comment number? Please advise.

Regards.

naheemsays’s picture

Not a core developer, but new comment number is always better (you can track just the whitestpace changes in an interdiff.txt or interdiff-oldno-newno.txt).

beautifulmind’s picture

Here is the cleaned patch.

Regards.

dww’s picture

@beautifulmind Thanks for uploading that. Would you be willing to also post interdiffs with your patches, so folks can see what's changed between each version?

Thanks!
-Derek

Status: Needs review » Needs work

The last submitted patch, 19: core-display-relevant-PSA-data-3041885-19.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

beautifulmind’s picture

Status: Needs work » Needs review

Certainly! I will post the interdiffs as well.

beautifulmind’s picture

Status: Needs review » Needs work

Didn't mean to change the status.

beautifulmind’s picture

StatusFileSize
new30.26 KB

Here is the interdiff.

On a side note, when I tried executing a test from CLI on my local environment, I accidentally deleted everything under /core, losing my work! But then I had the patch created just before that. I ran composer update, and then applied the patch! I have everything back!

But here, it keeps failing, throwing different kinds of errors. Any thoughts?

beautifulmind’s picture

I figured it out!

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new37.12 KB
new42.29 KB
  1. +++ b/core/modules/update/src/ProjectInfoTrait.php
    @@ -0,0 +1,197 @@
    +      if (!in_array($extension_type, $this->getExtensionsTypes())) {
    +        throw new \UnexpectedValueException("Invalid extension type: $extension_type");
    +      }
    +      $list = \Drupal::service("extension.list.$extension_type");
    

    We no longer need this logic because the only class that uses this trait is UpdatesPsa and it should always have $this->extensionLists[$extension_type] set. If not should throw the exception

  2. +++ b/core/modules/update/src/ProjectInfoTrait.php
    @@ -0,0 +1,197 @@
    +  protected function getExtensionsTypes() {
    +    return ['module', 'profile', 'theme'];
    +  }
    

    this can be remove too

  3. +++ b/core/modules/update/update.module
    @@ -173,6 +183,10 @@ function update_cron() {
    +    // Checkers should run before updates because of class caching.
    

    This comment I think refers to the readiness checkers. This is copied directly from the contrib module but it looks to me like the contrib module had this comment on the wrong line.

  4. +++ b/core/modules/update/update.routing.yml
    @@ -84,4 +84,4 @@ update.confirmation_page:
    -    _access_update_manager: 'TRUE'
    +    _access_update_manager: 'TRUE'
    \ No newline at end of file
    

    Unrelated change

  5. +++ b/core/modules/update/src/Services/Notify.php
    @@ -0,0 +1,169 @@
    +  /**
    +   * Event dispatcher.
    +   *
    +   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
    +   */
    +  protected $eventDispatcher;
    

    Unused

  6. +++ b/core/modules/update/src/Services/UpdatesPsa.php
    @@ -0,0 +1,238 @@
    +namespace Drupal\update\Services;
    

    It doesn't make sense to just add all the new services to a "Services" namespace. The update module already has a punch of services. I think we should create a new "Psa" namespace

  7. I noticed the functional tests from the contrib module were not move over. I copied them over pretty much as is except for moving the PSA related test methods into 1 test class.

    I didn't review them yet but I wanted to bring them to make sure this is working. I had to make some more changes to make the tests pass.

  8. A bunch of tests were failing because the update settings and schema files didn't pass

Status: Needs review » Needs work

The last submitted patch, 26: 3041885-26.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

longwave’s picture

+++ b/core/modules/update/src/ProjectInfoTrait.php
@@ -0,0 +1,181 @@
+      // From 8.8.0 onward, always use packaged for core because non-packaged
+      // will no longer make any sense.
+      if (version_compare(\Drupal::VERSION, '8.8.0', '>=')) {
+        $infos['drupal']['packaged'] = TRUE;
+      }

We can refactor away this check, because this will always be true.

beautifulmind’s picture

@tedbow Thank you for the suggestions. I fixed all the issues and will remove unused code and comments.

About the readiness check comment, I kept it thinking it will help implementing that feature when we arrive there. But for now, I will remove it.

@longwave Thank you for the suggestion. I will accomodate this in the patch.

tedbow’s picture

@beautifulmind sorry I was unclear I fixed the issues I commented on. See the interdiff file that I included with #26

About the readiness check comment, I kept it thinking it will help implementing that feature when we arrive there. But for now, I will remove it.

I removed that but also the readiness checks are being worked on here #3162655: Create Automatic Updates Readiness Checks in new experimental module. They are being implemented in a new experimental module.

UPDATE: sorry I guess I didn't actually remove this comment

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new7.11 KB
new45.38 KB

This patch adds 2 things

  1. Cleans up the test class added in #26. The class was mostly just copied from the contrib module in #26
  2. Fixed the 2 test failures in #26 by copying the new template added to both the stable and stable9 themes

Status: Needs review » Needs work

The last submitted patch, 31: 3041885-31.patch, failed testing. View results

beautifulmind’s picture

@tedbow Thank you for the update and clarification.

Regards.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new6.44 KB
new45.44 KB
  1. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,239 @@
    +      $version_array = explode('-', $version, 2);
    +      if ($version_array && $version_array[0] === \Drupal::CORE_COMPATIBILITY) {
    +        return isset($version_array[1]) ? $version_array[1] : NULL;
    +      }
    +      if (count($version_array) === 1) {
    +        return $version_array[0];
    +      }
    +      if (count($version_array) === 2 && $version_array[1] === 'dev') {
    +        return $version;
    +      }
    +    }, $json->insecure));
    +    $version_array = explode('-', $extension_version, 2);
    

    We should consider expanding \Drupal\update\ModuleVersion to handle this parsing.

    We added this class we intentionally made cover only the parsing we needed at the time and made @internal

    But since this is still in the Update module it would not affect the @internal status.

    UPDATE: actually this is dealing with versions in $json->insecure which we can't rely on.

    So probably don't need this whole method.

  2. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,239 @@
    +    $contrib_constraint = $parser->parseConstraints($current_version);
    +    if ($psa_constraint->matches($contrib_constraint)) {
    +      $messages[] = $this->message($json->title, $json->link);
    +    }
    

    I think the logic here is wrong.

    looking at the docs for the the PSA feed and the insecure property it says

    List of versions of the affected project that are currently insecure. This does not indicate which versions will be marked as insecure. This list will be updated after the security release is published, to also include newly-insecure versions.

    So in the code we are only adding messages if current version is within insecure. We should not be filtering by this.

    By policy for security releases the versions that will be affecting cannot be disclosed before hand.

    So I don't think we actually need this at all.

    UPDATE: so reading this code again I see it only matches the version if it isn't a PSA. I am not sure what other types of message are going to be in the JSON fee but currently the docs don't say the insecure changes for non-PSA's. I have pinged @drumm and @heddn about this.

  3. Until I get clarification on insecure I am going to assume the current functionality is correct.
    +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,239 @@
    +          if ($json->is_psa && ($json->type === 'core' || $this->isValidExtension($json->type, $json->project))) {
    +            $messages[] = $this->message($json->title, $json->link);
    +          }
    +          elseif ($json->type === 'core') {
    +            $this->parseConstraints($messages, $json, \Drupal::VERSION);
    +          }
    +          elseif ($this->isValidExtension($json->type, $json->project)) {
    +            $this->contribParser($messages, $json);
    +          }
    

    I still think the logic here is confusing.

    First we call $this->isValidExtension($json->type, $json->project) 2x. I think we should just call it once an at the top of this block and just continue if it is FALSE.

    2nd, parseConstraints() doesn't really "parse constraints". $json->insecure just as an array of versions only internally in parseConstraints() does it turn them into constraints, so from the caller perspective the name doesn't make sense. Also it calls \Composer\Semver\VersionParser::parseConstraints() which uses the same method name but does actually just parse constraints.

    3rd in addition parseConstraints() also adds to the $messages array which is not clear at all reading this code block.

    4th, contribParser() has the same problem as as above but also determines the installed version of the project. So it does 3 things.

    I have refactored this to have 3 methods, matchesInstalledVersion() which will call getContribVersions() and getInstalledVersion()

    I think this will be clearer because the 3 methods can be clearer in what the do.

    Then this code block can just be

    if ($json->type !== 'core' && !$this->isValidExtension($json->type, $json->project)) {
      continue;
    }
    if ($json->is_psa || $this->matchesInstalledVersion($json)) {
      $messages[] = $this->message($json->title, $json->link);
    }
    

    To me this makes it more clear that we add messages for all PSA's and for other types we only add the message if it matches the installed version.

    I wasn't clear on this before.

Status: Needs review » Needs work

The last submitted patch, 34: 3041885-34.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Issue summary: View changes
Status: Needs work » Needs review
Related issues: +#3068539: Add psa.json API endpoint to support automatic updates
StatusFileSize
new1.31 KB
new45.41 KB
  1. Updated remaining tasks with an item to specify if we want to follow the same logic regarding items where is_psa !== true
  2. +++ b/core/modules/update/tests/modules/psa_test/src/Controller/JsonTestController.php
    @@ -72,7 +72,7 @@ public function json() {
    -      'project' => 'token',
    +      'project' => 'layout_builder_st',
    

    Left this is because I was testing something. Fixed

tedbow’s picture

Issue tags: +Needs tests
StatusFileSize
new17.28 KB
new50.05 KB

More review

  1. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,161 @@
    +   * @param string $to
    +   *   The email address where the message will be sent.
    

    Since the the method is toSend I don't think we need to call this $to. $email seems better.

  2. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,161 @@
    +   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    +   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    

    We don't need the throws if the method itself don't throw these. At least that seems to be the pattern in core.

  3. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,161 @@
    +    foreach ($users as $user) {
    +      $to_user = reset($users);
    

    The $user var is never used. Also there will always at most be 1 user matching an email. So we don't need the foreach loop.

  4. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,248 @@
    +        foreach ($json_payload as $json) {
    +          if ($json->type !== 'core' && !$this->isValidExtension($json->type, $json->project)) {
    

    This class just accesses the properties $json directly with no documentation of what will be in the object.

    I have made a SecurityAnnouncement class that checks for the keys in its factory method throws an exception if the expected keys are not found.

  5. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,248 @@
    +      $this->logger->error($exception->getMessage());
    +      $messages[] = $this->t('Drupal PSA endpoint service is malformed.');
    ...
    +    $psa_constraint = $parser->parseConstraints($version_string);
    

    Before the changes in this comment the only place this could be thrown is in calls to \Composer\Semver\VersionParser::parseConstraints(). i have checked the contrib and this is the same.

    This means the exception could be thrown if an item in the JSON has a bad value for insecure. If so then no items will be displayed. This seems very unlikely to happen because the PSA feed from drupal.org is unlikely to have this error.

    But in the unlikely even that this does happen where there is problem with 1 item do we want to not show any items in the feed?

    Since these at first will be used with highly critical updates we should probably error on the side of showing as much information as we can.

    There seem to be 2 options for handling items that will have this problem

    1) Just don't show the individual item that has the problem but show the other items.

    2) If the item's only problem is in the parsing of insecure assume when can't parse this that we should show the message. Because it would be better to show the user a highly PSA message that does not apply to them then to NOT show them one that doesn't

  6. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,248 @@
    +    $versions = $json->type === 'core' ? $json->insecure : $this->getContribVersions($json->insecure);
    +    $version_string = implode('||', $versions);
    

    We concatenate the version into 1 constraint string here so that we can call \Composer\Semver\VersionParser::parseConstraints() one time.

    but this means that if any of the version strings are malformed an exception will be thrown.

    I also don't think think calling parseConstraints() 1 time vs once for every version will actually be much of a performance gain. Internally parseConstraints() will call itself recursively for each version anyways. It will probably actually be worse performing because it has to parse the string to get each version.

    Changing to call once for each version.

  7. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,248 @@
    +    $installed_constraint = $parser->parseConstraints($this->getInstalledVersion($json));
    

    This would throw the exception if the installed version has an invalid constraint. Not sure how this could happen but maybe if the extension was patched or the extension was installed from a git clone.

    This could result in not showing a PSA because the installed version number has a problem.

    Again I think here we should error on the side of showing the PSA when we can't be sure.

  8. +++ b/core/modules/update/src/UpdateSettingsForm.php
    @@ -88,6 +88,27 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +    $form['psa']['psa.enable'] = [
    +      '#type' => 'checkbox',
    +      '#title' => $this->t('Show Public service announcements on administrative pages.'),
    +      '#default_value' => $config->get('psa.enable'),
    +    ];
    +    $form['psa']['notify.psa'] = [
    +      '#type' => 'checkbox',
    +      '#title' => $this->t('Send email notifications for Public service announcements.'),
    +      '#default_value' => $config->get('notify.psa'),
    +      '#description' => $this->t('The email addresses listed above will be notified.'),
    +    ];
    +
    

    These 2 settings don't work they way they are described currently.

    Because psa.enable is checked in \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages() and an empty array is returned if it is empty then this settings actually everyone where the messages might be shown, 1) on admin pages, 2) on the status report and 3) via email.

    I am changing psa.enable to only be checked in update_page_top() so that it only affects showing on admin pages like it is described here.

    You should be able to not show the messages on admin pages and still send emails, which the current functionality does not allow you to do.

    I think showing on the status report should not be able to be disabled. I don't think we allow that for available updates except by disabling the Update module.

  9. +++ b/core/modules/update/src/UpdateSettingsForm.php
    @@ -140,6 +161,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    +      ->set('psa.enable', $form_state->getValue('psa.enable'))
    +      ->set('psa.notify', $form_state->getValue('psa.notify'))
    
    +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,147 @@
    +    $this->config('update.settings')
    +      ->set('psa.endpoint', $this->workingEndpoint)
    +      ->set('psa.enable', FALSE)
    +      ->save();
    

    The form setting didn't actually work here I think because of the period. The tests still passed because we were setting directly instead of using the form. I have fixed the form and changed the test to use the form.

  10. We need tests for malformed Json
tedbow’s picture

Issue tags: -Needs tests
StatusFileSize
new10.12 KB
new49.82 KB
  1. Adding test coverage for invalid JSON. I don't think the way patch currently handles this is good.
  2. I removed the psa_test module and instead added actual json test fixture files.
  3. Currently if there is invalid json:

    On the status report page: It will display "1 urgent announcement requires your attention" and then "Drupal PSA JSON is malformed." in the list

    In the notify email:: The same behavior happens.

    This is because in \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages() the exceptions are caught and "Drupal PSA JSON is malformed." is returned as the only message. So no caller has a good way to tell that an error occurred.

    I think a better solution would be for getPublicServiceMessages() to throw the exception and the callers handle it.

    I will fix this in the next patch.

tedbow’s picture

StatusFileSize
new15.35 KB
new52.55 KB
  1. This changes so that \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages() throws exception rather than returns the error message in the announcement messages array. see #38.3

    This means the callers can respond to the exception. I updated the tests but \Drupal\Tests\update\Functional\PsaTest::testInvalidJsonEmail() will fail. I am not sure what do in this case. Should we send an email saying we couldn't fetch PSA information and the admin should check drupal.org? or should we not send an email and just wait until it can get valid PSA's

  2. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,157 @@
    +      $frequency = $this->configFactory->get('update.settings')->get('psa.check_frequency');
    ...
    +      if (($this->time->getRequestTime() - $last_check) > $frequency) {
    

    Here we check if should actually send the messages via email based on update_psa.notify_last_check but we have already called getPublicServiceMessages(). If we aren't going to send the emails there is no reason to call this.

    Moving this to the beginning of the function.

Status: Needs review » Needs work

The last submitted patch, 39: 3041885-39.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new9.25 KB
new53.21 KB
  1. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,159 @@
    +    $messages = $this->updatesPsa->getPublicServiceMessages();
    

    When calling here to send an email I decided to just log an error if the message could not be retrieved.

  2. +++ b/core/modules/update/update.install
    @@ -177,3 +181,46 @@ function _update_requirement_check($project, $type) {
    +  catch (TransferException $transfer_exception) {
    

    This was happening in hook_requirements update phase so it was causing test to file. This only should happen in the runtime phase.

  3. +++ b/core/modules/update/update.install
    @@ -177,3 +181,46 @@ function _update_requirement_check($project, $type) {
    +    $requirements['update_psa']['value'] = t('Unable to retrieve PSA information from ' . \Drupal::config('update.settings')->get('psa.endpoint'));
    

    This same message was being constructed in 2 places and now a third that am handling errors when trying to send an email. I create a helper static function \Drupal\update\Psa\UpdatesPsa::getErrorMessageFromException(). This can be used in callers in get the message to display.

  4. Also fixed the standards issues in the last patch
tedbow’s picture

StatusFileSize
new21.23 KB
new51.32 KB
  1. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,292 @@
    +  protected function isValidExtension(string $extension_type, string $project_name) {
    +    try {
    +      $extension_list = $this->getExtensionList($extension_type);
    +      return $extension_list->exists($project_name) && !empty($extension_list->getAllAvailableInfo()[$project_name]['version']);
    +    }
    

    🙀I just realized that this logic is assuming the project value that comes back in the PSA feed will be the same as the extension(module,theme,etc) name. This is usually true but not always. The project name is short machine name that is on Drupal.org. It does not have to be used for the extension name.

    changing to use the UpdateManager instead since it returns an array keyed by project name. This is more expensive but since this is not called often it should be ok.

  2. Add a test case where the project does not match the extension name.
  3. +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    @@ -0,0 +1 @@
    

    I realize now the reason we can't use the json is because we can't hardcode for the core matching version. As it is now this would fail in 9.2.x. Changing back to use the controller.

    We could use the JSON if we dynamically set the core version which is what we use in other update tests. But changing to use the controller for now.

tedbow’s picture

Issue summary: View changes
StatusFileSize
new11.34 KB
new55.19 KB
  1. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,178 @@
    +    $notify_emails = $this->configFactory->get('update.settings')->get('notification.emails');
    +    if (!empty($notify_emails)) {
    

    This should be checked at the beginning of the method instead because we can't do anything if we don't have any emails. Fixed

  2. +++ b/core/modules/update/src/UpdateSettingsForm.php
    @@ -88,6 +88,27 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +    $form['psa']['psa_notify'] = [
    +      '#type' => 'checkbox',
    +      '#title' => $this->t('Send email notifications for Public service announcements.'),
    +      '#default_value' => $config->get('psa.notify'),
    +      '#description' => $this->t('The email addresses listed above will be notified.'),
    +    ];
    

    This checkbox can be checked even if there are no emails provide. Adding validation to prevent this.

  3. +++ b/core/modules/update/update.module
    @@ -173,6 +197,10 @@ function update_cron() {
       $interval = 60 * 60 * 24 * $frequency;
       $last_check = \Drupal::state()->get('update.last_check', 0);
       if ((REQUEST_TIME - $last_check) > $interval) {
    +    // Checkers should run before updates because of class caching.
    +    /** @var \Drupal\update\Psa\NotifyInterface $notify */
    +    $notify = \Drupal::service('update.psa_notify');
    +    $notify->send();
         // If the configured update interval has elapsed, we want to invalidate
    

    $notify->send() should not be within this if statement. Here $frequency is from check.interval_days setting which is how ofter to check for updates not PSA's.

    We could take it out of the loop and just call it every time cron runs because \Drupal\update\Psa\EmailNotify::send() already checks psa.check_frequency.

    but this still is not great because that means that if items stay in the PSA feed for more than 12 hours the admin would be sent the same message every 12 hours.

    So it think EmailNotify should not check how long it has been but checks if the last messages it sent are different than the messages it got back from \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages()

    getPublicServiceMessages() is already using a cache so it won't check that often.

    Changing testPsaEmail() to test that a email doesn't be sent if the messages have not changed and that the email is sent when the message does change.

    Using \Drupal\update_test\Datetime\TestTime to test the cache expires and the feed is checked again. But until #3113971: Replace REQUEST_TIME in services fixed this will not work. So copying just the changes to \Drupal\Core\Cache\DatabaseBackend that are needed for now. Hopefully we can get that issue committed soon.

  4. Added remaining tasks item to make a list of bugs fixed and other improvements to backport to contrib module
tedbow’s picture

Status: Needs review » Needs work

The last submitted patch, 43: 3041885-43.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Issue summary: View changes
Status: Needs work » Needs review
Related issues: +#3090359: Consider including all PSAs in the Drupal.org json feed, letting the module filter which ones to display
StatusFileSize
new10.43 KB
new56.04 KB
  1. coder fixes
  2. Added 2 items in "Remaining tasks"
  3. Not sure why the test failed in #43. I confirm it passed locally and fails on drupalci. Probably has something to do with faking the time. This patch will fail the same way.

Status: Needs review » Needs work

The last submitted patch, 46: 3041885-46.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Issue summary: View changes

Updated summary with a list of improvements made here from the contrib module that should be considered for the contrib module.
The project name !== extension name is probably the most critical

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new17.59 KB
new56.09 KB
  1. I could not figure why the tests in #43 and #46 fail on drupalci but not locally. I did a lot testing but I could not figure it out.
  2. I am changing \Drupal\update\Psa\UpdatesPsa::getInstalledVersion() get the core version that way it does for other projects from info returned by \Drupal\update\UpdateManagerInterface::getProjects(). This how the Update module currently determines the core version to determine if there available updates.

    This allows using actually json files instead of the controller class to return the json feed because we can alter the version of core like the patch does with other modules. This is also more like using the XML test fixtures that the Update already use.

    This should make the tests pass.

  3. More testing 😱
    For some reason the tests don't pass on drupalci because the mocking of the date to trigger the json PSA feed to be fetched again when the cache expires. This works locally but not on drupalci. If anybody could see #43 or #46 pass locally for them that would be be great.

    So I have changed the test to manually delete the updates_psa cache to trigger fetching of the PSA.

    Probably we don't need to do 1) to make the tests pass but I think using actual json files is better anyways. Also getting the core version the same way we do for other projects and the same way that other parts of the update module do is better.

Status: Needs review » Needs work

The last submitted patch, 49: 3041885-49.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new3.37 KB
new54.37 KB
  1. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,309 @@
    +        $response = $this->httpClient->get($psa_endpoint)
    +          ->getBody()
    +          ->getContents();
    

    After a ton of more debugging it seems this line is the problem.

    I looked at \Drupal\update\UpdateFetcher::fetchProjectData() I noticed it did not call getContents() but instead casts the result of getBody() to a string.

    I am not sure why but I am guessing because \Psr\Http\Message\StreamInterface::getContents() says it

    Returns the remaining contents in a string

    So if it has been called before you have to call \Psr\Http\Message\StreamInterface::rewind() set it back to the beginning of the stream. Not sure why that would be different on drupalci.

    @longwave and @laurii also tested by debug patch and they saw the same fails.

  2. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,280 @@
    +    // Deleting the PSA cache will not result in another email if the messages
    +    // have not changed.
    +    $this->cache->delete('updates_psa');
    

    Added a @todo here for #3113971: Replace REQUEST_TIME in services so that we can update this test to just mock the date and test the cache gets cleared

tedbow’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

updating the summary

xjm’s picture

Issue tags: +Needs usability review

Let's review this in the UX meeting next week.

tedbow’s picture

Issue summary: View changes
xjm’s picture

Thoughts on the points in the IS:

  1. I don't think we need this setting. The announcements are rare as it is and less config == better.
     

  2. I agree using notification.emails would make sense (rather than a new setting) for the reasons mentioned.
     

  3. Let's file an issue for this against d.o. Sometimes the PSA is about an upcoming release, but not always. And I agree for UX reasons we would ideally stop displaying the message once the user has updated to the version that addresses the issue. I think this could be a followup.
     

  4. Per @drumm the only way these items get into the feed is via a Drush command that someone with prod access on d.o runs. I think that's extreme enough that we should display everything in the feed for a project in the site's codebase. I asked @drumm if it could be a UI config option on PSAs, but even then, we should trust the sec team's judgement that the item is worth displaying on the status report. PSAs are not typically assigned a severity so I don't think we need extra logic for that.
     

  5. I agree that we should display the PSA for anything in the site codebase, installed or not. As discussed, even uninstalled modules can expose the site to security risks for certain vulnerabilities. I can think of several such vulnerabilities in the past 5 years.

xjm’s picture

Status: Needs review » Needs work

I think NW to address points 1, 2, and 4. There are also a number of CS errors:

FILE: ...aintainer/core/modules/update/config/install/update.settings.yml
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 15 | ERROR | [x] Expected 1 newline at end of file; 2 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 56ms; Memory: 8MB

PHPCS: core/modules/update/config/schema/update.schema.yml passed
PHPCS: core/modules/update/src/Psa/EmailNotify.php passed
PHPCS: core/modules/update/src/Psa/NotifyInterface.php passed
PHPCS: core/modules/update/src/Psa/SecurityAnnouncement.php passed
PHPCS: core/modules/update/src/Psa/UpdatesPsa.php passed
PHPCS: core/modules/update/src/Psa/UpdatesPsaInterface.php passed

FILE: ...jm/git/maintainer/core/modules/update/src/UpdateSettingsForm.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 1 LINE
----------------------------------------------------------------------
 145 | ERROR | [x] No space found after comma in argument list
 145 | ERROR | [x] Expected one space after the comma, 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 62ms; Memory: 8MB

PHPCS: core/modules/update/tests/src/Functional/PsaTest.php passed

FILE: /Users/xjm/git/maintainer/core/modules/update/update.install
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 16 | WARNING | [x] Unused use statement
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 76ms; Memory: 8MB


FILE: /Users/xjm/git/maintainer/core/modules/update/update.module
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 23 | WARNING | [x] Unused use statement
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
xjm’s picture

  1. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +class EmailNotify implements NotifyInterface {
    +  use StringTranslationTrait;
    

    I believe our coding standards specify a newline between the opening curly and the first line here (although it didn't fail phpcs).

  2. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
    +   *   The mail manager.
    +   * @param \Drupal\update\Psa\UpdatesPsaInterface $updates_psa
    +   *   The automatic updates service.
    +   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    +   *   The config factory.
    +   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    +   *   The language manager.
    +   * @param \Drupal\Core\State\StateInterface $state
    +   *   The state service.
    +   * @param \Drupal\Component\Datetime\TimeInterface $time
    +   *   The time service.
    +   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    +   *   Entity type manager.
    +   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    +   *   The string translation service.
    +   * @param \Psr\Log\LoggerInterface $logger
    +   *   The logger.
    

    Inane observation: That's a lot of services.

  3. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +  public function send() {
    ...
    +  protected function doSend(string $email, array $params) {
    
    +++ b/core/modules/update/src/Psa/NotifyInterface.php
    @@ -0,0 +1,15 @@
    +  public function send();
    
    +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +  public static function createFromArray(array $data) {
    ...
    +  protected static function validateAnnouncementData(array $data): void {
    
    +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +  public static function getErrorMessageFromException(\Exception $exception, bool $throw_unexpected_exceptions = TRUE) {
    
    +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +  public function testPsa() {
    ...
    +  public function testPsaMail() {
    ...
    +  public function testInvalidJsonEmail() {
    ...
    +  private function setSettingsViaForm(string $checkbox, bool $enable) {
    ...
    +  protected function getPsaEmails() {
    
    +++ b/core/modules/update/update.install
    @@ -177,3 +181,36 @@ function _update_requirement_check($project, $type) {
    +function _update_psa_requirements(array &$requirements) {
    

    Can we add return typehints where feasible?

  4. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +    $messages_hash = hash('sha256', serialize($messages));
    +    // Return if the messages are the same as the last messages sent.
    +    if ($messages_hash === $this->state->get(static::LAST_MESSAGES_STATE_KEY)) {
    +      return;
    +    }
    

    Non-blocking: This seems like it is probably code repeated from elsewhere and probably should be API or internal functionality for the messaging service rather than hardcoded logic in the caller.

  5. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +      '@count urgent Drupal announcement requires your attention for @site_name',
    +      '@count urgent Drupal announcements require your attention for @site_name',
    

    I think we have a practice of not putting the word "Drupal" in the UI anywhere since it can be branded as something else. Should we say "security announcements"?

  6. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,180 @@
    +   * @param array $params
    +   *   Parameters to build the email.
    
    +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +  /**
    +   * The currently insecure versions of the project.
    +   *
    +   * @var array
    +   */
    +  protected $insecureVersions;
    ...
    +   * @param array $insecure_versions
    ...
    +   *   a list of versions that will be insecure when the security release is
    ...
    +   * @param array $data
    +   *   The security announcement data as returned from the JSON feed.
    ...
    +   * @param array $data
    +   *   The announcement data.
    
    +++ b/core/modules/update/update.install
    @@ -177,3 +181,36 @@ function _update_requirement_check($project, $type) {
    + * @param array $requirements
    + *   The requirements array.
    
    +++ b/core/themes/stable9/templates/admin/updates-psa-notify.html.twig
    @@ -0,0 +1,38 @@
    + * Available variables:
    + * - messages: The messages array
    

    Can you guess what I'm going to say? ;)

     
     
     
     
     
     

    As always, let's use a more specific data type for the array if possible, and describe and/or reference the canonical array structure for associative or mixed arrays.

    Also, no apostrophe in "PSAs".

  7. +++ b/core/modules/update/src/Psa/NotifyInterface.php
    @@ -0,0 +1,15 @@
    + * Defines an interface for sending notification of update PSA's.
    
    +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +    // No email should be sent if PSA's are disabled even the endpoint has
    

    Nit: there should not be an apostrophe in "PSAs". (Here and elsewhere in the patch where it's a plural rather than a possessive.)

  8. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +        $volition_messages[] = (string) $violation;
    ...
    +      throw new \UnexpectedValueException(implode(",  \n", $volition_messages));
    

    I think they are violation messages, not volition messages? The words mean different things.

  9. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    + * Service class to get Public Service Messages.
    

    Blah blah start with a verb etc.

  10. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +class UpdatesPsa implements UpdatesPsaInterface {
    +  use StringTranslationTrait;
    +  use DependencySerializationTrait;
    

    Leading space.

  11. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +   * This module's configuration.
    

    What module's configuration?

  12. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +   * The http client.
    

    Nit: "HTTP" should be uppercase in code comments.

  13. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +        $this->cache->set('updates_psa', $response, $this->time->getCurrentTime() + $this->config->get('psa.check_frequency'));
    

    Interesting. This seems like it should be stored in state rather than cache? Otherwise a cache clear blows it away and resets it.

    Also, we seem to be storing prognostication about when we should check next, rather than when we checked last. I'd expect the other way around, with a check on the other side to decide whether it's long enough ago relative to the config.

    However, we should primarily mimic whichever pattern the update module already does when checking for updates.

  14. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +          $this->logger->error('PSA malformed: ' . $unexpected_value_exception->getMessage());
    +          throw new \UnexpectedValueException($unexpected_value_exception->getMessage(), static::MALFORMED_JSON_EXCEPTION_CODE);
    ...
    +      $this->logger->error('Drupal PSA JSON is malformed: @response', ['@response' => $response]);
    +      throw new \UnexpectedValueException('Drupal PSA JSON is malformed.', static::MALFORMED_JSON_EXCEPTION_CODE);
    

    Logger and an exception? Do we really need to log messages about malformed PSAs? The site owner can't do anything about that.

    Come to think of it, should we even be throwing an exception? (For the same reason.)

  15. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +      $this->logger->error($exception->getMessage());
    

    Same question about the use of the logger.

  16. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +   * Determines if the Psa versions match for the installed version of project.
    

    Nit: "PSA" should be uppercase when used in a sentence.

  17. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +   * @param \Drupal\update\Psa\SecurityAnnouncement $sa
    +   *   The security announcement.
    ...
    +   * @param \Drupal\update\Psa\SecurityAnnouncement $sa
    +   *   The security announcement.
    ...
    +   * @param \Drupal\update\Psa\SecurityAnnouncement $sa
    +   *   The security announcement.
    ...
    +  private function getInstalledVersion(SecurityAnnouncement $sa) : string {
    

    An SA is not the same thing as a PSA. I think the parameter name should be $psa.

  18. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +   *   TRUE if security announcement matches the installed version of the
    +   *   project, otherwise FALSE.
    

    Nit: Comma splice. Either the word "or" or a semicolon would resolve this.

  19. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,307 @@
    +      // If the installed version can not be parsed assume it matches to avoid
    

    Nit: "cannot" should be one word. (MURIKA!) Also, there should be a comma before "assume".

  20. +++ b/core/modules/update/templates/updates-psa-notify.html.twig
    @@ -0,0 +1,38 @@
    +<p>
    +  {% trans %}
    +    A security update will be made available soon for your Drupal site. To ensure the security of the site, you should prepare the site to immediately apply the update once it is released!
    +  {% endtrans %}
    +</p>
    +<p>
    +  {% set status_report = path('system.status') %}
    +  {% trans %}
    +    See the <a href="{{ status_report }}">site status report page</a> for more information.
    +  {% endtrans %}
    +</p>
    +<p>{{ 'Public service announcements:'|t }}</p>
    +<ul>
    +  {% for message in messages %}
    +    <li>{{ message }}</li>
    +  {% endfor %}
    +</ul>
    +<p>
    +  {{ 'To see all PSAs, visit <a href="@uri">@uri</a>.'|t({'@uri': 'https://www.drupal.org/security/psa'}) }}
    +</p>
    +<p>
    +  {% set settings_link = path('update.settings') %}
    +  {% trans %}
    +    Your site is currently configured to send these emails when a security update will be made available soon. To change how you are notified, you may <a href="{{ settings_link }}">configure email notifications</a>.
    +  {% endtrans %}
    +</p>
    

    Let's get UX review for all this text.

    Also, I think we should not use the abbreviation "PSA" in the UI. We should always say "public service announcement".

  21. +++ b/core/modules/update/tests/fixtures/psa_feed/invalid.json
    @@ -0,0 +1 @@
    +[{"title":"You can't parse this! Oh no! 🔥🙀🐶
    

    approve

  22. +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    @@ -0,0 +1,85 @@
    +      "8.5.14",
    +      "8.5.14",
    ...
    +      "8.6.15",
    +      "8.6.15",
    +      "8.5.15",
    +      "8.5.15",
    
    +++ b/core/modules/update/tests/fixtures/psa_feed/valid_plus1.json
    @@ -0,0 +1,96 @@
    +      "8.5.14",
    +      "8.5.14",
    ...
    +      "8.6.15",
    +      "8.6.15",
    +      "8.5.15",
    +      "8.5.15",
    

    Why are these versions in the list twice?

  23. +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    @@ -0,0 +1,85 @@
    +    "title":"AAA Update Project - Moderately critical - Access bypass - SA-CONTRIB-2019",
    ...
    +    "title":"AAA Update Test - Moderately critical - Access bypass - SA-CONTRIB-2019",
    ...
    +    "title":"BBB Update project - Moderately critical - Access bypass - SA-CONTRIB-2019",
    ...
    +    "title":"Missing Project - Moderately critical - Access bypass - SA-CONTRIB-2019",
    

    I know this is only test data, but the PSA title will always have a full date, not just a year. Otherwise we could only issue one per year.

  24. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected $defaultTheme = 'stark';
    +
    

    🤔 Do we need to override the default? (We already made Stark the default test theme in D9, right?) Does the theme even matter?

  25. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +   * A test end PSA endpoint that returns invalid JSON.
    

    Nit: I think this is supposed to say "A test PSA endpoint" (not "A test end PSA endpoint").

  26. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +  protected function setUp() :void {
    

    Missing space between colon and typehint.

  27. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +        'version' => '9.11.0',
    

    Inane observation on test data: Hot damn, 9.11.0. This is a version that should never be released. 🤞

  28. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +    // Test transmit errors with JSON endpoint.
    

    Not really a sentence.

  29. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,283 @@
    +    // Setup test PSA endpoint.
    

    Nit: "Set up" should be two words.

  30. +++ b/core/modules/update/update.install
    @@ -177,3 +181,36 @@ function _update_requirement_check($project, $type) {
    +  $requirements['update_psa'] = [
    +    'title' => t('Public service announcements'),
    +    'severity' => REQUIREMENT_OK,
    +    'value' => t('No announcements requiring attention.'),
    

    I don't think we should display any message if there are no PSAs as it would be non-actionable clutter on the status report. We should validate this in the UX review, at least.

tedbow’s picture

Assigned: Unassigned » tedbow
Status: Needs work » Needs review
StatusFileSize
new54.42 KB

@xjm thanks for the review!

  1. skipping #55 to fix clear code problems in this patch in next to comments. Will come back to this in next patch
  2. Fixed phpcs problem in #56

re #57

  1. fixed
  2. I agree
  3. Adding type hints. I couldn't type hint \Drupal\update\Psa\UpdatesPsa::getErrorMessageFromException() because it returns TranslatableMarkup|string
  4. Not sure what you mean by "messaging service" here. Maybe \Drupal\update\Psa\UpdatesPsa? This is where $messages comes from.

    In this case I think it only makes sense for the email context not send the messages again. The other callers of \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages() probably still want to display the message even if they have been displayed before.

    Here the intention really is "don't email the same messages 2x". Only the caller here knows if they have been emailed.

  5. fixed
  6. Changed type array type hints to more specific such as string[]
  7. fixed
  8. fixed for this and the other classes
  9. fixed
  10. Changed 'update.settings' because there could be other configuration added for the update module.
  11. fixed
  12. The Update module currently uses \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface::setWithExpire() to store available release on a per project basis in \Drupal\update\UpdateProcessor::processFetchTask()

    Changing to use that here but simpler because we are only dealing with 1 feed.

  13. I have remove the logging for the exceptions.

    I still think we should throw the exceptions. Maybe in the email notification and the displaying on the admin we don't need to say anything if the feed is malformed but I think on the status report we should. Hopefully this feed will never be malformed but if it is it also could be the case that there is something wrong with the fetching of json file on the clients network and not a problem with drupal.org. If we don't display a message on the status report the site admin would never know this and if I PSA actually was in the feed they won't get it.

    Also I think any caller of \Drupal\update\Psa\UpdatesPsa::getPublicServiceMessages() should be able to tell the difference of between a feed with no items and problem with the feed. Without the exception this is not possible.

  14. logger removed
  15. fixed
  16. This will only be called if is_psa is false because it is called for via matchesInstalledVersion() from here
    if ($sa->isPsa() || $this->matchesInstalledVersion($sa)) {
              $messages[] = $this->message($sa);
    }
    

    Because if it is a PSA we don't care what the installed version because as the feed currently works insecure will not tell use for a PSA whether it will affect the installed version when the fix is actually released.

    Leaving as $sa for now.

  17. fixed
  18. fixed
  19. changed to "public service announcement"
  20. 😂👍
  21. Not intentional, fixed
  22. fixed
  23. Yep still needed. If it is not set the test has error that points to this https://www.drupal.org/node/3083055
  24. fixed
  25. fixed
  26. Anything can happen in the alternate test universe 😉
  27. fixed
  28. fixed
  29. agreed, fixed

assigning to myself to address #55

tedbow’s picture

StatusFileSize
new33.46 KB

whoops here is the interdiff

tedbow’s picture

StatusFileSize
new8.92 KB
new51.85 KB

back to #55

  1. removed the setting
  2. removed the setting
  3. @todo Need to file the issue
  4. Leaving as is
  5. Add @todo in the code. Will need resolve before this patch is finished. Need a bigger change to not use project manager because of how it caches the results it won't work.
tedbow’s picture

StatusFileSize
new15.18 KB
new55.63 KB

Taking care of #55.5 showing messages for extensions that are not installed.

This meant we can't use the UpdateManager to get projects because it caches the results and doesn't get uninstalled extensions unless the setting is enabled.

So had to change to not returning the test json files directly because we can't change the version of number of Drupal core. So added test controller to swap out the core version dynamically in the test json fixtures.

+++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
@@ -0,0 +1,81 @@
+    "type":"theme",

This should have been 'module'. Didn't catch this before because \Drupal\update\UpdateManagerInterface::getProjects() returned all projects regardless of type and we weren't checking.

tedbow’s picture

StatusFileSize
new5.06 KB
new55.03 KB

Some more self review

  1. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * @param string $insecure_versions
    

    This should be string[]

  2. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,308 @@
    +  protected function isValidProject(string $project_name, string $project_type) : bool {
    +    try {
    +      $project = $this->getProjectInfo($project_name, $project_type);
    +      return !empty($project['version']);
    +    }
    +    catch (\UnexpectedValueException $exception) {
    +      return FALSE;
    +    }
    +  }
    

    Here all we care about is the project version so we can remove this and just call the new getProjectVersion

  3. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,308 @@
    +  private function getInstalledVersion(SecurityAnnouncement $sa) : string {
    

    We no longer are just worried about installed extension. Also here we actually are getting version constraint that we can compare. Changing the name to getExistingVersionConstraint()

  4. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,308 @@
    +  protected function getProjectInfo(string $project_name, string $project_type): array {
    

    In the places this is called all we actually need is the project version. So changing this to getProjectVersion and returning an empty string if it is not available.

tedbow’s picture

Issue summary: View changes
StatusFileSize
new71.57 KB
new27.71 KB
new16.59 KB
new6.44 KB
new53.06 KB
  1. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,183 @@
    +    if ($user = reset($users)) {
    +      $params['langcode'] = $user->getPreferredLangcode();
    +      $this->mailManager->mail('update', 'psa_notify', $email, $params['langcode'], $params);
    +    }
    

    This will only send an email if there is matching user. This is different than what _update_cron_notify() currently does. Changing to still send the email with the default language code if there is no user for the email.

  2. +++ b/core/modules/update/src/UpdateSettingsForm.php
    @@ -88,6 +88,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +    $form['psa'] = [
    +      '#type' => 'details',
    +      '#title' => $this->t('Public service announcements'),
    +      '#open' => TRUE,
    +    ];
    +    $form['psa']['description'] = [
    +      '#markup' => '<p>' . $this->t('Public service announcements are compared against the entire code for the site, not just installed extensions.') . '</p>',
    +    ];
    

    Removed the rest of the changes to the settings form since we have no new settings now

  3. +++ b/core/modules/update/update.module
    @@ -19,6 +19,7 @@
    @@ -122,6 +123,24 @@ function update_page_top() {
    
    @@ -122,6 +123,24 @@ function update_page_top() {
    +
    +    /** @var \Drupal\update\Psa\UpdatesPsaInterface $psa */
    +    $psa = \Drupal::service('update.psa');
    +    try {
    +      $messages = $psa->getPublicServiceMessages();
    

    I just realized that we should be showing these messages on all admin page except the status report. We were excluding the same pages as available updates but that list for exclusion is based on where the user maybe seeing or uploading new updates.

  4. Added screenshot for the UX review
xjm’s picture

Issue summary: View changes

Moving screenshots to the right section of the UI.

tedbow’s picture

Issue summary: View changes
Issue tags: -Needs usability review
StatusFileSize
new69.32 KB
new117.34 KB
new44.49 KB
new6.88 KB
new53.32 KB

Wording changes from the UX meeting.

xjm’s picture

Issue summary: View changes

Fixing the sample email to not reference links that got removed.

tedbow’s picture

Assigned: tedbow » Unassigned
benjifisher’s picture

Assigned: Unassigned » tedbow
benjifisher’s picture

Assigned: tedbow » Unassigned

Oops, I meant to decline credit, not assign to @tedbow.

dww’s picture

Status: Needs review » Needs work

Finally had a chance to closely review this. Mostly looking good. This is going to be a big improvement! Thanks to everyone who worked on this.

As is often the case when I review a huge patch, tons of pedantic nits, with a few items of substance...

  1. +++ b/core/modules/update/config/schema/update.schema.yml
    --- /dev/null
    +++ b/core/modules/update/src/Psa/EmailNotify.php
    
    +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +namespace Drupal\update\Psa;
    

    PSA is an acronym. What's the deal with namespaces and directories in this case? I guess core's "JsonApi" is an example of CamelCasing something that "should" be JSONAPI, so I guess this is fine. ;)

  2. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +   * The automatic updates service.
    ...
    +   *   The automatic updates service.
    

    This doesn't (really) have anything to do with automatic updates, right?

  3. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +   * @var \Drupal\update\Psa\UpdatesPsaInterface
    ...
    +  protected $updatesPsa;
    

    Curious why 'Updates' is plural here (and everywhere). We're adding this code to the 'Update Manager', not 'Updates Manager'...

  4. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +   * Entity type manager.
    ...
    +   *   Entity type manager.
    

    Every other one of these var comments starts with 'The'.

  5. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +      $this->logger->error($this->t(
    +        'Unable to send notification email because of error retrieving PSA feed: @error'),
    +        ['@error' => UpdatesPsa::getErrorMessageFromException($exception, FALSE)]
    +      );
    

    I'm probably tired and wrong, but it seems like the ['@error'] array is in the wrong place here. It's the 2nd arg to error(), not t()...

  6. +++ b/core/modules/update/src/Psa/EmailNotify.php
    @@ -0,0 +1,165 @@
    +      $users = $this->entityTypeManager->getStorage('user')
    

    Do we want to call getStorage('user') once outside of this loop, instead?

  7. +++ b/core/modules/update/src/Psa/NotifyInterface.php
    @@ -0,0 +1,15 @@
    +/**
    + * Defines an interface for sending notification of update PSAs.
    + */
    +interface NotifyInterface {
    

    Reading this patch from the top down, so maybe this is answered later, but I'm not clear why we need an interface for this. Are we expecting this to be a public API and contrib / custom are going to implement their own versions of this interface?

  8. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    + * These come form the PSA feed on drupal.org.
    

    s/form/from/

  9. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * Whether this announce is PSA instead of another type of announcement.
    

    a) s/announce/announcement/

    b) s/is PSA/is a PSA/

    c) What other types of announcements are there? I'm not clear when this is ever FALSE.

  10. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +  protected $isPsa;
    

    Pedantic aside: I still keep doing double takes on these. ;) Seems like isPSA would be more accurate...

  11. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   *   The version of the project that currently insecure. For PSAs this is not
    +   *   a list of versions that will be insecure when the security release is
    +   *   published.
    

    This comment doesn't parse. 1st sentence is missing 'is'. 2nd sentence 'not' throws me way off. What is this really trying to say?

  12. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * @param mixed[] $data
    ...
    +   * @param mixed[] $data
    

    xjm will probably say "yes", but is 'mixed[]' any better or more specific than 'array' for these? ;)

  13. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +    $new_blank_constraints = [
    ...
    +      new NotBlank(),
    

    Confused by an array called '$new_blank_constraints' with a NotBlank() constraint.

  14. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * Whether the security announcement is PSA or not.
    

    s/is PSA/is a PSA/

  15. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   *   TRUE if the announcement is a PSA otherwise false.
    

    a) Needs a comma before 'otherwise'.
    b) s/false/FALSE/

  16. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * Gets the currently insecure version of the project.
    

    s/version/versions/

  17. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   * Gets the links to the security announcement.
    

    s/links/link/

  18. +++ b/core/modules/update/src/Psa/SecurityAnnouncement.php
    @@ -0,0 +1,206 @@
    +   *   The link.
    

    Maybe "The link to the PSA."?

  19. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    + * Defines a service class to get Public Service Messages.
    

    s/Messages/Announcements/

  20. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * This 'update.settings' configuration.
    

    s/This/The/

  21. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * Update key/value store.
    

    Start with 'The'?

  22. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * The extension lists.
    ...
    +    $this->extensionLists['module'] = $module_list;
    +    $this->extensionLists['theme'] = $theme_list;
    +    $this->extensionLists['profile'] = $profile_list;
    

    Maybe worth documenting it's keyed by extension type?

  23. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +  public function getPublicServiceMessages() : array {
    ...
    +   * Gets a message from an exception thrown by ::getPublicServiceMessages().
    ...
    +   *   The exception throw by ::getPublicServiceMessages().
    

    s/Messages/Announcements/

  24. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +        if ($sa->getProjectType() !== 'core' && !$this->getProjectVersion($sa)) {
    +          continue;
    +        }
    

    I don't fully understand why we skip contrib SAs without a version. Maybe this needs a comment? Or maybe I'm misunderstanding...

  25. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +      throw new \UnexpectedValueException('Drupal PSA JSON is malformed.', static::MALFORMED_JSON_EXCEPTION_CODE);
    

    I wonder what a site admin is supposed to do when they see this exception message. ;)

  26. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *    Throw if the exception is not expected.
    

    s/Throw/Thrown/

  27. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * Determines if the PSA versions match for the installed version of project.
    

    wants 'a' before project. But then it'd be past 80 chars. Maybe:

    "Determines if the PSA versions match the installed version of a project." ?

  28. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *   TRUE if security announcement matches the installed version of the
    

    s/if security/if the security/

  29. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *   project; otherwise FALSE.
    

    s/;/,/

  30. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * @throws \UnexpectedValueException
    +   *   Thrown by \Composer\Semver\VersionParser::parseConstraints() if the
    +   *   constraint string is not valid.
    

    Do we document exceptions thrown by functions we call? That seems clumsy. I thought the whole point of exceptions is they can happen anywhere in the call stack...

  31. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +        // If an individual constraint is throws an exception continue to check
    +        // the other versions.
    

    This doesn't parse. Maybe:

    "If an individual constraint throws an exception, continue to check the other versions." ?

  32. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * Returns a message that links the security announcement.
    

    s/links the/links to the/

  33. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *   The PSA or SA message.
    

    Maybe: "A link to the upstream announcement using the message as the text." or something? It's not just returning the message, the interesting fact is that it's hard-coded to be a link tag.

  34. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * Gets the contrib version to use for comparisons.
    

    s/version/versions/

  35. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *   The versions that can be used for comparison.
    

    I'd love this comment to define what makes a version valid to use for comparison, so I wouldn't have to read the code to figure it out.

    Now that I've read the function, it seems more like a "Convert potentially BespokeVer Drupal versions into SemVer-esque versions for comparisons" function. Right? Maybe mention something about that?

    p.s. Don't we already have code like this somewhere else in Update Manager or in a core utility or component class? If not, shouldn't we?

  36. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +  private function getContribVersions(array $versions) : array {
    

    Why private instead of protected?

  37. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +      if ($version_array && $version_array[0] === \Drupal::CORE_COMPATIBILITY) {
    

    I thought we were trying to kill the CORE_COMPATIBILITY constant. ;)

  38. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   * Gets the project version.
    

    Gets which project version? ;) How is this different from getExistingVersionConstraint()? This comment could use some clarification.

  39. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +   *   The project version or an empty string if the project is not available.
    

    What does "the project is not available" mean? Is this "the project is not installed"? "Not enabled"? Other?

  40. +++ b/core/modules/update/src/Psa/UpdatesPsa.php
    @@ -0,0 +1,285 @@
    +    static $extensions = [];
    

    Static var since ExtensionList::getList() is too expensive? Seems like this will use a lot of RAM for something we're already storing in RAM. Perhaps worth a comment to justify this. Or maybe rip it out? Not sure...

  41. +++ b/core/modules/update/src/Psa/UpdatesPsaInterface.php
    @@ -0,0 +1,18 @@
    + * Defines an interface to get Public Service Messages.
    ...
    +   * Gets public service messages.
    ...
    +  public function getPublicServiceMessages() : array;
    

    s/Messages/Announcements/

  42. +++ b/core/modules/update/src/Psa/UpdatesPsaInterface.php
    @@ -0,0 +1,18 @@
    +   *   A array of translatable strings.
    

    A) s/A/An/
    B) Does this want to be more specific?

  43. +++ b/core/modules/update/templates/updates-psa-notify.html.twig
    @@ -0,0 +1,35 @@
    + * Default theme implementation for the public service announcements email notification.
    

    Should we use 'PSA' here to avoid overflowing 80 chars?

  44. +++ b/core/modules/update/templates/updates-psa-notify.html.twig
    @@ -0,0 +1,35 @@
    + * - messages: An array of message strings.
    

    Would maybe be better if this variable was 'announcements'...

  45. +++ b/core/modules/update/templates/updates-psa-notify.html.twig
    @@ -0,0 +1,35 @@
    +    An important security announcement is available for your Drupal site. You should read the announcement immediately and follow its instructions.
    

    Should this care about plural?

  46. +++ b/core/modules/update/templates/updates-psa-notify.html.twig
    @@ -0,0 +1,35 @@
    +<p>
    +  {% set settings_link = path('update.settings') %}
    +</p>
    

    Looks like this <p> is missing any visible payload.

  47. +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    @@ -0,0 +1,81 @@
    +    "insecure":[
    +
    +    ],
    

    Is this right?

  48. +++ b/core/modules/update/tests/fixtures/psa_feed/valid.json
    @@ -0,0 +1,81 @@
    +      "8.x-8.7.0"
    ...
    +      "8.x-8.7.0"
    

    Is this here for things we can't parse?

  49. +++ b/core/modules/update/tests/fixtures/psa_feed/valid_plus1.json
    @@ -0,0 +1,92 @@
    +    "title":"Critical Release - PSA because 2020",
    

    LMAO! ;) Love it!!

  50. +++ b/core/modules/update/tests/modules/update_test/src/Controller/PsaTestController.php
    @@ -0,0 +1,34 @@
    +   * Gets the contents of JSON file.
    

    More like "Reads a JSON file and returns the contents as a Response." or something, right?

    Should it also mention the magic about replacing [CORE_VERSION] with the current version of core?

  51. +++ b/core/modules/update/tests/modules/update_test/src/Controller/PsaTestController.php
    @@ -0,0 +1,34 @@
    +   *   The test response.
    

    Can this explain when you get a JsonResponse vs. a regular Response? Response is only for a 404 error condition for file-not-found, right?

  52. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,248 @@
    +    // Alter the 'aaa_update_test' to use the 'aaa_update_project' project name.
    +    // The PSA feed will match project name and not extension name.
    

    Not sure why we're doing this. Why not call the project name 'aaa_update_test' in the feed fixtures? Or this is here to test the case where the module name != project name? In that case, maybe spell that out more clearly in the comment?

  53. +++ b/core/modules/update/tests/src/Functional/PsaTest.php
    @@ -0,0 +1,248 @@
    +   * Tests sending an email when the PSA JSON is invalid.
    

    Looks like it tests that we don't send email for malformed PSA JSON...

  54. +++ b/core/modules/update/update.install
    @@ -177,3 +180,33 @@ function _update_requirement_check($project, $type) {
    + * Display requirements from Public service announcements.
    

    why capitalize Public?

  55. +++ b/core/modules/update/update.services.yml
    --- /dev/null
    +++ b/core/themes/stable/templates/admin/updates-psa-notify.html.twig
    

    Same comments on this template above apply to these copies in stable and stable9...

tedbow’s picture

Assigned: Unassigned » tedbow
Status: Needs work » Needs review
StatusFileSize
new12.67 KB
new53.34 KB

@dww thanks for the review!

  1. Yeah my understanding is acronyms still follow the same casing rule for class names and name spaces
  2. fixed
  3. Changing to Update singular

    Looking at the name of this service in particular I don't think we need "Update" in it at all. After talking with @xjm and other in security team, strictly speaking a PSA could be a notification about a security problem that can't be covered by an update.
    So changing this to UpdateFetcherInterface. I think the PSA feed simple enough that we don't need the "Processor" "Manager" and "Fetcher" that we do for the actual update XML logic.

    Other naming changes in this patch. Some related, some just giving harder look:

    • update-psa-notify.html.twig
    • changing from $this->tempStore->get('updates_psa'); to $this->tempStore->get('psa_response');. Here tempStore is already the update tempstore so we don't need to repeat it.
    • Service update.psa to update.psa_fetcher. We already have the update.psa_notify service so this makes it more clear what their responsibilities are.
    • logger.channel.update

Ok going to update with the above changes because the naming changes make a 13k interdiff and splitting it should make reviewing easier.

Assigning to myself because I am continue to address the rest of #71

tedbow’s picture

Issue summary: View changes
StatusFileSize
new3.39 KB
new52.57 KB

Continuing with #71

  1. Fixed and also a similar problem in the newly rename PsaFetcher
  2. You are correct. But also I checked other calls to \Psr\Log\LoggerInterface::error() and they don't all a t() function. Removing
  3. fixed
  4. I also think it is better idea not to add a public API. see my previous comment under "API changes" in the summary.

    I am going to remove the 2 services that this patch adds.

    I think if there is case for custom/contrib code to need the interface the case can be made later. It seems like it would be such a rare case to want to want to change this functionality that it would be almost as simple for custom code to just fetch the json feed themselves.

    The problem in that case would be that our services would still be doing their thing so they might get duplicate messaging.

    I am going to add Drupal::hasService('update.psa_*')

  5. checks so that custom code can still just unset our services if they want to write their own functionality to handle PSAs.

  6. fixed
  7. fixed a,b
    c, the current docs on https://www.drupal.org/docs/8/update/automatic-updates#s-public-service-... say

    The flag which indicates that the post is a PSA, and not another kind of Security Advisory.

    It could be just a "Security Advisory". I am not super clear on the distinction since by the very fact they are published make them all Public.
    I think the distinction is a PSA is something that is released before an upcoming release where as a SA if for a security vulnerability or fix that is already released.
    Maybe @xjm @drumm or somebody else from the security team could confirm this.

  8. yep

    What I found https://www.drupal.org/docs/develop/standards/object-oriented-code#naming is

    If an acronym is used in a class or method name, make it CamelCase too (SampleXmlClass, not SampleXMLClass). [Note: this standard was adopted in March 2013, reversing the previous standard.]

    it doesn't explicitly say anything about properties. I did find \Drupal\jsonapi\Routing\Routes::$jsonApiBasePath where JSON and API are both acronyms.

    I couldn't think of other acronyms to grep for. Didn't find "xml" or "yml" used as properties.

  9. changed to
       *   The versions of the project that are currently insecure. For PSAs this
       *   list does include versions that will be marked as insecure when the new
       *   security release is published.
    

    Basically PSA are for future releases, a security release will made available very soon.
    So if there is PSA for Drupal core, meaning a warning about an upcoming security release,
    $insecure_versions would contain all the versions from https://updates.drupal.org/release-history/drupal/current NOW that have the term, Release type == insecure.
    It does include versions will be insecure after the upcoming security release.

    We can't this in JSON feed because it is already used by the contrib module and also it could be used by other custom code.

  10. i guess I would say "yes" also. I guess this is explicitly saying you know there will be elements of different types vs "array" might say that or it might say just forgot to update to something more specific
  11. meant to be $not_blank_constraints

    fixed

  12. fixed
  13. fixed
  14. fixed
  15. fixed
  16. fixed
  17. fixed
  18. fixed
  19. fixed
  20. fixed

Have to break. I will continue on with #71 after a couple hours

tedbow’s picture

StatusFileSize
new7.24 KB
new58.19 KB

Continuing with #71 (thanks @dww a lot good points and questions!)

  1. Skipping for now to avoid changing multiple times.

    Agreed it should be changed but I am trying to get clarification now as to what proper naming should be.
    on https://www.drupal.org/security/psa
    We have both "Security Advisory" and "Security Announcement". @drumm has said on drupal.org he is moving towards "Security Advisory" because it is more standard across projects.

    From the feed it seems that a PSA is special type of "Security Advisory". Right now the feed and the command on drupal.org to create the feed handle both.
    So we may want make more changes like change the newly renamed "PsaFetcher" to "SecurityAdvisoryFetcher" since it fetches both kinds.

  2. This follows the logic of the contrib module where non-core item is shown if it doesn't pass isValidExtension() which requires a version number in the project.

    I think we should figure out what we want to do. for non-psa items the logic now is to only show items that have a match in insecure_versions. I think the reason here is because these aren't for future release so insecure_versions should reflect the versions that are affected and need to updated. So for non-psa items it may make sense to not show if the extension does not have version because we can't determine a match.

    On the other hand maybe we should default to showing non-psa items if we can't determine the existing version, just to be safe.

    Another problem here is that there maybe advisories that can't be solved with an update. Rather they may instruct the admin on some server config change or drupal config change that can't be automated(does someone know a previous example of this?). In that case insecure_versions wouldn't tell us anything. Maybe these will always be PSAs?

Because 24) raises some questions about how this should work and I have some other questions about the current version matching that was brought in from the contrib I am going to pause on address #71 and switch to writing a kernel test where we can clear test cases via a dataProvider. That way I think it will be easier for everyone looking at this patch to have the same understanding of when an item in the feed is displayed and when it is not displayed. We can determine if the current functionality is correct or if it should can change(I think it is probably not correct)

Couple of questions to address

  1. if I extension does not have a version what should we do for non-psa and psa?
  2. How do determine if an advisory is a current security vulnerability that can't be solved with an update? Will these happen?

    Will these always be is_psa === 1? If so it is easier because we don't need to worry about matching insecure_versions. If not I don't think we have enough information in the feed to tell use whether to check insecure_versions. We don't want to ignore insecure_versions because we don't want show message that we don't have to.

  3. this relates to @dww's question in #71.35

    \Drupal\update\Psa\PsaFetcher::getContribVersions() has logic that converts Drupal module version numbers then in \Drupal\update\Psa\PsaFetcher::matchesInstalledVersion() we use \Composer\Semver\Constraint\ConstraintInterface::matches()

    This handles the case where the installed version is 8.x-1.0 but insecure_version contains 1.0 and the reverse case.
    Would this ever happen? I know why we do this conversion in other parts of the Update module but there we are dealing with greater than/less than versions to determine updates. But here we have exact versions that we are matching.

    We know the version number that should be in the project and we know how the insecure_version array is generated.
    In which cases would not be exact string matches of the versions?

    If we don't need this type of match we can really simplify the logic here.

    for know the test case I added will match 8.x-1.0 == 1.0 just to show the current functionality.

tedbow’s picture

StatusFileSize
new442 bytes
new58.21 KB

forgot @group on new test

Status: Needs review » Needs work

The last submitted patch, 75: 3041885-75.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new476 bytes
new58.21 KB
+++ b/core/modules/update/tests/src/Kernel/PsaFetcherTest.php
@@ -0,0 +1,176 @@
+namespace Drupal\Tests\update\Unit;

I forgot to update the namespace after I changed this from a unit to a Kernel test.

We could make this a unit test but the mocking is pretty complicated. I think the kernel test is more readable and relies more on core real functionality.

Status: Needs review » Needs work

The last submitted patch, 77: 3041885-77.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.57 KB
new64.18 KB
  1. Ok adding more kernel test cases. From talking to @drumm and @heddn I am not sure we want to match all the test cases we are currently matching. I am not changing any of the matching logic just adding test cases to make it clear how it currently works.

    Then I think we can simply the logic and remove the cases where it matches but should not.

  2. The fail in #78 was a random test fail in an unrelated test. It passed when re-ran
tedbow’s picture

StatusFileSize
new11.71 KB
new62.23 KB
  1. +++ b/core/modules/update/src/Psa/PsaFetcher.php
    @@ -0,0 +1,288 @@
    +  protected function matchesInstalledVersion(SecurityAnnouncement $sa) : bool {
    

    Changing this to matchesExistingVersion as the extension might not be installed.

  2. +++ b/core/modules/update/src/Psa/PsaFetcher.php
    @@ -0,0 +1,288 @@
    +    static $extensions = [];
    +    $project_type = $sa->getProjectType();
    +    if (!isset($extensions[$project_type])) {
    +      $extensions[$project_type] = $this->extensionLists[$project_type]->getList();
    +    }
    

    I don't think we need to worry about local caching for $extensions here. \Drupal\Core\Extension\ExtensionList::getList() already takes care of caching for us.

  3. +++ b/core/modules/update/tests/src/Kernel/PsaFetcherTest.php
    @@ -0,0 +1,389 @@
    +      'contrib:not-exact:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['1.0'],
    +        ],
    +        'existing_version' => '8.x-1.0',
    

    Ok I removing the logic from PsaFetcher that parses versions to match these non-exact versions.

    So it will only match if the existing version is in insecure.

    I chatted with @drumm and he said the version in insecure should be complete version strings.

  4. I think the only reason we would want version parsing would be if a site is using a dev version of extension or core. I am not sure what we would want to show in this case. These of course will not have exact version matches in insecure. For items where is_psa == 1 this won't matter because we don't check insecure in that case.

    We could open a follow up to decide what we should do.

  5. If we feel we can't release this feature without the ability to show items where is_psa === 0 and the site is using dev version of core or a contrib extension I think a solution would be to show all items where the major version of the local project(core or an extension) matches the major version of any version in insecure.

    I don't think we should try to match the minor version. Here is example situation why.

    • Say we find a security vulnerability now that affects both the latest release 9.0.7 and dev branch 9.1.x
    • We make SA-2020-123 when the security release 9.0.8 comes out. The problem is not critical so we don't release a PSA before.
    • We fix the problem in 9.1.x but since there haven't be any releases for that branch yet there is no release needed.
    • After 9.0.8 comes we realize the vulnerability could have been exploited in different way that actually is critical.. 9.0.8 is still not vulnerable so don't need another release
    • Because we realize the problem in SA-2020-123 was actually critical we update the text of that SA and add it to the JSON feed this patch fetches.
    • Now if you running 9.1.x-dev you may be vulnerable but no version in insecure would match your minor only your major.

    This example maybe seem far fetched but having any items in this feed seem like it will be very rare and we don't a chance to fix it if we mess up the logic before one gets added. I would rather error on this side of showing more messages to sties running dev version than having them miss critical SAs.

  6. Now that I wrote that scenario out I realize the same situation could happen with the last minor branch of 9.x.x and the new dev branch 10.x.x before there are any release on 10.x.x. So in that case looking for major matches in insecure would not work.

    This could also be true contrib projects

  7. It seems like the whole point of including insecure is that for cases where is_psa === 0 we know the versions that are affected and we only need to show the message to sites that have 1 of those versions. But sites running a dev version we really can't be sure about the relation between the code you are running and security vulnerability that is in one of the version in insecure

    I think for this reason probably the best solution is for projects where the site is running a dev version we show all items in the feed for that project regardless of the values of is_psa or insecure.

    I have not done this yet but will do it next.

I am leaving assigned to myself because I am still working on the patch but don't mean to stop anyone else from chiming in my last comments/patches.

tedbow’s picture

StatusFileSize
new26.35 KB
new63.06 KB
+++ b/core/modules/update/config/schema/update.schema.yml
@@ -40,3 +40,13 @@ update.settings:
+      label: 'PSA settings'

+++ b/core/modules/update/src/Psa/EmailNotify.php
@@ -0,0 +1,165 @@
+  private const LAST_MESSAGES_STATE_KEY = 'update_psa.last_messages_hash';
...
+   * The PSA fetcher service.

+++ b/core/modules/update/src/Psa/PsaFetcher.php
@@ -0,0 +1,225 @@
+ * Defines a service class to get Public Service Announcements.
...
+class PsaFetcher {

+++ b/core/modules/update/update.install
@@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
+    $requirements['update_psa']['title'] = t('Critical security announcements');

There tons of places in this patch that uses psa or "Public Service Announcement" where what is really being dealt with is a "Security Advisory" and some but not all of these "Public Service Announcement".

So removing "PSA" or "Public Service Announcement" except in the explicit places where we dealing with PSAs.

I know this causes a huge interdiff but it just doesn't make sense to a have PsaFetcher that is fetching and processing PSA and other types of Security Advisories.

Talking with @xjm, @mlhess and @drumm I confirmed that PSA are specific kind of Security Advisory. The feed https://updates.drupal.org/psa.json may actually contain both kinds.

I left "PSA feed" in because that is what is documented as.

tedbow’s picture

Assigned: tedbow » Unassigned
StatusFileSize
new20.59 KB
new63.4 KB

Back to @dww review in 71

  1. This is now \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher::getSecurityAdvisoriesMessages with the change in #81 to not use "Public Service Announcements" when their will be other kinds of advisories. I would like to keep "Messages" here because we are not returning all the data about the item just the messages.
  2. This was following the contrib module logic where isValidExtension() would return FALSE if there was not "version" set in the info. If that makes sense we could leave the way it is. But I think for $sa->isPsa() === TRUE we should show the message regardless of whether we have a version or not since we don't check the version anyways. Adding back in getProjectInfo() we can use this to check if the project actually existing locally and skip if not. We can then call it from getProjectExistingVersion() which simplifies that method. Also checking !isset($this->extensionLists[$project_type]) directly here and taking it out later. If the project type is not valid we can't do anything.

    I have also added a new test case to \Drupal\Tests\update\Kernel\SecurityAdvisoriesFetcherTest::providerShowAdvisories() to confirm we show a PSA even if there is no local version set. There are already test cases to show we don't show a non-psa if there is no local version and we don't show either if the project does not exist.

  3. Since my other changes I can't tell which of 2 times this appears in the class this is referring to now.

    If appears once here:

    try {
      $sa = SecurityAdvisory::createFromArray($json);
    }
    catch (\UnexpectedValueException $unexpected_value_exception) {
      throw new \UnexpectedValueException($unexpected_value_exception->getMessage(), static::MALFORMED_JSON_EXCEPTION_CODE);
    }
    

    At this we actually know it is valid JSON because json_decode() didn't return NULL. So the exception would only be thrown here if an individual item in the feed didn't pass the validation in \Drupal\update\SecurityAdvisories\SecurityAdvisory::validateAdvisoryData()

    I think in this case we should actually just skip the item and continue to the next item, if any. It seems very unlikely that the feed from drupal.org would have some valid and some not valid entries but if that were to happen for some reason we should process any items that we can because they may contain a critical advisory that should be displayed on their site. Removing the exception.

    The 2nd time this exception thrown in the class is if json_decode() returns a NULL meaning the json item is malformed. I added the code MALFORMED_JSON_EXCEPTION_CODE to try to distinguish this from a \UnexpectedValueException thrown when calling \Composer\Semver\VersionParser::parseConstraint() but since we no longer calling that because we are not doing version parsing then we don't need this code. Removing the code.

    I am going put off deciding what the admin should do with this error message until I relook at the part of the patch where the exceptions are handled. As far as the service is concerned I think it should throw an exception that has valid information. I think a caller of getSecurityAdvisoriesMessages() should be able to tell the difference between an empty array meaning there were no items that match conditions to show on the current site and the feed not being able to be processed.

  4. fixed
  5. Changed to "Determines if an advisory matches for the existing version of a project."
    Not using "PSA" anymore and using "existing" because the project may not be installed
  6. fixed
  7. fixed
  8. You are correct but also no longer needed because we aren't calling parseConstraints()
  9. No longer needed because we aren't calling parseConstraints()
  10. fixed
  11. fixed
  12. No longer exists
  13. Method has been removed see #80.3
  14. Method has been removed see #80.3
  15. Method has been removed see #80.3
  16. I think this cleared up with new @return comment and the new name is getProjectExistingVersion(). getExistingVersionConstraint() has been removed
  17. Changed to "The project version or an empty string if the project does not exist on the site."
    Trying not to use "installed" because now it just matters if the project exists in the code base not just if it is installed.
  18. Removed because \Drupal\Core\Extension\ExtensionList::getList() handles caching. removed in #80.2
  19. interface no longer exists
  20. Fixed with more specific using your suggestions for the message() method above in 33). I am actually removing message() now because it is only called in 1 place and has no logic.
    I could be convinced no to remove it but it seems more readable.
  21. changing to "security advisories" since it will not just be PSAs, fits in 80 now. Also changing the template name to update-advisory-notify.html.twig. Changed in other templates too
  22. changed to "advisories".
  23. add plural
  24. removed. also removed See the <a href="{{ status_report }}">site status report page</a> for more information.. This was a recommendation from the UX that I forgot to implement. There is not actually more information on the status report page because it just has the same messages with links. Also if you happen to read the email after the items had been removed from the feed from drupal.org then the messages won't appear on the status report page at all which might make you think the problem doesn't exist which is probably not the case.
  25. This test case shows that for is_psa === 1 the version in insecure are actually ignored. Because it a PSA is about future release. Hopefully the test cases in \Drupal\Tests\update\Kernel\SecurityAdvisoriesFetcherTest() make this more clear. We can't put test comment in a json file like we can XML ☹️
  26. This is from the contrib project probably for parsing test. But we aren't parsing anymore, removing
  27. 👍
  28. fixed and added not about [CORE_VERSION]
  29. yes. added a not explaining difference with a 404
  30. Added more explanation
  31. fixed
  32. fixed , this is 'security advisories' now
  33. updated twigs files with new changes.

A few other changes to replace psa with advisory where needed

  1. changed the template names to update-advisory-notify.html.twig
  2. changed the email key to advisory_notify
  3. changed test controller to AdvisoryTestController

Unasigning myself as I am not working on the patch but I will now work on making sure the summary is update to date

tedbow’s picture

Title: Display relevant PSA data for Drupal » Display relevant Security Advisories data for Drupal
Issue summary: View changes
Related issues: +#3176934: Make it possible to determine if an item in the PSA feed has updated "insecure" versions after release.

Updating summary

tedbow’s picture

Issue summary: View changes
  1. cleaning up the summary more
  2. Removing the follow up #3041885: Display relevant Security Advisories data for Drupal. chatted with @drumm and the PSA entries should be removed from the feed shortly after the security release is published. A regular security advisory will likely remain in the feed after the release which explain why the non-release version is critical. This way we don't have to worry about insecure in items with is_psa === TRUE
tedbow’s picture

StatusFileSize
new10.1 KB
new68.65 KB

Found a few small problems

  1. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,165 @@
    +    $params['subject'] = new PluralTranslatableMarkup(
    +      count($messages),
    +      '@count urgent security announcement requires your attention for @site_name',
    +      '@count urgent security announcements require your attention for @site_name',
    +      ['@site_name' => $this->configFactory->get('system.site')->get('name')]
    +    );
    

    I think we should calling \Drupal\Core\StringTranslation\StringTranslationTrait::formatPlural() instead of using this class directly. I was trying to figure out why this class needed StringTranslationTrait. It does right now but I think using the trait is preferred. I see many more times in core.

  2. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,219 @@
    +        catch (\UnexpectedValueException $unexpected_value_exception) {
    +          continue;
    +        }
    

    I added an explanation here as to why just skip items that are in the incorrect format.

    I don't think will ever actually but in the very slim chance it does we should display the items are in the correct format and error out if one is in an invalid format.

  3. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,206 @@
    +        'is_psa' => new NotBlank(),
    

    This actually fails if the json has false. changing to handle values [1, '1', 0, '0', TRUE, FALSE].

    Right now in the test feed https://updates.drupal.org/psa-this-is-only-a-test.json
    it is using "1" but I think we should handle these other values in case for some reason, intentionally or not, the value type should changes.

    With this change I am creating a unit test for this class to fully test it.

  4. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,251 @@
    +  use StringTranslationTrait;
    

    Trait not used.

  5. +++ b/core/modules/update/update.install
    @@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
    +function _update_psa_requirements(array &$requirements): void {
    

    changing function name to _update_advisories_requirements since these will not just be PSAs.

  6. +++ b/core/modules/update/update.module
    @@ -70,6 +71,27 @@ function update_page_top() {
    +      catch (Exception $exception) {
    +        if (!($exception instanceof TransferException || $exception instanceof UnexpectedValueException)) {
    +          // If the exception is unexpected rethrow it.
    +          throw $exception;
    

    In this case the known exceptions that are instances of TransferException(could be subclasses) from \GuzzleHttp\Client::get() or UnexpectedValueException which is thrown if the json feed is malformed. We should match UnexpectedValueException exactly here and not its subclasses.

tedbow’s picture

StatusFileSize
new13.82 KB
new79.94 KB
  1. Addressing the last remaining question in the summary, how to handle a site using a dev release for is_psa === 0 item.

    Here is solution I propose

    If the site is using a dev release:

    If the is_psa === 1 then show the item. this is how the patch works currently

    If the is_psa === 0 then
    If the dev version only specifies a major version like 6.x-dev for branch using semantic versioning or 8.x-6.x for a branch using the legacy versions then
    Show the item if it matches the major version for any of the versions in insecure

    If the dev version specifies a major and minor version like 9.1.x-dev, for contrib or 9.1.0-dev, for core. then
    Show the item if it matches both the major and minor versions for any of the versions in insecure

  2. I added getMinorVersion() to \Drupal\update\ModuleVersion so we don't have string parsing in the new class. getMinorVersion() was actually in \Drupal\update\ModuleVersion in a early version of #3074993: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates where we created the class but we removed it because it wasn't needed and the class is internal so weren't intending to make a class for use outside of core
  3. See #80.4->7
  4. for my previous concerns about dev version. I don't think there is perfect solution to this problem but I think matching as much as we can is good.

tedbow’s picture

Issue summary: View changes

Update summary with dev version logic

dww’s picture

Assigned: Unassigned » tedbow

@tedbow - Thanks for all your fantastic work in here to get this ready, and for so thoroughly and thoughtfully addressing all my review points!

I've carefully reviewed all your comments since #71, but not every single interdiff. The interdiffs I reviewed, I liked what I saw. ;)

I'm +1 to having "Advisories" as the main terminology here, and differentiating PSAs from SAs. Thanks for sorting that out!

+1 to erring on the side of showing more advisories than we might technically need to show. Way better than not showing something we wished we were showing in an edge case. Fail safe. ;)

+1 to ripping out the complications of partial matches. Your reasoning on being able to do exact matches makes perfect sense at #74.3

+1 to punting on perfection for -dev. Update Manager has always had trouble with -dev, and -dev is just problematic in so many ways. ;) We can always try to improve things later. Better to get something out that handles official releases well, than to delay that while trying to solve every possible -dev thing. #86.1 seems very reasonable for an initial release of this feature.

Re: #86.2: Hah! Guess we can un-postpone #3110223: Add ModuleVersion::getMinorVersion() method. ;) Probably just mark it fixed once this lands.

I'm going to do a final review of the full patch next, but this sounds like it's either RTBC or darn close.

Thanks again!
-Derek

dww’s picture

Assigned: tedbow » Unassigned
Status: Needs review » Needs work

Assigned by accident, sorry.

Mostly everything looks great. The test coverage is extremely thorough. Basically everything that needs to be fixed was fixed.

I know we're almost out of time to get this into 9.1.x, but it's going to be painful to change the tempstore keys later, and some of the test coverage isn't what it says it is. That's why I'm setting NW. Everything else is basically trivial docs stuff or code polishing that could be done in a follow-up. But if we're rerolling anyway, maybe worth fixing now.

Thanks again!
-Derek

  1. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    + * Provides an service to send email notifications for security advisories.
    

    s/an a/

  2. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +  protected $saFetcher;
    

    Wonder if securityAdvisoryFetcher would be better for this var.

  3. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +   * @param \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher $psa_fetcher
    ...
    +  public function __construct(MailManagerInterface $mail_manager, SecurityAdvisoriesFetcher $psa_fetcher, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, StateInterface $state, TimeInterface $time, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, LoggerInterface $logger) {
    

    s/psa_fetcher/sa_fetcher/
    (or security_advisories_fetcher if the previous comment happens).

  4. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +      $messages = $this->saFetcher->getSecurityAdvisoriesMessages();
    

    Don't love the double-plural, but probably not worth fixing. ;)

  5. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +        'Unable to send notification email because of error retrieving PSA feed: %error',
    

    Is this the "PSA feed" you decided to leave as-is due to existing documentation or something?

  6. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +   * Constructs a new PsaFetcher object.
    

    s/PsaFetcher/SecurityAdvisoriesFetcher/

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +    $response = $this->tempStore->get('psa_response');
    ...
    +      $psa_endpoint = $this->config->get('psa.endpoint');
    +      $response = (string) $this->httpClient->get($psa_endpoint)->getBody();
    +      $this->tempStore->setWithExpire('psa_response', $response, $this->config->get('psa.check_frequency'));
    

    Do we want to stop keying all these tempstore things with "psa"?

  8. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +      return t(
    

    $this->t()

  9. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +        'Unable to retrieve PSA information from :url.',
    

    s/PSA/security advisory/ ?

  10. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +              // If the dev version doesn't specify a minor version matching on
    

    Wants a comma between "minor version" and "matching on".

  11. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +              // If the dev version specifies a minor version then insecure
    +              // version must match on the minor version.
    

    a) s/then insecure/then the insecure/

    b) comma after 1st 'minor version'

  12. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +        // If existing version is not a dev version then it must match an
    +        // insecure version exactly.
    

    "If the existing version is not a dev version, then it..."

  13. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,207 @@
    +   * The link to the advisory.
    ...
    +  protected $link;
    ...
    +   * @param string $link
    +   *   The link to the advisory.
    

    Should have caught this before, but presumably this is more the URL for the advisory, not a "link" (e.g. not <a href="...">blah</a> Undoubtably that's the key in the feed itself, which is why it's like this. So probably nothing to fix...

  14. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,249 @@
    +    // Alter the 'aaa_update_test' to use the 'aaa_update_project' project name.
    +    // This ensures that for an extension where the 'name' and the 'project'
    +    // properties do not match 'project' is used for matching 'project' in the
    +    // json feed.
    

    A) Thanks, this helps!
    B) "the 'aaa_update_test' module"?
    C) s/json/JSON/

  15. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,249 @@
    +
    
    +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +    static::assertCount(1, $links);
    ...
    +    static::assertCount(0, $links);
    

    Why static:assertCount() here? We don't do that anywhere else in core.

  16. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +      'core:exact:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'core',
    +          'project' => 'drupal',
    +          'insecure' => [\Drupal::VERSION],
    +        ],
    +      ],
    

    Super minor, but it was a bit jarring to see this among the contrib. Seems easier to make sense of this list if we keep all of contrib together, then all of core.

  17. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +      'contrib:not-exact:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['1.0'],
    +        ],
    +        'existing_version' => '8.x-1.0',
    +      ],
    ...
    +      'contrib:not-exact:non-psa-reversed' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['1.0'],
    +        ],
    +        'existing_version' => '8.x-1.0',
    +      ],
    

    The name says "reversed", but these two cases are identical.

  18. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +      'contrib:semver-non-exact:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['1.0'],
    +        ],
    +        'existing_version' => '1.0.0',
    +      ],
    +      'contrib:semver-major-match:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['1.0'],
    +        ],
    +        'existing_version' => '1.0.0',
    +      ],
    

    These 2 also have different names for the same case.

  19. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +      'contrib:semver-different-majors:non-psa' => [
    +        'feed_item' => [
    +          'is_psa' => 0,
    +          'type' => 'module',
    +          'project' => 'the_project',
    +          'insecure' => ['7.x-1.0'],
    +        ],
    +        'existing_version' => '8.x-1.0',
    +      ],
    

    Name says "semver" but these are BespokeVer strings.

  20. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,481 @@
    +      'link' => 'http://thesa.com',
    

    I think to be safe we should use example.com for this.

  21. +++ b/core/modules/update/update.install
    @@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
    +  if (!Drupal::hasService('update.sa_fetcher')) {
    

    Doesn't this want \Drupal:: not just Drupal:: ?

  22. +++ b/core/modules/update/update.install
    @@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
    +  /** @var \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher $psa */
    
    +++ b/core/modules/update/update.module
    @@ -70,6 +71,27 @@ function update_page_top() {
    +      $psa = \Drupal::service('update.sa_fetcher');
    ...
    +        $messages = $psa->getSecurityAdvisoriesMessages();
    

    Again, s/$psa/$fetcher/ (or something).

  23. +++ b/core/modules/update/update.install
    @@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
    +  $psa = \Drupal::service('update.sa_fetcher');
    

    "psa" is now a weird local variable name for this.

  24. +++ b/core/modules/update/update.module
    @@ -192,6 +217,12 @@ function update_cron() {
    +    /** @var \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher $notify */
    

    s/SecurityAdvisoriesFetcher/whatever the right class is/ ;)

dww’s picture

Status: Needs work » Needs review
StatusFileSize
new80.17 KB
new11.09 KB

This fixes nearly all of #89:

  1. Fixed
  2. Went with $securityAdvisoriesFetcher
  3. Fixed
  4. TBD
  5. TBD
  6. Fixed
  7. TODO - This is the main thing I'd like @tedbow to answer before RTBC.
  8. Oh right, it's a static method. Ignore.
  9. Fixed (hope that's cool)
  10. Fixed
  11. Fixed
  12. Fixed
  13. TBD
  14. Fixed
  15. Fixed
  16. Fixed
  17. Fixed
  18. Fixed: Added a new case for what that name said, and changed the name for the existing case to not say 'semver'.
  19. TBD - not sure how to resolve this. ;) Also not a blocker for commit.
  20. Fixed
  21. Fixed
  22. Fixed
  23. Fixed
  24. Fixed

Thanks again!
-Derek

tedbow’s picture

StatusFileSize
new15.98 KB
new80.76 KB

@dww thanks for the review and patch!

re #89

  1. Changed to getSecurityAdvisoryMessages()
  2. Yes this is the documentation about the feed https://www.drupal.org/docs/8/update/automatic-updates#s-public-service-... and also it is actually "psa.json".
    Hopefully we can update that documentation later.
  3. Fixed this. I changed the settings group in update.schema.yml to be advisories. and the tempstore key to be advisories_response
  4. The fix is good 👍
  5. Yes this was like this because the feed uses link but I changed the class to used URL and changed the method to getUrl() because the 1 place we use it in non-test code is
    ':url' => $sa->getUrl(),
  6. You said "TBD" for 19) but I think you fixed it. Correct?

    It looks to be the one you didn't fix is 18.

    I changed the 2nd test case there and added a new 1 after that

    'contrib:semver-major-match-not-minor:non-psa' => [
            'feed_item' => [
              'is_psa' => 0,
              'type' => 'module',
              'project' => 'the_project',
              'insecure' => ['1.1.0'],
            ],
            'existing_version' => '1.0.0',
          ],
          'contrib:semver-major-minor-match-not-patch:non-psa' => [
            'feed_item' => [
              'is_psa' => 0,
              'type' => 'module',
              'project' => 'the_project',
              'insecure' => ['1.1.1'],
            ],
            'existing_version' => '1.1.0',
          ],
    

    So making sure we don't do a partial match on semver if the site is NOT using a dev version.


1 Additional change and 1 suggestion(not implemented)
  1. Following the example of \Drupal\update\ModuleVersion I am marking the class as @internal and making it final. I am also changing the constructor to private since we have a public factory method createFromArray().

    I think the same argument I used for this pattern in ModuleVersion applies to this class. You can see my argument here #3074993-90: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates and in the next comment @xjm agree and giving more reasons.

    Here is @xjm's reasoning which I think applies directly in this case also

    Normally I'd worry about still not having a policy for how we use private (technically the docs still say we don't use it and the issue's still open) but given that this is a totally new, totally internal, immutable value-object-thing with factory methods (and not injecting services nor of any conceivable use to contrib with the constructor), I think it's actually totally justifiable in this case under a current read of core policy.

  2. Personally I think with functionality like this we should do whatever we can to not make this an API. I think the json feed itself should be considered what custom code should interact with if they don't like core's functionality. We already check for the existence of the 2 new services in the container before we use them. Therefore custom code can remove these services and add their own functionality without any errors.

    For this reason I would be in favor of making the other 2 classes in the Drupal\update\SecurityAdvisories namespace also final and @internal. We could always remove final later if a good case was made to do so. We can never make them final though if we don't release them that way.

    I am not willing to hold this issue up for this. I imagine it is not a popular opinion but just wanted to put it out there in case it makes sense to others. I guess technically we could do it in follow-up if it made it in before the code was in a release.

dww’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Related issues: +#3110223: Add ModuleVersion::getMinorVersion() method
  1. 👍
  2. 👍
  3. 👍🙏
  4. 👍
  5. 👍
  6. "It looks to be the one you didn't fix is 18." -- Indeed, sorry for the mix-up.
  7. 🙏 - your fix looks good and makes sense.

  1. 👍
  2. Hrrrrmm. 😉 I see what you mean. OTOH, final is so final. ;) What's the point of making this a service if folks can't decorate it if they need to?

    I think the json feed itself should be considered what custom code should interact with if they don't like core's functionality.

    That's pretty harsh. 😉 This is a lot of code, much of it would be relevant to someone else trying to use this functionality but that needed some additional changes / tweaks. Telling them: "sorry, start from scratch if you don't like what we're doing" isn't very Drupal-esque.

    How about we mark them @internal as a hint, but not actually final? Is that weird?

I also think we could change our minds on this during alpha (if not beta), so I'd rather not hold this up for commit. If you want, we can open a follow-up to continue this discussion.

I've carefully reviewed the latest interdiff, and very closely reviewed the full patch twice. Try as I might, I'm out of things to complain about. 😉 Bot's happy. Lots of great new test coverage. No CS violations. Summary is accurate with screenshots, API changes is correct, etc. It was missing a release note, but I added one.

This is important functionality to help improve site security. Let's get this in. RTBC 🎉

Thanks again, @tedbow, @beautifulmind, @heddn, @drumm, @xjm and everyone else who worked on getting this to this point!

Cheers,
-Derek

p.s. Adding #3110223: Add ModuleVersion::getMinorVersion() method as related. It's possible there's some useful test coverage in that patch we can still harvest even after this lands. Of course, not a blocker or urgent. ;)

tedbow’s picture

@dww thanks for the review again!

@xjm asked me to put the "Needs release manager review"

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

pasqualle’s picture

Version: 9.2.x-dev » 9.1.x-dev
Issue tags: +Security improvements
xjm’s picture

@Pasqualle, only committers should move minor-only issues back to 9.1.x after the release of alpha1 (and generally only after commit). Thanks!

Reference: https://www.drupal.org/about/core/policies/core-change-policies/allowed-...

xjm’s picture

Version: 9.1.x-dev » 9.2.x-dev

For what it's worth, we probably will backport this issue, but following the issue deadline this decision is made only once the patch has successfully been committed to 9.2.x. So setting the branch accordingly.

Please do not change the branch elsewhere for minor-only issues; if they don't get backported during the alpha at committer discretion, what you're doing is mixing issues that were specifically targeted against a minor branch in with the patch queue that 9.1.x will shortly become.

xjm’s picture

One thing that occurred to me since we trimmed down the email message to this for readability:

SUBJECT: An urgent security announcement requires your attention for Drupal

An important security announcement is available for your Drupal site. You should 
read the announcement immediately and follow its instructions.

Public service announcements:

  * Critical Release - SA-2019-02-19 [1]

To see all public service announcements, visit
https://www.drupal.org/security/psa [2].


[1] https://www.drupal.org/sa-2019-02-19
[2] https://www.drupal.org/security/psa

Among other things, we deliberately took out this bit:


Your site is currently configured to send these emails when a security update will be made available soon. To change how you are notified, you may <a href="{{ settings_link }}">configure email notifications</a>.

Since then, I've been wondering if this violates some user rights best practice of explaining to someone how they can unsubscribe for an email notification. Going to ping the sec team to see if they have thoughts on whether we need to include such a sentence here. (If we do have to add it back, we could just add back the last sentence rather than both of them.)

xjm credited webchick.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Per @webchick, we do indeed need a link telling the email recipient how to change the notifications. @webchick referenced the CAN-SPAM, act, and I believe there are other legal precedents as well.

I think just the second sentence will do:


 To change how you are notified, you may <a href="{{ settings_link }}">configure email notifications</a>.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new3.1 KB
new81.6 KB

Updated email

dww’s picture

Status: Needs review » Reviewed & tested by the community

Interdiff looks great.
All 3 templates updated, and test coverage expanded to match. Tests are all passing.
No CS violations.
Back to RTBC.

Thanks!
-Derek

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Reviewed down to the start of the tests. Mostly nitpicks but one critical question WRT dev versions in point 12.

  1. +++ b/core/modules/update/config/schema/update.schema.yml
    @@ -40,3 +40,13 @@ update.settings:
    +    advisories:
    +      type: mapping
    +      label: 'Security advisory settings'
    +      mapping:
    +        endpoint:
    +          type: string
    +          label: 'Endpoint URI for security advisories'
    +        check_frequency:
    +          type: integer
    +          label: 'Frequency to check for security advisories, defaults to 12 hours'
    

    Didn't we discuss using an existing configuration setting for this check frequency?

  2. +++ b/core/modules/update/src/ModuleVersion.php
    @@ -73,7 +86,7 @@ public static function createFromVersionString($version_string) {
    -    return new static($major_version, $version_extra);
    +    return new static($major_version, $minor_version, $version_extra);
       }
     
       /**
    @@ -81,11 +94,14 @@ public static function createFromVersionString($version_string) {
    
    @@ -81,11 +94,14 @@ public static function createFromVersionString($version_string) {
        *
        * @param string $major_version
        *   The major version.
    +   * @param string|null $minor_version
    +   *   The minor version.
        * @param string|null $version_extra
        *   The extra version string.
        */
    -  private function __construct($major_version, $version_extra) {
    +  private function __construct(string $major_version, $minor_version, $version_extra) {
         $this->majorVersion = $major_version;
    +    $this->minorVersion = $minor_version;
         $this->versionExtra = $version_extra;
       }
    

    I was going to say that this is a BC break, but we did make the constructor private.

  3. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +   * EmailNotify constructor.
    

    "Constructs a"

  4. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +   * Send notification when security advisories are available.
    

    "Sends notifications"

  5. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +  public function send(): void {
    

    Missing space before the colon.

  6. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +        'Unable to send notification email because of error retrieving PSA feed: %error',
    

    Since this can end up as a user-facing string, let's use a complete sentence with articles:

    Unable to send a PSA notification email because of an error retrieving the PSA feed.

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +      $this->tempStore->setWithExpire('advisories_response', $response, $this->config->get('advisories.check_frequency'));
    

    Why a tempstore?

  8. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +        if ($project_type !== 'core' && (!isset($this->extensionLists[$project_type]) || !$this->getProjectInfo($sa))) {
    +          continue;
    +        }
    

    Hm, code comment here would be good as it's not immediately obvious why we skip it under these conditions.

  9. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +  public static function getErrorMessageFromException(\Exception $exception, bool $throw_unexpected_exceptions = TRUE) {
    

    Noe to self: check for where this bool flag is set.

  10. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +   *   TRUE if the security advisory matches the existing version of the
    +   *   project, otherwise FALSE.
    

    "or FALSE otherwise."

  11. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +          catch (\UnexpectedValueException $exception) {
    +            // Version numbers that start with core prefix besides '8.x-' are
    +            // expected in $insecure_versions but will never match and will
    +            // thrown an exception.
    

    Note to self: Check for test coverage of all three cases here.

  12. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +          if ($existing_project_version->getMajorVersion() === $insecure_project_version->getMajorVersion()) {
    +            if ($existing_project_version->getMinorVersion() === NULL) {
    +              // If the dev version doesn't specify a minor version, matching on
    +              // the major version alone is considered a match.
    +              return TRUE;
    +            }
    +            if ($existing_project_version->getMinorVersion() === $insecure_project_version->getMinorVersion()) {
    +              // If the dev version specifies a minor version, then the insecure
    +              // version must match on the minor version.
    +              return TRUE;
    

    Wait. With a dev version, we have no indication of how recent it is. It could be from yesterday or five years ago.

  13. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +   * @return array
    +   *   The project information if the project exists, otherwise an empty array.
    

    Can we be more specific than array? And where is the array structure canonically defined?

    Also: "or otherwise".

  14. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,254 @@
    +  protected function getProjectInfo(SecurityAdvisory $sa): array {
    

    Missing space before the colon again. We should check for or file a coder rule followup for this.

  15. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    + * These come from the PSA feed on drupal.org.
    

    We have a content standard that "Drupal.org" should be capitalized.

  16. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    +   *   The versions of the project that are currently insecure. For PSAs this
    +   *   list does include versions that will be marked as insecure when the new
    +   *   security release is published.
    

    Why "does"?

  17. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    +  public function getTitle(): string {
    ...
    +  public function getProject(): string {
    ...
    +  public function getProjectType(): string {
    ...
    +   */
    ...
    +  public function getInsecureVersions(): array {
    ...
    +  public function getUrl(): string {
    

    Spaces before the colons.

  18. +++ b/core/modules/update/templates/update-advisory-notify.html.twig
    @@ -0,0 +1,34 @@
    +    An important security announcement is available for your Drupal site. You should read the announcement immediately and follow its instructions.
    +  {%- plural advisories -%}
    +    Important security announcements are available for your Drupal site. You should read the announcements immediately and follow their instructions.
    

    Should we omit the word "Drupal" here? Going back to the content standard of avoiding using the word "Drupal" in user-facing content since it can be branded as something else. Edit: Just checked and the existing email notifications about insecure sites do say "Drupal" already. Dunno if we should file a followup about that or let it be. Maybe just let it be since it's been this way forever.

xjm’s picture

+++ b/core/modules/update/config/schema/update.schema.yml
@@ -40,3 +40,13 @@ update.settings:
+          label: 'Frequency to check for security advisories, defaults to 12 hours'

Also: comma splice. Should be two sentences.

effulgentsia’s picture

With a dev version, we have no indication of how recent it is. It could be from yesterday or five years ago.

Right. So the patch errs on the side of displaying the SA. There's a chance you have a dev snapshot that already includes the fix, but we can't determine that. However, it shouldn't be all that likely that you have a dev snapshot that includes the fix. The most common reason to be on a dev snapshot is that you need to be ahead of the last tag that was made on that branch. But it that was your situation prior to the SA, then once there's an SA, there's either a new tag on that branch, or that branch is no longer supported and you need to update to the branch that has the security fix and there's now a newly released tag on that one. Ok, but what if a day or a week after that SA, a non-security bug fix gets committed on that branch, and your site needs it? This is the situation in which you'd now be on a dev snapshot and seeing the old SA again, even though you already have the fix. While not ideal, is it acceptable, considering that if we don't show the SA for dev snapshots, then the more common case of someone being on an old dev snapshot would not see the SA?

ayushmishra206’s picture

StatusFileSize
new5.65 KB
new81.64 KB

I have made the following changes in this patch:
103.3
103.4
103.5
103.6
103.10
103.13.2
103.14
103.15
103.16
103.17
and #104

Remaining changes are:
103.1
103.2
103.7
103.8
103.9
103.11
103.12
103.13.1

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.72 KB
new81.86 KB

@xjm thanks for the review and @ayushmishra206 thanks for addressing many of the items!

re #103

  1. I remember discussing that and the use of the existing email setting but I thin the conclusion was that the existing check frequency can not be used because you can set it to "weekly" with many of these items being in the feed for a week or less then a check once a week for a PSA for an upcoming release within a week would be pretty much meaning less for many sites.
  2. Yeah this was one main reasons to make the constructor private and the reason to make the new \Drupal\update\SecurityAdvisories\SecurityAdvisory::__construct() also private
  3. #106 addressed this but needs to change from "Constructs a EmailNotify constructor" to "Constructs a EmailNotify object"
  4. 👍
  5. 👍
  6. 👍
  7. To make this the same as UpdateProcessor and UpdateManager in the Update module. @xjm see your comment in #57.13

    Interesting. This seems like it should be stored in state rather than cache? Otherwise a cache clear blows it away and resets it.

    However, we should primarily mimic whichever pattern the update module already does when checking for updates.

  8. Added a comment on why we skip here.
  9. Flag is set in the catch in \Drupal\update\SecurityAdvisories\EmailNotify::send(). I think my intention was to not to have an exception that we were not expecting stop cron. Now looking at \Drupal\Core\Cron::invokeCronHandlers() exceptions are already caught and handled. So removing this $throw_unexpected_exceptions
  10. 👍
  11. See test cases in \Drupal\Tests\update\Kernel\SecurityAdvisoriesFetcherTest()
  12. See @effulgentsia comment in #105
  13. Changed to mixed[]. This is from the info.yml files but augmented in \Drupal\Core\Extension\ExtensionList::getList() or where this function overridden and also passed through hook_info_alter()
    I don't believe we have a canonical place where this structure is documented. Update the @return comment
  14. 👍
  15. 👍
  16. This should have been "does not". fixed
  17. There were some more of these but now I am unsure if having the space before the colon is actually correct.
    The example in the proposed change in #2928856: Adopt the PSR-12 standard for PHP return types suggest not to have a space before colon. grepping in /core for "function.*\):" with no space finds over 100 results and "function .*\) :" with the space only finds 27.

    Not changing any more for now

  18. I would say to let it be because I think it is highly likely that people may have set up email filters for this exact text in the existing emails to flag these emails. Also I would say if we don't already have an issue where people have asked to change this because it is causing confusion when a site is branded as something else then changing it will probably cause more problems than it is worth. I didn't check how to override the existing emails but since this is in a twig template it is easily changeable for any sites that want to.
xjm’s picture

I remember discussing that and the use of the existing email setting but I thin the conclusion was that the existing check frequency can not be used because you can set it to "weekly" with many of these items being in the feed for a week or less then a check once a week for a PSA for an upcoming release within a week would be pretty much meaning less for many sites.

Makes sense.

There were some more of these but now I am unsure if having the space before the colon is actually correct.
The example in the proposed change in #2928856: Adopt the PSR-12 standard for PHP7 return types suggest not to have a space before colon. grepping in /core for "function.*\):" with no space finds over 100 results and "function .*\) :" with the space only finds 27.

In that case, can we be consistent the other way, and not have the space before the colon anywhere? This sort of violates core's standard of having a space on either side of an operator, but we should at least be consistent within the patch.

#2928856: Adopt the PSR-12 standard for PHP return types doesn't actually specify whether there should be a space before the colon or not. It only says there does need to be a space after it.

xjm’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/update/config/schema/update.schema.yml
    @@ -40,3 +40,13 @@ update.settings:
    +          label: 'Frequency to check for security advisories. It defaults to 12 hours'
    

    The second sentence should be:

    Defaults to 12 hours.

    It should have a period at the end.

  2. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,257 @@
    +      if ($existing_project_version->getVersionExtra() === 'dev') {
    +        foreach ($insecure_versions as $insecure_version) {
    +          try {
    +            $insecure_project_version = ModuleVersion::createFromVersionString($insecure_version);
    +          }
    +          catch (\UnexpectedValueException $exception) {
    +            // Version numbers that start with core prefix besides '8.x-' are
    +            // expected in $insecure_versions but will never match and will
    +            // thrown an exception.
    +            continue;
    +          }
    +          if ($existing_project_version->getMajorVersion() === $insecure_project_version->getMajorVersion()) {
    +            if ($existing_project_version->getMinorVersion() === NULL) {
    +              // If the dev version doesn't specify a minor version, matching on
    +              // the major version alone is considered a match.
    +              return TRUE;
    +            }
    +            if ($existing_project_version->getMinorVersion() === $insecure_project_version->getMinorVersion()) {
    +              // If the dev version specifies a minor version, then the insecure
    +              // version must match on the minor version.
    +              return TRUE;
    +            }
    

    Can we add some inline comments above this section explaining what @effulgentsia explained?

  3. +++ b/core/modules/update/update.module
    @@ -401,23 +433,32 @@ function update_fetch_data_finished($success, $results) {
    +      $message['body'][] = t('Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, @url.', ['@url' => $settings_url]);
    ...
    +      $message['body'][] = t('Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, @url.', ['@url' => $settings_url]);
    
    +++ b/core/themes/stable/templates/admin/update-advisory-notify.html.twig
    @@ -0,0 +1,34 @@
    +  {{ 'To see all public service announcements, visit <a href="@uri">@uri</a>.'|t({'@uri': 'https://www.drupal.org/security/psa'}) }}
    ...
    +++ b/core/themes/stable9/templates/admin/update-advisory-notify.html.twig
    @@ -0,0 +1,34 @@
    +  {{ 'To see all public service announcements, visit <a href="@uri">@uri</a>.'|t({'@uri': 'https://www.drupal.org/security/psa'}) }}
    

    URLs should use :placeholders, not @placeholders, so that they receive the proper sanitization.

  1. +++ b/core/modules/update/src/ModuleVersion.php
    @@ -118,6 +134,16 @@ public function getMajorVersion() {
    +  public function getMinorVersion() {
    +    return $this->minorVersion;
    +  }
    

    Nullable typehint?

  2. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,257 @@
    +  public static function getErrorMessageFromException(\Exception $exception) {
    

    Return typehint?

xjm’s picture

Skimmed the test coverage, which is beautiful, and reviewed down to the top of the update.module changes. A few nitpicks again.

  1. +++ b/core/modules/update/tests/modules/update_test/src/Controller/AdvisoryTestController.php
    @@ -0,0 +1,39 @@
    +  public function getPsaJson(string $json_name) {
    

    Return typehint?

  2. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,498 @@
    +   * Dataprovider for testShowAdvisories().
    

    "Data provider" should be two words.

  3. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,498 @@
    +  public function providerShowAdvisories() {
    

    Return typehint?

  4. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,498 @@
    +   * Tests Advisories that should be ignored.
    

    Nit: Advisories does not need to be capitalized here.

  5. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,498 @@
    +   * Dataprovider for testIgnoreAdvisories().
    

    "Data provider" should be two words.

  6. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -0,0 +1,498 @@
    +  public function providerIgnoreAdvisories() {
    

    Return typehint?

  7. +++ b/core/modules/update/tests/src/Unit/ModuleVersionTest.php
    @@ -27,6 +27,21 @@ public function testGetMajorVersion($version, array $expected_version_info) {
    +  /**
    +   * @covers ::getMinorVersion
    +   *
    +   * @dataProvider providerVersionInfos
    +   *
    +   * @param string $version
    +   *   The version string to test.
    +   * @param array $expected_version_info
    +   *   The expected version information.
    +   */
    +  public function testGetMinorVersion($version, array $expected_version_info) {
    +    $version = ModuleVersion::createFromVersionString($version);
    +    $this->assertSame($expected_version_info['minor'], $version->getMinorVersion());
    +  }
    

    Beautiful. I love it when our past test coverage lets us add new test coverage so cleanly.

  8. +++ b/core/modules/update/tests/src/Unit/SecurityAdvisoryTest.php
    @@ -0,0 +1,158 @@
    +  public function testCreateFromArrayIsPsa($value, bool $expected) {
    ...
    +  public function testCreateFromArrayMissingField(string $missing_field) {
    ...
    +  public function testCreateFromArrayInvalidField(string $invalid_field, string $expected_type_message) {
    

    Void return typehint?

  9. +++ b/core/modules/update/tests/src/Unit/SecurityAdvisoryTest.php
    @@ -0,0 +1,158 @@
    +  public function providerCreateFromArrayIsPsa() {
    ...
    +  public function providerCreateFromArrayMissingField() {
    ...
    +  public function providerCreateFromArrayInvalidField() {
    

    Return typehint?

xjm’s picture

+++ b/core/modules/update/update.install
@@ -177,3 +180,36 @@ function _update_requirement_check($project, $type) {
+  catch (Exception $exception) {
+    $requirements['update_advisories']['title'] = t('Critical security announcements');
+    $requirements['update_advisories']['severity'] = REQUIREMENT_ERROR;
+    $requirements['update_advisories']['value'] = SecurityAdvisoriesFetcher::getErrorMessageFromException($exception);
+    return;

Wait. We're raising requirements errors from exceptions in the fetcher on the status report? Can the site owner do anything about it if there's exceptions when we try to get advisories?

xjm’s picture

+++ b/core/modules/update/tests/src/Unit/SecurityAdvisoryTest.php
@@ -0,0 +1,158 @@
+      'link' => 'https://www.drupal.org\/SA-CONTRIB-2019-02-02',

What's with the backslash in the URL?

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new12.2 KB
new82.49 KB

re #108

  1. In that case, can we be consistent the other way, and not have the space before the colon anywhere?

    Yep here is a fix for that

  2. doesn't actually specify whether there should be a space before the colon or not. It only says there does need to be a space after it.

    I think #2928856: Adopt the PSR-12 standard for PHP return types does

    The colon and declaration MUST be on the same line as the argument list closing parentheses with no spaces between the two characters.

    (emphasis added)

    I double checked with psr-12 directly and it still has that wording https://www.php-fig.org/psr/psr-12/
    the examples on that page also use it consistently
    public function sampleFunction(int $a, int $b = null): array

#109

  1. ☑️
  2. I wrote a comment that explains that we can't know if there is direct match so we should error on the side showing the advisories.
  3. The examples of @url in the update.module file are actually existing code. They are only in the patch because update_mail() now has a if/else and the lines are now indented. Maybe we should create a follow-up to address along with others in the update module or in all of core? I found some others

    Fixed the instances in twig files.

second ordered list in #109

  1. Ah i didn't know about this. Fixed
  2. I don't think you can use a type hint here because the method may return 2 different types in php 7, \Drupal\Core\StringTranslation\TranslatableMarkup|string

re #109

Skimmed the test coverage, which is beautiful,

☺️

  1. fixed
  2. fixed
  3. fixed
  4. fixed
  5. fixed
  6. fixed
  7. 🎉
  8. fixed

re #111
see #58.13

the site can't do anything if the problem is on the drupal.org side but they could do something if the problem with the request is something on their network side. It's been so long since I have dealt with network issues that I can't imagine what that might be.

But if there is some problem on their side and we never show them a message then when a PSA comes along they won't see it and we won't have raised any error they could see anywhere. To much of edge case?
Leaving for now.

re #112
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
Congratulations!!!

This backslash was left in the patch as part of the weekly Most Thorough Reviewer Award™️
You have won!!! 🍾
Expect your award check in 6 weeks to 8 decades!
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

😉 Also fixed!

xjm’s picture

Status: Needs review » Needs work

Interdiff looks good. Two small fixes:

  1. +++ b/core/modules/update/config/schema/update.schema.yml
    @@ -49,4 +49,4 @@ update.settings:
    -          label: 'Frequency to check for security advisories. It defaults to 12 hours'
    +          label: 'Frequency to check for security advisories. Defaults to 12 hours'
    

    The period is still missing at the end of the sentence.

  2. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -177,10 +177,18 @@ public static function getErrorMessageFromException(\Exception $exception) {
    +      // we should error on the side of assuming the site's code does have the
    

    I think this should be "err on the side of". "Error on the side of" is a common misuse of the idiom.

@tedbow and I also discussed that for #111 we can log the error instead of showing it on the status report. That way the site owner can still find it, but it's not a super-serious, potentially inactionable error on the status report.

xjm’s picture

+++ b/core/themes/stable/templates/admin/update-advisory-notify.html.twig
@@ -0,0 +1,34 @@
+ * - advisories: The messages array

+++ b/core/themes/stable9/templates/admin/update-advisory-notify.html.twig
@@ -0,0 +1,34 @@
+ * - advisories: The messages array

Oh, missed this before since I don't review templates all that often, but there should be a period at the end of the sentence here too.

xjm’s picture

Issue summary: View changes

Updating the release note snippet with examples of the kinds of PSAs that will end up in the feed.

xjm’s picture

Issue summary: View changes
tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.58 KB
new82.35 KB

re #114

  1. fixed
  2. fixed
  3. re #111 removed malformed message from status page

#115
Fixed but changed to "An array of message strings." across all 3 twig files since this had been updated because of prior comment by @xjm but I missed it in the other 2 twig files.

xjm’s picture

+++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
@@ -0,0 +1,209 @@
+    $violations = Validation::createValidator()->validate($data, $collection_constraint);
+    if ($violations->count()) {
+      foreach ($violations as $violation) {
+        $violation_messages[] = (string) $violation;
+      }
+      throw new \UnexpectedValueException(implode(",  \n", $violation_messages));

So, this is one of the exceptions that could end up logged in the logger. I wonder if we should add more context to it? Otherwise I think the constraints' errors will be quite generic. It could be something like:

'Malformed PSA data: ' . implode(...);
tedbow’s picture

StatusFileSize
new2.08 KB
new82.42 KB

fixing #119

dww’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new82.29 KB
new486 bytes

Verified that #120 addresses all the feedback since #102. Here's a fix for one remaining extremely trivial minor grammar bug. 😉 Self-RTBC'ing, since this is so tiny.

Thanks!
-Derek

tedbow’s picture

StatusFileSize
new82.39 KB

Just a reroll

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 122: 3041885-122.patch, failed testing. View results

xjm’s picture

Status: Needs work » Reviewed & tested by the community

Known random fail. Rerunning.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Partial review:

  1. We need to either add "PSAs" to the cspell dictionary, or (preferred) write it out as "public service announcements":

    CSPELL: checking all files
    /Users/xjm/git/maintainer/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php:78:69 - Unknown word (PSAs)
    
  2. +++ b/core/modules/update/config/install/update.settings.yml
    @@ -8,3 +8,6 @@ fetch:
    +  check_frequency: 43200
    

    I checked and update module itself checks daily by default, and is only configurable to the day, not the second:

    check:
      disabled_extensions: false
      interval_days: 1

    The update settings form allows configuring this to be a day or a week, and someone could presumably set it to another value with contrib or custom code.

    On the one hand, 12 h is reasonable as a default -- twice as frequent as the default for updates. I could honestly see checking for security updates at least twice a day but checking for other updates only once a week.

    And in some ways, having fewer configuration options in the UI is nice.

    However, OTOH, I wonder if this might confuse people because they can control the update check frequency through the UI, but not the security announcement check frequency.

    Our three options are:

    • Leave it as-is
    • Expose it through the UI (more form elements is worse UX, but will people be confused if their site keeps hitting d.o extra frequently?)
    • Use the same config for both. (Ruled out as per #107.)

    So I guess the first option might still be best, but I do get stuck on it every time I open this patch.

  3. +++ b/core/modules/update/src/ModuleVersion.php
    @@ -62,6 +69,12 @@ public static function createFromVersionString($version_string) {
    +    elseif ($version_parts_count === 3) {
    +      $minor_version = $version_parts[1];
    +    }
    

    What if $version_parts_count is 4 or more due to a change in version formatting?

    Answer: An exception is already thrown in HEAD in that case, so such a version can't work anyway.

  4. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +  private const LAST_MESSAGES_STATE_KEY = 'update_sa.last_messages_hash';
    

    Missing a docblock.

  5. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +      count($messages),
    +      '@count urgent security announcement requires your attention for @site_name',
    +      '@count urgent security announcements require your attention for @site_name',
    +      ['@site_name' => $this->configFactory->get('system.site')->get('name')]
    

    Let's get rid of this string entirely. These messages are so rare, like once every two years. There are definitely zombie sites for which there would be two messages, but all of them have also probably been pwned already by the time there's a second message for the list. And even then we don't need to tell them how many bullets are in the list, because they can count them. The combination of the left column and the PSA link title(s) are sufficient to explain this.

  6. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +   *   An array of translatable strings which are links to the upstream advisory
    +   *   using the message as the text.
    

    This is a bit of a run-on. How about:

    An array of translatable strings. Each string is a link to the upstream advisory, using the advisory title as the text.

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +          $messages[] = new FormattableMarkup('<a href=":url">:message</a>', [
    +            ':message' => $sa->getTitle(),
    

    Also, let's call them "items" or something. They're not really "messages".

  8. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +   *   The exception throw by ::getSecurityAdvisoryMessages().
    

    Typo: thrown.

  9. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +    else {
    +      throw new \UnexpectedValueException('Drupal PSA JSON is malformed.');
    +    }
    

    So, I'm still fussing a bit about this exception re-throwing, and about unexpected exceptions from connectivity or timeout issues with requests causing sites to whitescreen / cron to fail / etc.

    +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,164 @@
    +    try {
    +      $messages = $this->securityAdvisoriesFetcher->getSecurityAdvisoryMessages();
    +    }
    +    catch (\Exception $exception) {
    +      $this->logger->error(
    +        'Unable to send a PSA notification email because of an error retrieving the PSA feed. %error',
    +        ['%error' => SecurityAdvisoriesFetcher::getErrorMessageFromException($exception)]
    +      );
    +      return;
    +    }
    
    +++ b/core/modules/update/update.install
    @@ -177,3 +180,34 @@ function _update_requirement_check($project, $type) {
    +  try {
    +    $messages = $fetcher->getSecurityAdvisoryMessages();
    +  }
    +  catch (Exception $exception) {
    +    \Drupal::logger('update')->error(SecurityAdvisoriesFetcher::getErrorMessageFromException($exception));
    +    return;
    +  }
    
    +++ b/core/modules/update/update.module
    @@ -70,6 +71,27 @@ function update_page_top() {
    +      try {
    +        $messages = $fetcher->getSecurityAdvisoryMessages();
    +        if ($messages) {
    +          \Drupal::messenger()->addError(t('Critical security announcements:'));
    +          foreach ($messages as $message) {
    +            \Drupal::messenger()->addError($message);
    +          }
    +        }
    +      }
    +      catch (Exception $exception) {
    +        if (!($exception instanceof TransferException || get_class($exception) === UnexpectedValueException::class)) {
    +          // If the exception is unexpected rethrow it.
    +          throw $exception;
    +        }
    

    I checked for all the places the method is called to make sure we're catching and logging exceptions. It looks like the third case, in update.module, is re-throwing exceptions that aren't one of two types, while the other two places are simply catching and logging all exceptions. Why does this last one have different behavior?

    +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +        try {
    +          $sa = SecurityAdvisory::createFromArray($json);
    +        }
    +        catch (\UnexpectedValueException $unexpected_value_exception) {
    +          // Ignore items in the feed that are in an invalid format. Although
    +          // this is highly unlikely we should still display the items that are
    +          // in the correct format.
    +          continue;
    +        }
    

    This is only ever UnexpectedValueException, because that's what we've defined in the new method and its validation.

    +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +    $json_payload = json_decode($response, TRUE);
    

    This returns NULL if the feed can't be decoded, so no exception there either.

    +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +      $response = (string) $this->httpClient->get($advisories_endpoint)->getBody();
    

    However, this might throw other errors, and that's what I'm worried about in terms of breaking site operation.

    +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,265 @@
    +      $response = (string) $this->httpClient->get($advisories_endpoint)->getBody();
    

    I cannot, for the life of me, find where this get() method is defined. There is only one reference in a docblock on GuzzleHttp\Client to:

    guzzle/src/Client.php: * @method ResponseInterface get(string|UriInterface $uri, array $options = [])
    

    ...but that doesn't actually seem to exist on ResponseInterface and I can't find where it's implemented.

    Then in the Guzzle docs there's this line:

    https://docs.guzzlephp.org/en/6.5/quickstart.html#making-a-request

    ...but it doesn't say where said magic methods are defined or how they work.

    How does it work?

    All that said, looking at the List of Guzzle exceptions, it seems theoretically there are a lot of exceptions that could come out of network issues, timeout issues, etc. that wouldn't be caught here and would break the mail service or cron or etc. Maybe we should catch and log all exceptions in all three cases where the method is called? Or is there a reason that the update.module case is different?

tedbow’s picture

Status: Needs work » Needs review

@xjm thanks for the review!

  1. fixed
  2. Talked with @xjm about this. Changed to `interval_hours` which is more easily understandable and more similar to the existing interval_days for updates.
  3. Yep HEAD has throw new \UnexpectedValueException("Unexpected version number in: $original_version");
    So we have to update the code if we want to support different formats
  4. fixed
  5. This string is for the email subject so we need something anyways.

    It sounds like from your comment you were describing this as the status report text. I noticed the text in the status report is very similar so removing it from there

  6. fixed
  7. Changing $messages to $items

    I left
    ':message' => $sa->getTitle(),
    as 'message' I think changing to 'title' would be confusing because title is an attribute for <a> elements.

  8. I am going to address everything except exception throwing in this patch so it can be in it's own patch. So that it for this patch.
tedbow’s picture

StatusFileSize
new6.81 KB
new82.18 KB

whoops here is the patch. can't wait till all of the issues I am involved are merge requests

tedbow’s picture

StatusFileSize
new14.38 KB
new82.47 KB
  1. re #125.9

    I cannot, for the life of me, find where this get() method is defined. There is only one reference in a docblock on GuzzleHttp\Client to:

    Yes this weird what actually gets executed here is \GuzzleHttp\Client::__call() which passes $method on to requestAsync() or request() of the same class.

  2. Relating to exceptions on the last Drupal UX meeting this issue came up and we discussed how we should report issues to the user.

    I had said I would look to see how the Update module currently handles display errors to the user when it has problems attempting to get the update data via drupal.org update XML feeds.

    I had said that I thought I didn't report an error to the user until it had a certain number of fail attempts. This was from briefly looking at the code around the setting max_attempts

    such as

    if (empty($this->failed[$fetch_url_base]) || $this->failed[$fetch_url_base] < $max_fetch_attempts) {
          $data = $this->updateFetcher->fetchProjectData($project, $site_key);
        }
    

    It turns out I read this wrong. 😅. Actually it reports an error to the user every attempt saying "Failed to fetch available update data:" with a link to this handbook page. This message was just added in #1538118: Update status does not verify the identity or authenticity of the release history URL. It also shows this message if didn't just attempt to request the XML but the last time was a failure.

    What the max_attempts is actually for is to limit the number of times it will attempt to get XML on failure before the update data is cleared.

    I don't think we actually need something like max_attempts for this new code. The big difference is each project has its own XML so if you have 50 contrib modules that would be 50 attempts each time updates are checked. But with the PSA feed it is only 1 attempt each time it is tried for every Drupal site regardless of the number of extensions installed.

    So I think we should follow the same pattern here and report the fetch error to the user on the status report. For this problem with update XML the status report has a minimal message but then links to admin/reports/updates/update since we don't have page like that for PSAs we should probably link the handbook page directly on the status report page.

  3. Removing exception handling from \Drupal\update\SecurityAdvisories\EmailNotify::send() because this is done on cron which handles catching and logging exceptions
  4. Changing _update_advisories_requirements() to only catch GuzzleException and display a similar message and link to the same handbook page that is already displayed if there is problem fetching XML.

    Right now i am reusing the newly added update-fetch-error-message.html.twig and template_preprocess_update_fetch_error_message() to display the message.

    This templates description is

    Default theme implementation for the message when fetching data fails.

    which I think this case fits.

    This allows removing the awkward \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher::getErrorMessageFromException()

  5. changing the handling of a problem with decoding the JSON fetch to just log a message. This seems very unlikely to happen. It would only happen if the feed was fetched with no problem but had invalid JSON.

Status: Needs review » Needs work

The last submitted patch, 128: 3041885-128.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review

UpdateReportTest fail because it calls template_preprocess_update_fetch_error_message() directly and didn't set the new value. It has a default value so any non test callers should use the render pipeline which will set the default value.

tedbow’s picture

StatusFileSize
new2.39 KB
new84.86 KB

🤷🏼‍♂️

xjm’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -122,16 +111,7 @@ public function send(): void {
    -    try {
    -      $messages = $this->securityAdvisoriesFetcher->getSecurityAdvisoryMessages();
    -    }
    -    catch (\Exception $exception) {
    -      $this->logger->error(
    -        'Unable to send a PSA notification email because of an error retrieving the PSA feed. %error',
    -        ['%error' => SecurityAdvisoriesFetcher::getErrorMessageFromException($exception)]
    -      );
    -      return;
    -    }
    +    $messages = $this->securityAdvisoriesFetcher->getSecurityAdvisoryMessages();
    

    Can we add an inline comment here pointing to the spot where cron handles exceptions?

  2. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -136,38 +147,12 @@ public function getSecurityAdvisoryMessages(): array {
    -      throw new \UnexpectedValueException('Drupal PSA JSON is malformed.');
    +      $this->logger->error('The security advisory JSON feed from drupal.org could not be decoded');
    

    Nit: Drupal.org should be capitalized and there should be a period at the end of the sentence.

    I can't remember if the logger logs strings or plaintext. Either way, we should probably provide an action in the logger message (with a link to the handbook page we discussed, I think).

  3. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -237,6 +222,9 @@ protected function matchesExistingVersion(SecurityAdvisory $sa): bool {
    +    // The project name on the security advisory will not always match the
    +    // machine name for the extension, so we need to search through all
    +    // extensions of the expected type to find the matching project.
    

    When would this be the case? Are we talking submodules, like Views and Views UI of old? I thought we got rid of submodules during D8 development. Or something else?

  4. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -148,6 +148,11 @@ public function testPsa(): void {
    +    // Test site status report displays error.
    

    "Test that the site status report displays an error."

  5. +++ b/core/modules/update/update.install
    @@ -190,13 +190,17 @@ function _update_advisories_requirements(array &$requirements): void {
    +  catch (GuzzleException $exception) {
    +    $requirements['update_advisories']['title'] = t('Critical security announcements');
    +    $requirements['update_advisories']['severity'] = REQUIREMENT_WARNING;
    +    $requirements['update_advisories']['description'] = ['#theme' => 'update_fetch_error_message', '#fetch_type' => 'advisories'];
    +    watchdog_exception('update', $exception, 'Failed to retrieve security advisory data.');
    

    Weren't we going to link a handbook page here for debugging problems with the feed? It might need to be created still; the existing docs are more developer-focused and we should create a page that has troubleshooting info for site builders.

  6. +++ b/core/modules/update/update.module
    @@ -19,7 +19,7 @@
    -use GuzzleHttp\Exception\TransferException;
    +use GuzzleHttp\Exception\GuzzleException;
    
    @@ -85,11 +85,8 @@ function update_page_top() {
    +      catch (GuzzleException $exception) {
    

    Since my original review, I learned a lot more about Guzzle's exceptions and how we respond to them in core. Since then #3188920: Make Guzzle exception handling forward-compatible with Guzzle 7 has also been committed, which updates all instances of "try a network request and catch and handle a failure" in core to use TransferException. There are no places that use GuzzleException generally in core, and there is also a followup for D10 #3189301: Use \Psr\Http\Client\ClientExceptionInterface instead of \GuzzleHttp\Exception\TransferException to use a standard PSR interface for the exceptions.

    So I was wrong and I think we can just respond to TransferException as well.

  7. +++ b/core/modules/update/update.module
    @@ -85,11 +85,8 @@ function update_page_top() {
    +        watchdog_exception('update', $exception, 'Failed to retrieve security advisory data.');
    

    That aforementioned handbook page should be linked here too.

xjm’s picture

One other thing. The message says "failed to retrieve security advisory data," but as previously discussed, they're mostly PSAs and might sometimes be SAs. So I think we should say "security announcement data" to cover both cases.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.04 KB
new85.38 KB
  1. Added the comment in update_cron where this is called. I also add a @throws tag here to document the exception could be thrown.
  2. fixed
  3. The project name in the security advisories is what the used to create the project on Drupal.org. There are is no guarantee the developer will actually use that for the module name. The cases I have seen are mymodule vs my_module or the machine of the module just being initials and the project name being words separate by underscores\Drupal\Core\Utility\ProjectInfo::processInfoList() which is called by the UpdateManager because the update XML always has this same problem. Drupal.org packaging add project to the info.yml file.
  4. fixed
  5. See the existing template_preprocess_update_fetch_error_message() which will be displayed here. This is the page that is linked is https://www.drupal.org/node/3170647. I think the same problems would apply but we would probably need to update explain the new use case
  6. fixed
  7. Added
phenaproxima’s picture

These are all relatively minor points. For my own reference, I got part of the way through the patch; stopped at the first kernel test.

  1. +++ b/core/modules/update/config/install/update.settings.yml
    @@ -8,3 +8,6 @@ fetch:
    +advisories:
    +  endpoint: 'https://updates.drupal.org/psa.json'
    +  interval_hours: 12
    

    Do we need an update path, to set the default values for these settings?

  2. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,151 @@
    +/**
    + * Provides a service to send email notifications for security advisories.
    + */
    +class EmailNotify {
    

    Two things, both nitpicks: 1) EmailNotify seems like an odd name for a service class. Would EmailNotifier make more sense? 2) Should we mark this class internal?

  3. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,151 @@
    +  /**
    +   * The time service.
    +   *
    +   * @var \Drupal\Component\Datetime\TimeInterface
    +   */
    +  protected $time;
    

    Might just be a brain glitch, but I'm not seeing where we're using this...?

  4. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,151 @@
    +    $this->stringTranslation = $string_translation;
    

    We should use $this->setStringTranslation() here, so that we're consuming StringTranslationTrait's API.

  5. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotify.php
    @@ -0,0 +1,151 @@
    +   * @throws \GuzzleHttp\Exception\TransferException
    +   *   Thrown if an exception occurs during the request for security advisories
    +   *   feed.
    

    Do we need this? The method does not actually throw an exception; it just doesn't catch the one that may come from the security advisory fetcher.

  6. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +/**
    + * Defines a service class to get security advisories.
    + */
    +class SecurityAdvisoriesFetcher {
    

    Again, should this be internal? It feels like a good candidate for that.

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +  /**
    +   * The update key/value store.
    +   *
    +   * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
    +   */
    +  protected $tempStore;
    

    According to the changes in update.services.yml, this is a regular expirable key-value store, NOT one of the tempstore services. I know they use similar mechanisms under the hood, but they're still treated as distinct concepts in Drupal, so maybe we should rename this property to $keyValueStore or something else?

  8. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +  /**
    +   * The time service.
    +   *
    +   * @var \Drupal\Component\Datetime\TimeInterface
    +   */
    +  protected $time;
    

    I must be blind, but I don't see where we're using this either...

  9. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +   * @throws \GuzzleHttp\Exception\TransferException
    +   *   Thrown if an exception occurs during the request for security advisories
    +   *   feed.
    

    Maybe we could reword this for clarity? How about "Thrown if an error occurs while retrieving security advisories."?

  10. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +        catch (\UnexpectedValueException $unexpected_value_exception) {
    +          // Ignore items in the feed that are in an invalid format. Although
    +          // this is highly unlikely we should still display the items that are
    +          // in the correct format.
    +          continue;
    +        }
    

    Why not at least log the exception message as debugging info, or even a warning? If invalid items
    really are highly unlikely, then it seems worthwhile to capture some information if we do see one.

  11. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +    if ($existing_version = $this->getProjectExistingVersion($sa)) {
    +      $existing_project_version = ModuleVersion::createFromVersionString($existing_version);
    

    Why not have $this->getProjectExistingVersion() return an instance of ModuleVersion directly?

  12. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +      // If a site is running a dev version of Drupal core or an extension we
    +      // cannot be certain if their version has the security vulnerabilities
    +      // that make any of the versions in $insecure_versions insecure. Therefore
    +      // we should err on the side of assuming the site's code does have the
    +      // security vulnerabilities and show the advisories. This will result in
    +      // some sites seeing advisories that do not affect their versions but it
    +      // will make it less likely that sites with the security vulnerabilities
    +      // will not see the advisories.
    

    For whatever it's worth, I think this is the right choice. 👍

  13. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +      if ($existing_project_version->getVersionExtra() === 'dev') {
    

    Not in scope, but I wish that ModuleVersion didn't use the vague word "extra" to describe stability-related parts of the version string. That really should be getStability(). But hey, since ModuleVersion is final, maybe we can change it later in a follow-up :)

  14. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +          catch (\UnexpectedValueException $exception) {
    +            // Version numbers that start with core prefix besides '8.x-' are
    +            // expected in $insecure_versions but will never match and will
    +            // throw an exception.
    +            continue;
    +          }
    

    Is this the only reason why createFromVersionString() will ever throw \UnexpectedValueException? If not, then we might need to narrow this catch.

  15. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +            if ($existing_project_version->getMinorVersion() === $insecure_project_version->getMinorVersion()) {
    

    IMHO this should be elseif.

  16. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +    return [];
    

    A minor thing, but why not return NULL here (and change the return type hint to ?array) if the project lookup fails? That'd be a clearer signal to calling code that the project is absent.

  17. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,258 @@
    +   * @return string
    +   *   The project version or an empty string if the project does not exist on
    +   *   the site.
    

    Same question. Why not return ?string, so that the result is clearer to calling code?

  18. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    +   * @throws \UnexpectedValueException
    +   *   Thrown if the array is not a valid security advisory.
    +   */
    

    This method doesn't actually throw anything, so do we need this @throws in the doc comment?

  19. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    +  public static function createFromArray(array $data): SecurityAdvisory {
    

    Nit: I think we can use self as the return type hint if we want to.

  20. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,209 @@
    +        'insecure' => new Type(['type' => 'array']),
    

    Is there any use in validating the contents of the array (i.e., that it's a bunch of non-empty strings)?

  21. +++ b/core/modules/update/templates/update-advisory-notify.html.twig
    @@ -0,0 +1,34 @@
    +  {% set settings_link = path('update.settings') %}
    

    Since this is going to be in an email, surely we want this to be a full absolute URL? As far as I know, the path() function doesn't do that.

  22. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    // the 'project' properties do not match 'project' is used for matching
    

    Can you add a comma after the word "match"? This is a bit hard to parse otherwise.

  23. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    $assert->pageTextContains('Critical Release - SA-2019-02-19');
    +    $assert->pageTextContains('Critical Release - PSA-Really Old');
    +    $assert->pageTextContains('AAA Update Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    +    $assert->pageTextContains('BBB Update project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    +    $assert->pageTextNotContains('Node - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    +    $assert->pageTextNotContains('Views - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    

    If these are links, maybe we should use $assert->linkExists() instead?

  24. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    // Test site status report.
    +    $this->drupalGet(Url::fromRoute('system.status'));
    +    $assert->pageTextContains('Critical Release - SA-2019-02-19');
    +    $assert->pageTextContains('Critical Release - PSA-Really Old');
    +    $assert->pageTextContains('AAA Update Project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    +    $assert->pageTextContains('BBB Update project - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    

    Why don't the Node and Views PSAs show up here? If that's intentional, this could probably use a comment.

  25. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    $assert->pageTextContains('Critical Release - SA-2019-02-19');
    +    $assert->pageTextContains('Critical Release - PSA-Really Old');
    +    $assert->pageTextNotContains('Node - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    +    $assert->pageTextNotContains('Views - Moderately critical - Access bypass - SA-CONTRIB-2019-02-02');
    

    Same question; why don't the AAA and BBB advisories appear?

  26. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    $this->container->get('cron')->run();
    

    Is there any reason for us not to use CronRunTrait in this test?

  27. +++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
    @@ -0,0 +1,255 @@
    +    $this->container->get('state')->set('system.test_mail_collector', []);
    

    Not in scope, but I wish there was a helper method for this in AssertMailTrait.

tedbow’s picture

StatusFileSize
new22.27 KB
new86.51 KB
  1. Good catch added. Added update_update_9101(). Is that the numbering? hook_update_N() just documents the numbering for contrib as far as I could tell

    also added an update test

  2. Renamed EmailNotifier

    Not changing to internal. Going to leave it to my next patch to address what should internal or an API

  3. Nope it is not used anymore. removed
  4. fixed
  5. see #134.1 and the original comment it addresses
  6. changing to internal. but will look back discussion at this with @xjm
  7. Changed to $keyValueExpirable
  8. no longer used. removed
  9. fixed
  10. logging exception
  11. because we need the string version later in the function
    return in_array($existing_version, $insecure_versions, TRUE);
  12. 👍
  13. I also wish we called ExtensionVersion since it is not always a module
  14. Updated the comment. This is just an expected exception but any invalid version number should not stop up from looping through $insecure_versions. For instances if we find "🐶" as a version number we should ignore it and evaluate the other versions.
  15. fixed
  16. fixed
  17. fixed
  18. fixed
  19. fixed
  20. No because for PSA we don't evaluate this at all. So we don't want to miss a PSA because this field has invalid values
  21. This seems to make the absolute URL. Added test coverage for it

Stopping at 21) for now because @phenaproxima said he has to time to review the progress

tedbow’s picture

StatusFileSize
new9.81 KB
new86.47 KB

Continuing with #135

  1. fixed
  2. fixed
  3. This is leftover from the contrib module tests. These items are actually in our test fixtures anymore.

    I am checking with @drumm whether the PSA feed will always have project:"drupal" for core items or if will sometimes have name of an individual extensions inside core. The contrib module has logic for finding the version of experimental modules in core which if the feed always uses project:"drupal", we don't need.

    UPDATE: @drumm confirm for core advisories it will be project:"drupal"

  4. Added these back they should appear.
  5. I didn't know about CronRunTrait. Fixed

Also
I had discussions with @xjm and @effulgentsia about what should be internal and what should be final

  1. Making \Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher final but not internal.

    It is not internal because we want people to call it to get the messages. In this class we have some logic in matchesExistingVersion() that we do not want encourage others to duplicate by having to process the feed themselves. This is code has logic about which versions should be considered matching and therefore which highly critical advisories should be shown for a site. If this is class is internal then other code should not call it and if they want advisories for their site they have to rewrite this logic. Advisories in this feed will hopefully be very rare. If there is not a real item for a year once there is a real item it is too late if the developer messed something up they may have missed the advisory and the site could already be hacked.

    For the same reason we should make this class final. If we build this as a class that developers have easy access to extend core's behavior about what advisories to show on their site there is no real way for a developer to test if they have gotten this right with real data from drupal.org. Again if they extend this class but mess something up they may not know until they mess a high critical advisory.

    If developers want to use the JSON feed for something else, like a list of advisories for projects not install on the current site(say for aegir) they can easily write code to consume this JSON feed directly for that purpose.

    @effulgentsia wondered if ever make classes not @internal but final. We found \Drupal\Core\Config\ConfigEvents among others

  2. For \Drupal\update\SecurityAdvisories\EmailNotifier I have made this both @internal and final

    I added a class comment that explains that
    a) hook_mail_alter() can be used to alter the emails sent from the class
    b) To send other emails or notifications for advisories the update.sa_fetcher service should be called directly.

tedbow’s picture

StatusFileSize
new4.48 KB
new87.02 KB

Self review

  1. +++ b/core/modules/update/config/install/update.settings.yml
    @@ -8,3 +8,6 @@ fetch:
       emails: {  }
    

    Changing this to protected since all the rest are. Functionality it is the same but if we later made this class not final the diff would be smaller.

  2. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +   * Constructs an EmailNotify object.
    

    This should be EmailNotifier

  3. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,244 @@
    +   * The update key/value store.
    

    Adding "expireable" to the description here.

  4. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,244 @@
    +   * Gets security advisory links.
    

    Added "that are applicable for the current site" here to make sure it is clear we don't just return links for all items in the feed.

  5. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,244 @@
    +    $items = [];
    

    Changing this to $links now that we are return link objects.

  6. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,244 @@
    +        if ($sa->isPsa() || $this->matchesExistingVersion($sa)) {
    

    Adding a comment about as to why we always display PSAs regardless of whether they match the current version on the site

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisory.php
    @@ -0,0 +1,206 @@
    +      'allowExtraFields' => TRUE,
    

    Added a comment that we set this so new fields in the JSON feed will not cause a validation error.

    Also added a unknown field to \Drupal\Tests\update\Unit\SecurityAdvisoryTest::getValidData() to prove this.

phenaproxima’s picture

Read the last two interdiffs and I think they look good! No complaints here.

xjm’s picture

Status: Needs review » Needs work

It's failing CS checks though.

phenaproxima’s picture

Reviewing the changes requested in #132:

  1. ❌ Has this been added? If so, I'm not seeing it...
  2. ✅ Capitalization is fixed, but it doesn't look like a link to the handbook has been added to the log message.
  3. N/A for code changes, but could still use some explanation from @tedbow.
  4. ✅ Fixed.
  5. ❌ Doesn't appear to be fixed yet.
  6. ✅ The patch now contains several mentions of TransferException, but none of GuzzleException. So I'd call this one fixed.
  7. ✅ This one looks fixed, but I guess we still need to fix this for points 5 and 2.
tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new1.2 KB
new87.07 KB

re #141 check on #132

@phenaproxima thanks for double checking this.

  1. I did not add the comment there but in update_cron()

    see my comment in #134.1

  2. The handbook page is linked in template_preprocess_update_fetch_error_message() see my comment in #134.5

Also fixed the spelling error that caused the fail in #138

phenaproxima’s picture

I don't think I found anything really wrong here. I still want to give it a manual test before RTBC, and get these questions/comments addressed. But feel free to push back on any of them; they're mostly in nitpick territory.

  1. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +  /**
    +   * State key used to store a hash of the last messages emailed.
    +   */
    +  protected const LAST_MESSAGES_STATE_KEY = 'update_sa.last_messages_hash';
    

    Does this need a @var string?

  2. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +   *   Thrown if an exception occurs during the request for security advisories
    +   *   feed.
    

    Nit: This phrasing is a bit awkward. How about "Thrown if an exception occurs while fetching the security advisories"? Or, we could just copy the same comment from getSecurityAdvisoryLinks().

  3. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +    $messages = $this->securityAdvisoriesFetcher->getSecurityAdvisoryLinks();
    +
    +    if (!$messages) {
    +      return;
    +    }
    

    Nit: Maybe $links would be a more accurate name for this variable?

  4. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +    $messages_hash = hash('sha256', serialize($messages));
    

    Is it safe for us to directly serialize a set of Link objects like this? Since they can carry a service reference (the link generator), yet don't have DependencySerializationTrait, it might be wise for us to convert the links to strings first before hashing them.

  5. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +      '@count urgent security announcement requires your attention for @site_name',
    

    Nit: "1 urgent security announcement" sounds machiney. Why not go with a more natural "An urgent security announcement" instead?

  6. +++ b/core/modules/update/src/SecurityAdvisories/EmailNotifier.php
    @@ -0,0 +1,145 @@
    +    foreach ($notify_emails as $notify_email) {
    +      /** @var \Drupal\user\UserInterface[] $users */
    +      $users = $user_storage->loadByProperties(['mail' => $notify_email]);
    

    Not a big deal at all, but it's unfortunate that we need to do multiple separate load operations as we loop over the email addresses. Maybe this could be refactored to load them all in one fell swoop and key the resulting user entities by their email address.

  7. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,248 @@
    +  use DependencySerializationTrait;
    

    Why does this class need DependencySerializationTrait?

  8. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,248 @@
    +  /**
    +   * The extension lists keyed by extension type.
    +   *
    +   * @var \Drupal\Core\Extension\ExtensionList[]
    +   */
    +  protected $extensionLists;
    

    Nit: Maybe this should default to an empty array.

  9. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,248 @@
    +   * @return \Drupal\Core\Link[]
    +   *   Links to the upstream advisories, using the advisory title as the text.
    

    Nit: Is the word "upstream" really needed here?

  10. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,248 @@
    +    if ($json_payload !== NULL) {
    

    I think is_array() might be a better check here, since "not NULL" still means it could be scalar.

  11. +++ b/core/modules/update/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
    @@ -0,0 +1,248 @@
    +          $links[] = new Link($sa->getTitle(), Url::fromUri($sa->getUrl()));
    

    Nit: IMHO it would be cool if $sa had a toLink() method which would generate the link, instead of relying on calling code to do it.

  12. +++ b/core/modules/update/update.install
    @@ -177,3 +193,37 @@ function _update_requirement_check($project, $type) {
    +  if (!\Drupal::hasService('update.sa_fetcher')) {
    +    return;
    +  }
    

    This could use a comment. Why wouldn't the service exist?

  13. +++ b/core/modules/update/update.services.yml
    @@ -22,3 +22,26 @@ services:
    +  update.sa_fetcher:
    +    class: Drupal\update\SecurityAdvisories\SecurityAdvisoriesFetcher
    +    arguments:
    +      - '@config.factory'
    +      - '@keyvalue.expirable'
    +      - '@http_client'
    +      - '@extension.list.module'
    +      - '@extension.list.theme'
    +      - '@extension.list.profile'
    +      - '@logger.channel.update'
    +  update.sa_notifier:
    +    class: Drupal\update\SecurityAdvisories\EmailNotifier
    +    arguments:
    +      - '@plugin.manager.mail'
    +      - '@update.sa_fetcher'
    +      - '@config.factory'
    +      - '@language_manager'
    +      - '@state'
    +      - '@entity_type.manager'
    +      - '@string_translation'
    

    Don't we need to put the arguments all on one line?

phenaproxima’s picture

StatusFileSize
new40.05 KB

In manual testing, I noticed something:

A screenshot of critical security issues being displayed in the admin interface.

"Critical security announcements" is maybe not the most friendly or useful wording, considering that we display these as errors. Maybe we should expand that a bit (two sentences tops) to tell people what these links are, and what they should do to make the errors go away, since they'll definitely want to do that.

Another thing I noticed is that, when I set it to use the invalid JSON endpoint, no message is ever displayed to me unless I think to look at /admin/reports/dblog. If I don't look there, I wouldn't notice that anything is wrong at all. The hook_requirements() code is catching TransferException, but no exception is thrown if the JSON cannot be parsed...so it just gets logged quietly to watchdog. Would it be useful for us to show something in the status report if the feed could be fetched, but not parsed?

The thing thing is more of a wish, but when I change the advisories.endpoint configuration setting, I need to manually clear the response cached in the key-value store. It'd be amazing if that would automatically be cleared whenever the setting is changed. To be clear, it's not very likely that anyone will ever change the setting...but the fact that it's possible means that we should probably account for it. Or just hard-code it, which I think would be fine (as long as there are workarounds in place for testing).

tedbow’s picture

StatusFileSize
new9.16 KB
new88.69 KB

@phenaproxima thanks again for the reviews.

In this patch I am just addressing your last point now in #144 about the changing "advisories.endpoint configuration setting" and clearing the cached response

To be clear, it's not very likely that anyone will ever change the setting...but the fact that it's possible means that we should probably account for it. Or just hard-code it, which I think would be fine (as long as there are workarounds in place for testing).

agree if that if it is config setting we have to account for people changing it. The only reason I think it is a config setting is for testing. It came from the contrib module.

Also allowing this to be changed implies that we support other feed sources besides drupal.org. If we do that then have to careful if we change the format or the processing in core because we have to consider that other sources for the json feed will not have updated to new format. I don't see a reason to support this.

For testing I figured out that if we decorate the http_client service we can just switch out URI when $uri === 'https://updates.drupal.org/psa.json' to our test endpoints.

tedbow’s picture

StatusFileSize
new10 KB
new87.96 KB

back to #144

  1. No, it appears const declarations in core don't use @var. It least all the ones I looked at
  2. copied from getSecurityAdvisoryLinks(). It would be the same reason for the exception so it should be the same.
  3. fixed. also change other places in this class that refer to "messages" and changed to "links"
  4. Good catch. Before they were formatted text so it didn't matter......

    Actually this is bit tricker. Calling $link->toString() actually causes a problem not here but later in
    $this->mailManager->mail('update', 'advisory_notify', $notify_email, $params['langcode'], $params); because at that point the links are serialized.

    So what happens is calling $link->toString() then calls \Drupal\Core\Link::getLinkGenerator() which actually sets the link generator service. So they do not have the service reference to the service until you call toString()🤯

    I got around this by using

    $links_string .= ':' . $link->getUrl()->getUri() . ':' . $link->getText();
    

    This doesn't set any services.
    So I can still get unique string for a set of links. Also since there should never be that many links I don't think we even need to hash this string.

  5. The human touch, I like it. fixed
  6. I was able to simply this section

    We can actually load the all the users at once using

    $user_storage->loadByProperties(['mail' => $notify_emails])
    

    We can send multiple emails and will return back all the users that match. Ultimitely this is handled in \Drupal\Core\Entity\EntityStorageBase::buildPropertyQuery() which is using an IN clause.
    We don't need to worry about the key to the array because the we can just get the email from the loaded user. I updated testPsaMail() to test 2 emails to make this works(should have had this to test the loop anyways).

    Also

    $params['langcode'] = $users ? (reset($users))->getPreferredLangcode() : $default_langcode;
    

    we don't need default_langcode because getPreferredLangcode() has an optional $fallback_to_default = TRUE which will return back the default for us. Also using getPreferredLangcode() is safer anyways because it handles the case where users prefered language is not an available language on the site(I guess if it was when it was selected)
    Now the class does not need the language_manager service

  7. I don't think it does. Removed and test this pass.
  8. sure, fixed
  9. @xjm suggested this. probably to make it clear these messages are generated by the site. Leaving for now
  10. fixed
  11. Personally I like keeping SecurityAdvisory. Right now it only knows about the expected keys and validation. It has no opinion on how it might be output as far is it going to output as a link or markup. Also maybe it won't be output at all, maybe it would be just read to determine if there are any messages and then the user redirected to the advisionary URL. I would rather not be connected to what SecurityAdvisory anymore that it has to be. For instance if drupal 10 uses NewImprovedLink then as it is now SecurityAdvisory would not need to be updated
  12. I was thinking if a site wanted to process these PSA in different way or say a hosting company notified the users on platform level instead of the individual site level they may want to remove our services.

    I don't particularly want to suggest reasons in comment. The fact is core allows altering services so they could not be there for any number of reasons and if we don't check we will get an exception

  13. either works. I won't sure if we had a standard to break it up into multiple lines like we do for php arrays after certain length
    But fixed because I haven't seen this done in core.

Not addressing #144 for now.

tedbow’s picture

StatusFileSize
new8.49 KB
new92.48 KB

Addressing #144 now

  1. Regarding the wording, we did a UX review the wording other and other aspects on this and updated with the changes suggested in #66. Would rather not second guess that meeting. It was agreed that less text is better.

    There is not a universal way to remove this messages. Some can be removed by updating to a new version of a projects. Others are for upcoming releases so they cannot be removed until the new version is released and installed. Others are for changes that need to be made that don't involve project update but maybe a server change, for those the message will stay until it is removed from the feed.

    Having any items in the feed will be super rare and they are for highly critical issue so I think it is ok if admins can't easily remove them. Also they will be in the feed for a short period of time(less than a week?). In addition most users will not have permission to see the items.

  2. We also went over this in the UX meeting. If the feed was successfully retrieved but it can't be parsed there is nothing the admin can do about it, it's inactionable, so didn't want to have message in the logs for this.
  3. I addressed the point about the config endpoint changing #145 by hardcoding the endpoint

I also talked to @phenaproxima about deleting stored feed response when the interval config setting is changed. I have added this but only if the interval is decreased. If the interval is increased then it should already handle waiting until the new interval is reached.

phenaproxima’s picture

+++ b/core/modules/update/tests/modules/update_test/src/AdvisoriesTestHttpClient.php
@@ -0,0 +1,47 @@
+  public static function setTestEndpoint(string $test_endpoint):void {

Need a space after the colon.

+++ b/core/modules/update/tests/src/Functional/SecurityAdvisoryTest.php
@@ -167,9 +167,10 @@ public function testPsa(): void {
+    $this->createUser([], 'GracieDog');

🐕

Would rather not second guess that meeting. It was agreed that less text is better.

I agree that less is better, but IMHO that just means we need to make whatever text we do have as useful and concise as possible. I don't find the current text very useful, to be honest, but if this was signed off on by the UX team, I won't stand in the way.

There is not a universal way to remove this messages. Some can be removed by updating to a new version of a projects. Others are for upcoming releases so they cannot be removed until the new version is released and installed. Others are for changes that need to be made that don't involve project update but maybe a server change, for those the message will stay until it is removed from the feed.

Fair. Can we make sure this is documented online somewhere? Because I feel like it is a question that will definitely be asked.

I also talked to @phenaproxima about deleting stored feed response when the interval config setting is changed. I have added this but only if the interval is decreased.

👍 I think that's a great idea.

  1. +++ b/core/modules/update/src/EventSubscriber/ConfigSubscriber.php
    @@ -0,0 +1,59 @@
    +class ConfigSubscriber implements EventSubscriberInterface {
    

    Suggestion: it seems appropriate, in this case, to have SecurityAdvisoriesFetcher implement EventSubscriberInterface and react to config changes. That keeps all of the logic related to fetching the feed and managing the stored response in the same class, which means we won't need to share underlying details, like the name of the storage key, across multiple classes. SecurityAdvisoriesFetcher has a very clear responsibility, and I think this falls squarely within its boundaries. What do you think?

  2. +++ b/core/modules/update/tests/src/Kernel/SecurityAdvisoriesFetcherTest.php
    @@ -467,6 +462,9 @@ public function providerIgnoreAdvisories(): array {
    +    $this->container->get('kernel')->rebuildContainer();
    

    If you do this, I think you might also need to do $this->container = $this->container->get('kernel')->getContainer().

tedbow’s picture

StatusFileSize
new2.06 KB
new92.74 KB

re #148

Need a space after the colon.

fixed

Fair. Can we make sure this is documented online somewhere? Because I feel like it is a question that will definitely be asked.

I will make a hanbook page. not sure if we should link to the page in update_page_top() or just in the status page.

maybe after the PSA links. because I don’t think we want “click here to find out more about these messages” above the PSA links. then they might think the handbook page is enough to read rather than clicking on the actual advisory links
Thoughts

to number points

  1. copying from my chat with @phenaproxima
    ok. I think I am against moving into SecurityAdvisoriesFetcher . I think you are right it would be nice to have it all in 1 place. If this wasn’t core and there wasn't an existing pattern I would agree

    but I think that is outweighed by the benefit of following an existing pattern. If I look at the classes that implement EventSubscriberInterface in core almost all are *Subscriber , and there are couple other ConfigSubscriber classes. They seem to mostly to be in `[module]\EventSubscriber` namespaces.
    So I think if I were to look at the Update module and wanted to know if it had event listeners I would look in `update/src/EventSubscriber`. even the ones that don’t follow the naming pattern are still stand alone classes that just react to events.
    I think I should a @see though to the other class

    if it starting scratch I think your pattern is better. but I think in this case following the pattern might make it easier for future developers

    I will not add a @see in \Drupal\update\EventSubscriber\ConfigSubscriber::onConfigSave because that already has
    $this->keyValueExpirable->delete(SecurityAdvisoriesFetcher::ADVISORIES_RESPONSE_EXPIRABLE_KEY);
    which directly point to that class clearer that a @see but will put a @see in the other class point to this one.

  2. fixed

Still @todo add the handbook page and link to it

tedbow’s picture

StatusFileSize
new3.37 KB
new95.06 KB
  1. Added the info to hook_help and add a link to the help we display messages
  2. I suggested to @xjm and @phenaproxima that this functionality to the system module instead. A lot of sites do not have the update module enabled on production for security reasons. We don't want those sites to have enable the update module just to get these advisories.

    The change would not be that hard. @xjm was going to try to get other feedback on this

xjm’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/update/update.module
    @@ -46,6 +46,11 @@ function update_help($route_name, RouteMatchInterface $route_match) {
    +      $output .= '<dd>' . t('The Update Manager module displays highly critical and time sensitive security announcements published by the Drupal Security Team. Some security announcements will be displayed until a critical security update is installed. Announcements that are not associated with a specific release will appear for a period of time determined by the Drupal Security Team.') . '</dd>';
    

    Nit: time-sensitive.

    Also, upon reflection, I think we should drop the words "published by the Drupal Security Team", and change the second sentence to end with "...for a fixed period of time." (We avoid printing the word "Drupal" at runtime so that distros can be rebranded etc.)

    Finally, this makes me wonder if we should make the message only a warning and not an error if there's no release associated with it.

  2. +++ b/core/modules/update/update.module
    @@ -83,6 +88,11 @@ function update_page_top() {
    +          if (\Drupal::moduleHandler()->moduleExists('help') && !($route_name === 'help.page' && $route_match->getParameter('name') === 'update')) {
    +            \Drupal::messenger()->addError(t('<a href=":update-help">What are critical security announcements?</a>.', [
    +              ':update-help' => Url::fromRoute('help.page', ['name' => 'update'])->toString(),
    +            ]));
    +          }
    

    🤔 I don't think it makes sense to print a separate error message for this. Let's append it to the string for the existing message, conditionally based on help being installed, or make it an info message (does the messenger service even support those?) rather than an error.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new18.62 KB
new23.07 KB
new1.46 KB
new94.99 KB

@xjm thanks the review(I know more is coming 😉)

  1. Fixed
  2. There could be multiple(not likely errors messages. so this just adds another line. see the screenshot.
    advisories messages as errors

    The messenger service supports "status" messages but the problem is that it groups them all together and groups the error messages together. So there would be guarantee that if we used a "status" message it would not be sandwiched between 2 unrelated status messages if there are other messages on the page. It is not unlikely there will be other messages on the page since these are admin pages.

    Here is an example of doing a status message before the advisories messages and 2 after

    a screenshot showing the stauts message would appear above and with other status messages.

xjm’s picture

I'd maybe wrap the "What are critical security announcements?" in unlinked parens rather than that straggling period.

What do you think of the suggestion to add logic to make it a warning only if it's a PSA that we can't tell if they've addressed or not?

phenaproxima’s picture

On the help page that we link to from "What are critical security announcements?", maybe we should explicitly mention that the messages are only made visible to people with the appropriate permissions. Otherwise, admins might worry that anonymous users will see them too.

tedbow’s picture

Just commenting the issue is update after all the Merge Request commits and comments

tedbow’s picture

Had further discussion with @xjm and we decided to move this into the system module(see #150)

Of course it was a little move complicated than I thought 🤷🏼‍♂️

I think all the test should pass but somethings that still need to happen. I will comment on the merge request

tedbow’s picture

The test are now failing because of #2571475: Outbound HTTP requests fail with KernelTestBase

Because we now have an outbound request in system_requirements

I updated the patch there to try to get it moving

catch’s picture

Can we double check what happens if the request to Drupal.org times out? Thinking about what will happen if nothing is stored and someone hits the status report page - we would really not want to break that page because d.o is down.

mxr576’s picture

#159++

I hope you do not mind that I also shared my 2 cents. There is already a scary amount conversation on this issue, flooded with patch- and PR reviews, this demonstrates the power of the community :)

Good job!

tedbow’s picture

tedbow’s picture

re #160
@mxr576

I hope you do not mind that I also shared my 2 cents.

Thanks for the detailed review!!!

There have been a lot of discussion the but the new Merge Request comments make it so much easier to have multiple people discuss a particular section of the code!

tedbow’s picture

re #159 @catch thanks for bring this up!

I added a comment on the Merge request so we could have a thread for this https://git.drupalcode.org/project/drupal/-/merge_requests/284#note_10995

tedbow’s picture

Sorry for all the noise here with the commits. I used the "Rebase" button on gitlab because #2571475: Outbound HTTP requests fail with KernelTestBase was committed and that is need to get our tests to pass(locally).

I holding off on some of the problems based on resolution of #3196368: [policy, no patch] Determine to which module the new security advisory functionality should be added because some stuff that are changes to Update module that now would need to be in System won't need to happen if we decide to move it back to Update module

Leaving at needs Review because I did push up some changes but there is still some open questions that will need work.

tedbow’s picture

Regarding changes above, some big changes

  1. Created \Drupal\system\ExtensionVersion which is copy of \Drupal\update\ModuleVersion plus the new changes we needed for that class for this issue.
    I changed the name of the class because it was badly named to begin with. It is not just modules, but themes and profiles

    Deprecated ModuleVersion though it is already internal

  2. Removed all the changes to Update module except deprecation of ModuleVersion. Removed dependence on the Update module for our test
  3. Removed email notification functionality. This is more complicated in the system module because we don't already have an email setting we can use like we do in the Update module. Chatter with @xjm about this and she is good with this
  4. Opened #3197333: Email site admins for security advisories displayed in Drupal as a follow-up. Should mostly be able to reverse this commit.

tedbow credited alexpott.

tedbow credited mglaman.

tedbow’s picture

`\Drupal\Tests\Scripts\TestSiteApplicationTest` was failing because it was attempting fetch the advisories at install time.
@alexpott suggested that tests should have to opt in to having the PSA advisories functionality enabled.

@mglaman helped me figure out how to do this!

Thanks to both and crediting

phenaproxima’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update

@tedbow and I have now iterated on this extensively and I think I'm out of things to complain about.

The scope is very clear: when there are critically important security announcements (which should be an exceedingly rare event), we want sites to nag their administrators about them. That's what this patch does, on most administrator-facing pages, and on the status report. Nice and clear.

I do, however, think the issue summary needs to be updated to reflect the changes that happened during development, including moving this functionality to System (rather than Update). So I'm marking this as needs work for that.

tedbow’s picture

Status: Needs work » Postponed
tedbow’s picture

Status: Postponed » Needs work
Issue tags: +Needs documentation updates

Ok in #3196368: [policy, no patch] Determine to which module the new security advisory functionality should be added it was decided this should be in the system module

So the issue summary needs to be update and we probably need a documentation update for troubleshooting the advisories are not fetchable.

tedbow’s picture

Updated summary. Removed parts of about email notifications and pasted them into a comment on the follow up #3197333: Email site admins for security advisories displayed in Drupal so they won't be lost.

tedbow’s picture

Issue summary: View changes

Added not about how to disable the fetching of the advisories in settings.php to the release code snippet.

phenaproxima’s picture

Issue summary: View changes

Tried rewriting the issue summary for clarity and formatting. I hope it's still accurate!

phenaproxima’s picture

Issue summary: View changes

Removed a few follow-ups that I think are addressed in the merge request. Someone else should prune that list more comprehensively, though.

phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
tedbow’s picture

Issue summary: View changes
  1. @phenaproxima thanks for updating the issue summary. Much better

    Including just 2 edits to the description of determining if a advisories matches to include "development snapshot" because this is the only case it will happen.

  2. We link to this page https://www.drupal.org/node/3170647
    to troubleshoot fetching. It is existing page that mentions the Update module but it should be the same for our use case.

    Now that we are going to have Security Advisories fetching in the System module we need to update this to:

    • refer to the new functionality in addition to the Update module functionality
    • in the “fallback to HTTP” to section mention the system module does not provide a fallback. Maybe with link to the d.o page https://www.drupal.org/security so they can monitor this manually.
phenaproxima’s picture

Component: update.module » system.module
Status: Needs work » Reviewed & tested by the community
StatusFileSize
new28.35 KB

"Keep calm and ship it"

I've now reviewed this code many times and I think I'm out of things to complain about. This is well-scoped and does what it promises to do. From my perspective, it's ready. On the assumption that tests will pass, RTBC. Onward and upward to release manager review, and documentation updates before commit.

tedbow’s picture

So we still have Needs documentation updates tag

  1. Right now we link to PHP OpenSSL requirements if there is error requesting the feed on drupal.org

    This doc page was made for #1538118: Update status does not verify the identity or authenticity of the release history URL and but not under the Update manager but System Requirements section. We should update this page to mention this new use case. There is a section about HTTP fallback but the new request does not have that feature. We probably don't want to have that here as that would open up another attack vector but comprising the request for the feed and linking to a fake advisory. Because advisories might be for non update reasons like needing make some server configuration change we should require HTTPS here otherwise a fake advisory could trick an admin into setting an unsafe server configuration change.

  2. I don't think we need any other documentation for a failed request than the item above. Our new request has no more reason to be more error prone the Update XML request to Drupal.org. The new request actually has less of reason to fail because the JSON file will never be as large as many project's Update XML which will have the complete history of the project's releases.

    If we never needed a documentation page for failed requests for the Update XML until we convert the request to HTTPS in #1538118 I don't see why we would any extra documentation here.

  3. In the official User guide we should create an issue to mention the new request here: https://www.drupal.org/docs/user_guide/en/security-announce.html
  4. I am not where we would need to update the "Wiki" documentation. Any suggestions? or is update the official docs enough?
  5. system_help() has been updated and is linked where the advisories are displayed.
dww’s picture

Status: Reviewed & tested by the community » Needs review

The only reason we added the HTTP-fallback stuff to Update Manager was that people argued that not fetching anything at all for sites where PHP isn't properly configured to allow HTTPS was worse for security overall than the hypothetical man-in-the-middle attack vector. I wasn't entirely convinced, but I agreed to do the work on the fallback to make everyone happy and finally fix that ~9 year old critical bug.

However, we should either use that HTTP fallback consistently, or remove it entirely. This advisories feed is not inherently more important or risky than the release history feeds. If someone can compromise either feed, it's Really Bad(tm) for the site owner. A fake advisory that tries to trick an admin into setting an unsafe server configuration isn't really more dangerous than a fake feed pointing to fake updates with malicious code that the Update Manager might happily install for you. I don't think #181 justifies the exception. That reasoning is fundamentally right that the HTTP fallback is potentially risky, but then we should remove the HTTP fallback from Update Manager, too. I don't think this boils down to documentation updates. I think this either needs work in here to use the HTTP fallback, or we need a follow-up to remove the HTTP fallback entirely and not make a special exception for this feed.

tedbow’s picture

@dww thanks for the context. I think that makes sense. I didn't think of the case that Update manager will use the XML to directly download update when the user submits the update. I agreed that that is more direct way to compromise the site.

If anyone had a case for not adding the fallback here the other option would be on failure in the status report page to an warning with a link security advisory listing and note they should follow this.

In the case of the Update module not getting the XML feed stops the site from using the Update module from downloading updates and via the UI.

But with the assumption that we do want the fallback luckily #1538118: Update status does not verify the identity or authenticity of the release history URL introduced a new setting update_fetch_with_http_fallback and does not rely on an Update module config setting we could use that setting here also. Obviously the that name might be a bit confusing since now it has 2 purposes but I think it would be less confusing than have 2 settings that control a fallback to HTTPS. I can't imagine where you would want to fallback for 1 but not the other.

So we would at least need to update the comment in default.settings.php to mention this will affect the new advisory feed fetching also.

We should probably at some point rename this setting maybe to fetch_with_http_fallback and deprecate the old name. Should just do that here or in a follow up?

tedbow’s picture

  1. re #182 Added the http fallback and test for this.
    Our way of capturing log messages via \Drupal\advisory_feed_test\TestSystemLoggerChannel doesn't work for this test because we need to log `watchdog_exception()` calls which seem to not work with the decorated service. Added a simpler way to capture log messages but not sure that way will work for functional tests. So may need this 2 ways for the tests. I will to try to see if I can get to work with functional tests.
  2. #181.4 I chatted with @jhodgdon about this she suggested

    I leaning towards the 2nd one but haven't added anything yet.

    We should probably update Security advisory process and permissions policy which Drupal Security Team specific page

tedbow’s picture

Issue summary: View changes

added docs section to summary

  1. Main documentation, needs review: Highly Critical Security Advisories within Drupal
  2. Needs to be updated to mention the fetching of advisories: PHP OpenSSL requirements
  3. User guide issue: #3203748: Add information about highly critical security advisories within Drupal
tedbow’s picture

tedbow’s picture

Issue summary: View changes
Issue tags: -Needs documentation updates
StatusFileSize
new21.6 KB
new35.79 KB

Changed the hook help the fail message to point to the new handbook page

hook_help text
We could get rid of the 2nd paragraph if we want because this is in the handbook page but I don't think it hurts to have it here.
failure message
The failure is message is the same as we added recently to the Update module except for the handbook page name. see template_preprocess_update_fetch_error_message()

jhodgdon’s picture

One other Docs To Do on this issue would be to add a Help Topic about this (or add information to an existing help topic). My guess is that it's not desirable to have that in the main patch, as Help Topics are still Experimental. (Not sure about that?)

xjm’s picture

Status: Needs review » Needs work
Issue tags: +Needs followup
  1. So one thing we should change is that we should not say "on Drupal.org" in the user interface.
    • In the first screenshot, first paragraph, we can replace the entire last sentence with:
      <a href=":url">More information on critical security advisories</a>.
    • Similarly, the second sentence of the second paragraph can be replaced with:
      <a href=":url">View all security announcements</a>.
    • Finally, in the dsm, the words "in the Drupal.org handbook" can simply be omitted.

     

  2. In a similar vein, we should not mention "Drupal". (I confirmed with @webchick that it is policy to not use "Drupal" in the user interface so that distros can easily rebrand core.) We can just drop "within Drupal" as well.
     

  3. With the above changes to reduce the verbiage, I think the paragraph linking the full feed is worth having both in the in-module help and on the handbook.
     

  4. How about "Troubleshooting" instead of "debugging"? The former is more user-oriented, and we're not actually giving them debugging instructions; all the suggestions are more high-level. Edit: We would change the word in the handbook page as well.
     

  5. We should add the information about how long the announcements are displayed to the handbook page as well.
     

WRT whether to address Help Topics, I think either on-issue or in a followup is fine. In the case of this issue a followup might be preferable, both because this issue is already so long, and because I think the help topic might have a broader scope of things from other modules related to security and/or updates. Thanks @jhodgdon!

NW for the various text fixes.

tedbow’s picture

Status: Needs work » Needs review
Issue tags: -Needs followup
  1. Fixed
  2. fixed
  3. Not sure if there is any action needed here it is already in "in-module help" and there is link on https://www.drupal.org/docs/updating-drupal/highly-critical-security-adv... also.
  4. Changed in the error message and on the handbook.
  5. Not sure how long items will stay in the feed. Asked on drupal slack and will update. Or @xjm unless you know
  6. Added #3204175: Add security advisories Help Topic as follow-up
xjm’s picture

Status: Needs review » Needs work

NW for my suggested docs changes on the MR; it was not actually fixed in the linked commit.

xjm’s picture

Hiding all patch files since they are very out of date now that we are working on the MR.

tedbow’s picture

Status: Needs work » Needs review

re #191 I misse MR comment before and was going off #190.

should be fixed now

tedbow’s picture

Issue summary: View changes

API Changes in summary

tedbow’s picture

Issue summary: View changes
dww’s picture

Thanks for all the recent effort in here! Haven't looked closely at the MR, but it seems to be very close.

Came across #2378513: Create system for distributing pre-announcements of extreme security issues in core/contrib while triaging, which I just marked as a child of this. Dunno if we should credit those folks here, or there, or what. Please advise (or do the deed). ;) Thanks!

jhodgdon’s picture

I reviewed the API docs in this patch, and made a small suggestion about one spot. The rest looks good to me! (I did not look at the test modules/classes very carefully.)

tedbow’s picture

Added a change record https://www.drupal.org/node/3206473. improvements welcome

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

This is a complex merge request to follow, so it's possible I missed a spot or two. But as far as I can tell, all feedback has been addressed, and all questions have been answered. Documentation appears to be up-to-date, and the remaining tasks in the issue summary appear to be done. Tests are passing. Best I can figure, this is ready.

catch’s picture

A handful of questions/comments in the PR, nothing blocking.

One extra point:

I'm sure this has been discussed in the issue, but 12 hours makes it very easy to spend an entire working day without seeing an update - i.e. check feed at 8am, start work at 9am, finish work at 6pm, next check at 8pm. Why not six hours?

Related to this, do we have a follow-up to e-mail the site admin e-mail address for PSAs the same as we do for security releases? We'd have to track e-mails sent somehow to avoid sending the same thing multiple times.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Just spent 30 minutes reviewing the last patch only to find it was converted to a merge request. Should we hide the patch files?

Copied over the comments that still applied

phenaproxima’s picture

Good point, @larowlan. Hiding the defunct patches.

webchick’s picture

StatusFileSize
new195.25 KB
new239.32 KB

@tedbow and @phenaproxima gave me a live demo of this patch (I didn't review the code because by the time we finished, @larowlan and @catch were looking at the code :)).

Here's my understanding; let me know if I'm wrong about any of this.

In a hypothetical recurrence of a SA-CORE-2018-002 type of situation, the following would happen:

1. A few days before the Wednesday security release (let's say, Sunday night), a Drupal security team member would run a script somewhere to update https://updates.drupal.org/psa.json with a pointer to a PSA node, like https://www.drupal.org/psa-2018-001, with its is_psa flag set to true.

This is done manually, versus just sucking in everything from https://www.drupal.org/security/psa, so that the notices going through this system are truly critical and things all site owners should be aware of. (Which is good, because this code is in System module, and though possible to opt out of, is not something we want people to do or it undermines the entire point of this feature.)

2. All Drupal 8.9.15+ and Drupal 9.2+ and Drupal 10+ sites out there would receive notice of said PSA, visible to users with the "administer site configuration" permission, in both the Status report page, as well as any admin/ path on their site, on the following page load and all subsequent page loads. This message would be a "warning" style, because there is nothing they can actually do about it, yet. (The only way to turn off these messages is to set a configuration setting to opt out of the feature entirely, which once again, we really do not want people to do.)

(I have some questions about performance? what happens if drupal.org is down? do we cache this feed so as not to incur server hits? etc. but they are mostly irrelevant to this improvement; more on the backend side, and just points of curiosity.)

3. On Wednesday, the release is cut, a security team member updates the the JSON feed to the SA (vs. PSA) at https://www.drupal.org/sa-core-2018-002 with its is_psa flag set to false. The notice on all Drupal 8.9.15+ and Drupal 9.2+ and Drupal 10+ sites turns from "warning" to "error" and they are linked to the SA with instructions on what to do. This time the message stays there until you do something to move off of the insecure version of core/your plugin, then it disappears.

4. At some future date (the end of the week? the following week? 6 months later?) the JSON feed is updated once again to remove mention of the critical PSA. This is so these don't stack up over time.

---

Assuming all of that holds...

a) Our #1 goal from a security POV is that people don't turn off or otherwise work around this feature. That means keeping these messages as infrequent and as relevant as possible. If the outcome is IT help desks getting pinged by 300 people across their university (for example) suddenly seeing these messages, they're gonna get blitzed real quick. :\

Infrequency I believe is addressed by this being a manual addition process, separate from https://www.drupal.org/security/psa, "only break in case of emergency." So +1 for that. Better would be a "dismissable message" paradigm for the 2+ days that these messages are sitting there and you can do literally nothing about them, but that's not this feature's problem to solve. (Filed #3211534: Add support for dismissable messages for that, since I couldn't find an existing issue for some reason?)

For relevancy, reviewing the docs at https://www.drupal.org/docs/8/update/automatic-updates#s-public-service-... what I expected was that a PSA like https://www.drupal.org/psa-2019-02-19 would only show up on 8.5.x and 8.6.x sites. But this is apparently not how it works. The "insecure" array in the JSON schema is apparently ignored for PSAs, so I'll see it even if I'm running Drupal 9.

I don't think the lack of this should block the feature, but I do think it's an important consideration. We do not want our first time using this system to end up being a "boy who cried wolf" situation, causing lots of people to get the impression that this system nags them about irrelevant things and either ignore it or turn it off, so that the next time we need them to actually pay attention, they do not.

Filed #3211532: Add metadata to psa.json feed to allow only showing relevant core notices for that.

b) We need to remember that most people who maintain Drupal sites do so in addition to 50,000 other things they do, perhaps dozens of other technology platforms they support, etc. We all know what a string like "PSA-2018-10-17" means, but for someone new to Drupal, or someone who only touches their Drupal site once every 2 years, they very much might not.

So I was surprised when, in contrast with what's shown in the issue summary, the output on /admin pages is just barfing titles directly, with no extra context around it, like "Critical security announcements." There is a link to a help page, but the help shows up kinda halfway down the page with no special styling, and had @tedbow not pointed it out, I would never have found it. :\

List of errors pointing to PSAs
Help hanging out mid-page

I do not think it's reasonable to put the expectation on the security team to wordsmith these titles vs. just getting the information out in the event of a crisis. So a suggestion (does not need to hold up the patch): Just go back to the old labeling shown here:

@tedbow couldn't remember why that was removed. If there's a general feeling that it's not explanatory enough, could also put a [more info] link next to the title that links to the help text.

c) (also non-blocking curiosity) Assuming #4 above is true, why ever get rid of PSAs from the feed? If I haven't looked at my Drupal site in months/years, why not tell me about all of the things that need my attention? This is what Windows does, for example: https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Ffilestore.co...

----

Apart from that, this seems like a GREAT improvement that will help us tremendously down the line if another major issue happens that we need to get the word out about. Awesome work, everyone!

catch’s picture

(I have some questions about performance? what happens if drupal.org is down? do we cache this feed so as not to incur server hits? etc. but they are mostly irrelevant to this improvement; more on the backend side, and just points of curiosity.)

The JSON is stored in key value expirable, and refreshed every 12 hours, however you've just made me think of something else... will comment on the PR.

tedbow’s picture

Assigned: Unassigned » tedbow

Thanks for the feedback everyone. I going through the MR comment now and will push up changes.

@webchick re

@tedbow couldn't remember why that was removed. If there's a general feeling that it's not explanatory enough, could also put a [more info] link next to the title that links to the help text.

i found the commit it was removed in. I will try to match that to date of comments here or in the MR and see if was taken out for reason or if it was mistake.

tedbow’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new19.16 KB
new18.59 KB
new20.85 KB
  1. @webchick The "Critical security announcements:" heading I think was removed by me as a mistake

    It was done in this commit where I was changing the logic where if there are any non-PSA advisories then all the messages should be shown as errors, so they are grouped together. I looked for comments around the date of this commit asking for it be removed and I don't see any.

    Adding screenshots to summary

  2. I think I have addressed all the feedback in the MR comments
tedbow’s picture

Issue summary: View changes

fixing image link

tedbow’s picture

Assigned: tedbow » Unassigned

unsigning myself because addressed all current feedback on MR

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

aaronmchale’s picture

Generally agree with everything in @webchick's review at #203.

Maybe I missed it, but at what point does an email get sent with the notice, and do we have a screenshot of what that email would look like (thinking along the lines of webchick's point about the context of the message that's displayed in the UI conveying the right information)?

phenaproxima’s picture

but at what point does an email get sent with the notice, and do we have a screenshot of what that email would look like

Email will be implemented in #3197333: Email site admins for security advisories displayed in Drupal, not this issue. :)

aaronmchale’s picture

Email will be implemented in #3197333: Email site admins for security advisories displayed in Drupal, not this issue. :)

Ah yes, thank you, I had a vague recollection of it being discussed somewhere but forgot it was in a follow-up :)

I guess my only other thought, relates to webchick's review:

2. All Drupal 8.9.15+ and Drupal 9.2+ and Drupal 10+ sites out there would receive notice of said PSA, visible to users with the "administer site configuration" permission, in both the Status report page, as well as any admin/ path on their site, on the following page load and all subsequent page loads.

Is "administer site configuration" the right permission, it might be, but should be have a separate permission for this, so in the event a site administrator isn't clear who exactly will see these messages or wants to restrict it to those who can actually perform upgrades, a distinctive permission might address that.

It's reasonable to assume that a site might be setup in such a way where an administrator has the permission "administer site configuration", but that same administrator is not able to perform upgrades.

Consider the Managed Drupal Scenario: the provider/developer (who the client pays to manage the site) has every permission, but there exists a "limited administrator" role that the client has, which allows them to do things like change the site name, but not install modules or perform upgrades. This kind of messaging would not be relevant to the "limited administrator" because they can't do anything about it, it might just cause confusion and undue concern for them. After all, they are paying someone else to take care of upgrades for them so they don't have to worry about it.

tedbow’s picture

@AaronMcHale thanks for reviewing the issue

re

It's reasonable to assume that a site might be setup in such a way where an administrator has the permission "administer site configuration", but that same administrator is not able to perform upgrades.

and

This kind of messaging would not be relevant to the "limited administrator" because they can't do anything about it, it might just cause confusion and undue concern for them.

This is already an existing pattern in core.

For instance if the Update module is enabled these users will see notifications of available updates including security updates even though they can't actually perform the updates in the UI. It seems unreasonable that we would show these users available updates and not alert them to the fact that one of these updates is actually a highly critical, once in every couple years kind of update that is of utmost importance. It also seems unreasonable that we would show them current security updates and not warn them that highly critical update will be available soon via PSAs in the UI.

The fact is we don't know that they can't perform updates just that they can't do it through the Update module. Since Composer is the recommended way to update Drupal core and modules often the Update module won't be enabled or if it is enabled you still might not want to assign the "Administer software updates" to any users because you don't want any user to update via the UI. Updating Drupal core is also not supported through the Update module and will not work for any module that has Composer dependencies.

With the Update module enabled currently "administer site configuration" permission is enough to

  1. Be alerted that there security updates
  2. Click the link in the message you see "See the available updates page for more information."
  3. See a list of Updates including which are security updates
  4. Performing the actual update if you are not using the functionality in the Update module and always for Core(which Update module doesn't support) is not controlled by any permission in Drupal

If we assume the Update module will not always be enabled or even it is the "Administer software updates" permission does not control how many sites, and all sites for Drupal core itself, run updates then we have to pick a permission to control showing these alerts to. Because "administer site configuration" is a restricted permission and should not be given to people that are not trusted and have responsibility on the site I think this is a good permission to choose and don't see an alternative permission.

for instance they can already:

  • change the email address that password reset emails come from
  • disable cron
  • change the default file system downloads from private to public
  • In the JSON API module, change from only read-only access to read/write access
aaronmchale’s picture

Re @tedbow in #213: Thanks for that very detailed explanation Ted, I think that all makes sense and I agree with your rational. I think if (as you said) there is already an existing pattern/precedent here then "administer site configuration" sounds like the right permission for this issue to target.

I do wonder though if a follow-up issue should be opened to look more broadly at if there should be a separate permission for everything to do with updates, so not just these PSAs, but the available update reports and other areas. You highlighted an important point, we cannot know which users are actually able to perform updates; I worry that the "administer site configuration" permission could become quite broad in its scope, and I could reasonably envision a site that might want to separate the administration of configuration from everything to do with updates, so exploring that further in a follow-up issue I think would be valuable.

But, as I said, I think for this issue, I agree with everything you've said.

phenaproxima’s picture

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

I think all committer feedback so far has been addressed. (Mostly this consists of @tedbow replying to explain the reasoning behind certain things.) I think this is ready for a committer to look again, so restoring RTBC.

larowlan’s picture

Removing 'Needs release manager review' on the basis of #200

A handful of questions/comments in the PR, nothing blocking.

Updating issue credit

larowlan’s picture

I had hoped to review this in time to make the alpha deadline, but because of various work commitments I missed the window.

This looks good to go to me.

phenaproxima’s picture

Opened #3213461: [policy, no patch] When should service arguments be wrapped? to discuss the possible coding standards change suggested by @larowlan.

  • larowlan committed 699f02b on 9.2.x
    Issue #3041885 by tedbow, beautifulmind, dww, ayushmishra206,...
  • larowlan committed 79dd832 on 9.3.x
    Issue #3041885 by tedbow, beautifulmind, dww, ayushmishra206,...
larowlan’s picture

Version: 9.3.x-dev » 9.2.x-dev
Status: Reviewed & tested by the community » Fixed
Issue tags: +9.2.0 release highlights

I discussed this with catch and xjm around the release date of 9.2.0-alpha1.

Both agreed this was fine to go into 9.2 between alpha and beta because of the strategic importance.

I reviewed this at the time, but just missed the alpha-freeze cut-off.

Committed 79dd832 and pushed to 9.3.x. Thanks!

Backported to 9.2.x per the above.

For all those involved, great work!

Published the change record and unpostponed two issues.

tedbow’s picture

🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊
@larowlan thanks! And thanks everyone who worked on this!
🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊🎉😊

jibran’s picture

This broke the drush site install command for my local dev. I'm using the latest drush release with the latest HEAD and PHP 8. I had to disable the event to install the site.

diff --git a/core/modules/system/src/EventSubscriber/AdvisoriesConfigSubscriber.php b/core/modules/system/src/EventSubscriber/AdvisoriesConfigSubscriber.php
index 91e5c127d0..ee768aa239 100644
--- a/core/modules/system/src/EventSubscriber/AdvisoriesConfigSubscriber.php
+++ b/core/modules/system/src/EventSubscriber/AdvisoriesConfigSubscriber.php
@@ -54,7 +54,8 @@ public function onConfigSave(ConfigCrudEvent $event): void {
    * {@inheritdoc}
    */
   public static function getSubscribedEvents(): array {
-    $events[ConfigEvents::SAVE][] = ['onConfigSave'];
+    $events = [];
+//    $events[ConfigEvents::SAVE][] = ['onConfigSave'];
     return $events;
   }
 

Complete error for the reference https://pastebin.com/X6SZtp49

larowlan’s picture

what version of Guzzle are you running @jibran

benjifisher’s picture

I was curious about the report in #223, so I tried to reproduce it, using Lando to install PHP 8.

In my test, drush si standard works fine.

.lando.yml:

name: drupal-php8
recipe: drupal9
config:
  php: '8.0'
  webroot: .

Some versions:

benji@pop-os:~/Sites/php8$ lando php --version
PHP 8.0.3 (cli) (built: Apr 10 2021 13:18:16) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.3, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.3, Copyright (c), by Zend Technologies
benji@pop-os:~/Sites/php8$ git hist -1
* 8db131bd38 2021-05-17 | Issue #3214412 by bnjmnm, tedbow: Build + prettier not run after yarn dependency update 3210633 (HEAD -> 9.3.x, origin/HEAD, origin/9.3.x) [catch]
benji@pop-os:~/Sites/php8$ lando drush version
 Drush version : 10.5.0 
benji@pop-os:~/Sites/php8$ lando composer --version
Composer version 2.0.7 2020-11-13 17:31:06
benji@pop-os:~/Sites/php8$ lando composer show guzzlehttp/guzzle | grep version
versions : * 6.5.5

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

andypost’s picture

Please help review additions to help topics about added advisories #3204175: Add security advisories Help Topic