Version 4 supported

4.12.0 (unreleased)

Overview

Regression test and Security audit

This release has been comprehensively regression tested and passed to a third party for a security-focused audit.

While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release.

Features and enhancements

jQuery updated from 1.7 to 3.6

We've upgraded jQuery in the CMS from 1.7 to 3.6. Note that this won't affect your front-end at all unless you've been using back-end javascript in your front-end, which is heavily discouraged.

Why not wait to do this in CMS 5?

The latest patch for jQuery 1.7 was released in 2012. CMS 4 is going to remain in support for bug fixes for a year after CMS 5's release, and for security fixes for another year after that. Updating jQuery now means we can have greater confidence in the security of the CMS for the support lifetime of CMS 4. It also has the benefit of reducing the difference in maintenance surface between CMS 4 and CMS 5 - if there is some regression related to javascript in the CMS, we have one less variable to account for.

During the upgrade we've made the determination that the risk of breaking project-specific CMS customisations is quite low, and is far outweighed by the benefits of this upgrade.

Will this break my CMS customisations?

The short answer is that your CMS javascript customisations are unlikely to be affected unless your code is very old (e.g. you have a site upgraded from CMS 3 which has CMS javascript customisations from that time).

jQuery API changes

Very little has actually changed with the jQuery API itself over this time, for the most part any removed API was removed years ago. It's likely that most projects haven't been using old jQuery API unless they've inherited old code from CMS 3 customisations.

jQuery provides a list of deprecated API which also marks any API which has been removed. Most of these have recommendations for replacing them as well, so updating your code should be pretty straight forward.

While it is best practice to stop using any deprecated API, you should be able to just do a quick search through your back-end javascript and replace anything which the list marks as removed from jQuery - everything else was deprecated but is still present and working for the time being.

If you're using any jQuery extensions or plugins, it will pay to make sure these are up to date.

jQuery behaviour changes

jQuery has made some breaking behavioural changes over time. If you are experiencing problems after updating your usage of the jQuery API and any plugins you're using, you might want to check the jQuery upgrade guides to see if there is a solution to your problem there.

Entwine behavioural change

The only change to behaviour we've had to make to accommodate jQuery's breaking changes was with entwine's onadd and onmatch handlers. jQuery has made substantial changes to its internal functionality, especially in regards to how it adds elements to the DOM, so we had to change with it. Entwine used to change jQuery's internal functionality to fire events synchronously when adding elements to the DOM. It now uses a MutationObserver to detect elements being added to the DOM, which means the events are fired asynchronously.

This should only affect your project in the unlikely scenario that you are adding elements to the DOM, then immediately performing some action that relies on something having happened in the onadd or onmatch handler for those elements. Across all of the core and supported modules we only found one instance of this occurring. If you do have a scenario like this, you could defer the action like so:

Before:

$('.some-class').entwine({
  // Initialise something in the onmatch or onadd handler
  onmatch: function() {
    this.data('some-data', 'some-value');
  }
});
$('.some-other-class').entwine({
  // Add an instance of the above element into the DOM and then try to use the initialised data
  onclick: function() {
    this.parent(parentSelector).append('<div class="some-class"></div>');
    // Expects the data to have been set in onmatch instantly because the event used to be synchronous
    const initiliasedData = this.siblings('.some-class').data('some-data');
    console.log(initiliasedData); // this will almost certainly print "undefined" to the console, because the MutationObserver event hasn't fired yet
  }
});

After:

$('.some-class').entwine({
  // Add an entwine property to defer initialisation events
  DeferredInitilisationCallback: null,
  // Add an entwine property to indicate whether this element has been initialised
  IsInitialised: false,
  // Initialise something in the onmatch or onadd handler
  onmatch: function() {
    this.data('some-data', 'some-value');
    this.setIsInitialised(true);
    // If some action was deferred, perform that action now
    const deferredCallback = this.getDeferredInitilisationCallback();
    if (typeof deferredCallback === 'function') {
      deferredCallback();
      this.setDeferredInitilisationCallback(null);
    }
  }
});
// Add an instance of the above element into the DOM and then try to use the initialised data
$('.some-other-class').entwine({
  onclick: function() {
    this.parent(parentSelector).append('<div class="some-class"></div>');
    const newElement = this.siblings('.some-class');
    // Check if the element has been initialised before performing the action
    if (newElement.getIsInitialised()) {
      const initiliasedData = newElement.data('some-data');
      console.log(initiliasedData); // this will almost never be reached but you might get a situation where the event fires immediately, so it pays to be safe
    } else {
      // Defer the action until the element has been initialised
      newElement.setDeferredInitilisationCallback(function() {
        const initiliasedData = newElement.data('some-data');
        console.log(initiliasedData); // this will print "some-data" to the console, because it will be called after initialisation has finished
      });
    }
  }
});

Note that if you are not trying to use the initialised data/functionality immediately after adding the element to the DOM, this kind of change is not necessary (e.g. click event handlers which rely on the element the user is clicking having been initialised do not need to defer their actions like this, because the user is extremely unlikely to click the element in the time it takes for the MutationObserver to fire its events). It is very much exclusive to scenarios where code which adds or replaces elements in the DOM then immediately expects some initialisation to have occurred.

Search multiple searchable_fields by default

The general search text field which appears in the search interface now searches across all of your searchable fields by default. It will return a match if the search terms appear in any of the searchable fields.

If you have fields which you do not want to be searched with this general search (e.g. date fields which need special consideration), you can mark them as being explicitly excluded by setting general to false in the searchable fields configuration for that field:

use SilverStripe\ORM\DataObject;

class MyDataObject extends DataObject
{
    private static $searchable_fields = [
        'Name',
        'BirthDate' => [
            'general' => false
        ],
    ];
}

There is also additional configuration, such as which SearchFilter it uses (PartialMatchFilter by default), and whether it should split the search query into individual search terms or search as a phrase. Check out General search field for more information.

Samesite attribute on cookies

The samesite attribute is now set on all cookies. To avoid backward compatibility issues, the Lax value has been set by default, but we recommend reviewing the requirements of your project and setting an appropriate value.

The default value can be set for all cookies (except for the session cookie) in yaml configuration like so:

SilverStripe\Control\Cookie:
  default_samesite: 'Strict'

Check out the cookies documentation for more information.

The session cookie is handled separately. You can configure it like so:

SilverStripe\Control\Session:
  cookie_samesite: 'Strict'

Note that if you set the samesite attribute to None, the secure is automatically set to true as required by the specification.

For more information about the samesite attribute check out https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite

The canView permission required to access a submitted file in UserForm

A security enhancement to UserForm has been made to protect files uploaded using UserDefinedForm from unauthorised access by content authors who do not have access to the submitted files folder. If a content author with access to the UserForm page does not have sufficient can view rights on submitted files, they will see the message "Insufficient rights to view the file" and will not be able to access the file.

If you want to retain the previous behaviour and automatically grant content authors access to view submitted files, you can apply the following extension to your project.

app/src/extensions/MyFileExtension.php

<?php
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Member;

class MyFileExtension extends DataExtension
{
    public function canView(Member $member)
    {
        $submittedFileField = $this->getOwner()->SubmittedFileField;
        if ($submittedFileField && $submittedFileField->canView($member)) {
            return true;
        }
    }
}

Then just apply this extension through your config to File class:

app/_config/app.yml

SilverStripe\Assets\File:
  extensions:
    - MyFileExtension

New convenience methods for ModelAdmin

ModelAdmin has some new methods to make it easier to work with:

  • getCMSEditLinkForManagedDataObject() returns a link to the edit form for the object passed in, assuming it's managed by this ModelAdmin.
  • getLinkForModelClass() returns a link to the tab for a specific class in the ModelAdmin - useful as a base for links to specific actions.
  • getLinkForModelTab() similar to getLinkForModelClass(), except you pass in a specific model tab you want a link to. This is useful if your ModelAdmin has multiple tabs for managing a single class.
  • isManagedModel returns true if the class or model tab passed in is managed by this ModelAdmin. EditForm

New CMSEditLinkExtension

The silverstripe/admin module has a new CMSEditLinkExtension which is used to generate CMS edit links for DataObject records, including those in nested GridField setups.

This allows you to share DataObject edit links directly with others. This extension is also used to facilitate CMS preview of DataObject records

See managing records for a detailed explanation of this extension.

Assets API formalisation and deprecations

In the Silverstripe CMS 4.4 release, many aspects of the silverstripe/assets module were refactored to support permanent links.

Many new abstractions were created in the process. However, those new abstractions were marked as @internal and were not covered by Silverstripe CMS's semantic versioning commitment. Multiple file migration tasks were also added in that release.

With Silverstripe CMS 5 on the horizon, it is an opportune time to formalise some of those abstractions and to deprecate those migration tasks.

The following classes and methods are now officially part of the Silverstripe CMS API:

These new abstractions make several APIs redundant. The following methods are deprecated in the Silverstripe CMS 4.12 release and will be removed in Silverstripe CMS 5:

Silverstripe CMS 4 includes multiple tasks to migrate files to newer states. These tasks will no longer be needed in Silverstripe CMS 5. The following tasks and helpers have been deprecated in Silverstripe CMS 4 and will be removed in Silverstripe CMS 5:

Other new features

Bugfixes

This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!

Deprecation class(#deprecation-class)

The Deprecation class did not function correctly when used with modules that had a version less than 4.0.0, notably modules such as silverstripe/admin which has a major version of 1. The old "version filtering" functionality has been replaced with a simple call to Deprecation::enabled() that will show all deprecation notices, regardless of the version passed as the first parameter.