Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Keep development-only files out of dist installs (Packagist zips). CI and local module work must
# therefore install this module as a PATH repository with symlink: true, or a real checkout -
# a dist install contains no tests at all.
/tests export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.editorconfig export-ignore
221 changes: 221 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
name: CI

on:
push:
# The default branch is `main` (portfolio convention; renamed from `master` for 3.0). If it is
# ever renamed again, this list must follow, or pushes to the default branch stop running CI.
branches: [main]
pull_request:
workflow_dispatch:

jobs:
test:
name: SS${{ matrix.silverstripe }} / PHP ${{ matrix.php }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
include:
# One job per supported Silverstripe major, at the oldest and newest PHP that major is
# used with here. The module's composer.json is the source of truth for the supported
# range; this matrix must follow it. Silverstripe 4 is not supported and not tested.
- silverstripe: '5'
php: '8.1'
recipe: '^5'
testing: '^3'
- silverstripe: '5'
php: '8.3'
recipe: '^5'
testing: '^3'
- silverstripe: '6'
php: '8.3'
recipe: '^6'
testing: '^4'
- silverstripe: '6'
php: '8.4'
recipe: '^6'
testing: '^4'

services:
database:
# MariaDB rather than MySQL: it matches both the local dev machine and the
# Forge servers, so a version-specific failure here means something real.
image: mariadb:11.4
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: ss_ci
ports:
- 3306:3306
options: >-
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=10s --health-timeout=5s --health-retries=10

steps:
- name: Check out the module
uses: actions/checkout@v4
with:
path: module

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, intl, pdo_mysql, mysqli, gd, zip, dom, xml, tokenizer, simplexml, ctype, hash, session
tools: composer:v2
coverage: none

- name: Build a host project
# The module cannot be tested on its own: it needs a booted Silverstripe app.
# The module is required through a PATH repository pointing at the checkout, and
# symlinked rather than mirrored ON PURPOSE - `/tests` is `export-ignore` in
# .gitattributes, so a dist or mirrored install contains no tests at all.
# recipe-cms rather than bare framework: the module's stylesheet is registered on the
# CMS's LeftAndMain, and the suite checks that registration.
run: |
mkdir -p app-host
cd app-host
cat > composer.json <<JSON
{
"name": "restruct/ci-host",
"require": {
"silverstripe/recipe-cms": "${{ matrix.recipe }}",
"silverstripe/recipe-testing": "${{ matrix.testing }}",
"restruct/silverstripe-copybutton": "*"
},
"repositories": [
{ "type": "path", "url": "../module", "options": { "symlink": true } }
],
"autoload-dev": {
"psr-4": {
"Unisolutions\\\\Tests\\\\": "vendor/restruct/silverstripe-copybutton/tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": { "allow-plugins": true }
}
JSON
# Silverstripe 5 is in security-fixes-only until its April 2027 EOL, and every
# release of it is covered by advisory PKSA-pjvm-vnw2-n3m2 (range <6.2.1),
# which will not be patched there. Composer therefore refuses to install it
# under the default policy.
# Blocking is disabled HERE ONLY, in a throwaway CI host project.
composer config policy.advisories.block false
composer update --no-interaction --no-progress

- name: Configure the test suite
run: |
cd app-host
cat > phpunit.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<!-- Warning-to-error escalation is the RUNNER's behaviour and differs by PHPUnit
major: 9 (SS5) converts warnings by default, 11 (SS6) has no such attribute.
failOnWarning is pinned so a warning behaves identically on every leg. -->
<!-- failOnEmptyTestSuite: a suite that silently collects nothing (wrong path, autoload
mapping lost, tests export-ignored by a dist install) must turn the leg red, not
pass as "No tests executed". -->
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true"
failOnWarning="true" failOnEmptyTestSuite="true">
<testsuites>
<testsuite name="copybutton">
<directory>vendor/restruct/silverstripe-copybutton/tests</directory>
</testsuite>
<testsuite name="consumer">
<directory>app/tests</directory>
</testsuite>
</testsuites>
</phpunit>
XML

- name: Add a consumer-shape database test
# Guards the class of bug that shipped in quickaddnew 2.1.0: an unbuildable fixture in a
# module's tests/ dir fatals the temp-database build in every CONSUMING project, and the
# module's own suite structurally cannot catch it (its fixtures are loaded by PHPUnit, not
# registered in the class manifest, so no tables are ever built from them).
run: |
mkdir -p app-host/app/tests
cat > app-host/app/tests/ConsumerDbTest.php <<'PHP'
<?php

use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member;

class ConsumerDbTest extends SapphireTest
{
protected $usesDatabase = true;

public function testTempDatabaseBuilds()
{
$m = Member::create(['FirstName' => 'x']);
$m->write();
$this->assertGreaterThan(0, $m->ID);
}
}
PHP

- name: Run tests
working-directory: app-host
env:
SS_DATABASE_CLASS: MySQLDatabase
SS_DATABASE_SERVER: 127.0.0.1
SS_DATABASE_PORT: 3306
SS_DATABASE_USERNAME: root
SS_DATABASE_PASSWORD: root
SS_DATABASE_NAME: ss_ci
SS_ENVIRONMENT_TYPE: dev
# Every run flushes the manifest, and how differs per major:
# - SS5 (framework 5, PHPUnit 9): SS_PHPUNIT_FLUSH does not exist in framework 5, so it
# would be inert. The flush is a trailing `flush=1` argument, which only works AFTER an
# explicit test path - `--testsuite x flush=1` makes PHPUnit 9 open "flush=1" as a file
# and fail. So the SS5 legs run by path; phpunit.xml still supplies bootstrap and
# failOnWarning.
# - SS6 (framework 6, PHPUnit 11): a `flush=1` argument no longer works; framework 6 reads
# SS_PHPUNIT_FLUSH instead, which also combines with --testsuite.
# --fail-on-empty-test-suite on every call as well as in phpunit.xml: the SS5 legs run by
# path, and the flag holds even if phpunit.xml is ever not picked up. PHPUnit 9.6 accepts
# the flag although its --help does not list it (checked: exit 1 on an empty directory).
run: |
if [ "${{ matrix.silverstripe }}" = "5" ]; then
vendor/bin/phpunit --fail-on-empty-test-suite vendor/restruct/silverstripe-copybutton/tests flush=1
vendor/bin/phpunit --fail-on-empty-test-suite app/tests flush=1
else
SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit --fail-on-empty-test-suite --testsuite copybutton
SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit --fail-on-empty-test-suite --testsuite consumer
fi

- name: Boot the app and build the database
# The suite proves the component behaves; this proves the module does not break a real
# boot and flush. The CLI differs per major: sake db:build on 6, dev/build on 5.
working-directory: app-host
env:
SS_DATABASE_CLASS: MySQLDatabase
SS_DATABASE_SERVER: 127.0.0.1
SS_DATABASE_PORT: 3306
SS_DATABASE_USERNAME: root
SS_DATABASE_PASSWORD: root
SS_DATABASE_NAME: ss_ci
SS_ENVIRONMENT_TYPE: dev
run: |
if [ "${{ matrix.silverstripe }}" = "5" ]; then
vendor/bin/sake dev/build flush=1
else
vendor/bin/sake db:build --flush
fi

lint:
name: Lint PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The floor and the ceiling of the PHP versions the supported majors run on here.
php: ['8.1', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Syntax check every PHP file
run: find code tests -name '*.php' -print0 | xargs -0 -n1 php -l
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Changelog

## 3.0.0 (2026-09-25)

Silverstripe 5 and 6 on one line. See [UPGRADING.md](UPGRADING.md).

### Breaking

- **Silverstripe 4 is no longer supported**: `silverstripe/framework` `^5 || ^6` (was
`^4 || ^5 || ^6`), and PHP `^8.1`. Silverstripe 4 reached end of life in April 2025; projects on it
stay on `2.0.x`.
- In menu mode the "Copy" item is no longer offered to users for whom the record's `canCreate()` is
false (see Changed). Anything that relied on seeing it there, such as a UI test, will not find it.
- `silverstripe/vendor-plugin` `^2 || ^3` is now a direct requirement (the module exposes `css/`
through it). Recipe-core 5 and 6 already bring it in; a project that pinned vendor-plugin 1.x cannot
install 3.0.
- `composer.json` still `replace`s `dhensby/silverstripe-copybutton` (both packages ship the same
class, `Unisolutions\GridField\CopyButton`, so they can never be installed side by side). Since
upstream now publishes its own 3.0.0, a project that asks for `dhensby/silverstripe-copybutton:^3`
and gets this package instead gets this package's behaviour (see Changed), not upstream's.

### Fixed

- **#3: the `canCreate()`-denied path fatalled on Silverstripe 4 and 5.** 2.0.1 imported
`SilverStripe\Core\Validation\ValidationException`, which only framework 6 has, so a user without
create permission who triggered a copy got a class-not-found error instead of "No create
permissions". The exception class is now resolved for the running framework major
(`SilverStripe\ORM\ValidationException` on 5, `SilverStripe\Core\Validation\ValidationException`
on 6).
- A copy that comes back unwritten now throws a `RuntimeException` instead of calling
`user_error(..., E_USER_ERROR)`, which is deprecated as of PHP 8.4.
- A copy action whose request carries no `RecordID` is ignored. Before, it raised an
"Undefined array key" warning.

### Changed

- `CopyButton` extends `AbstractGridFieldComponent`, like the core row actions, so
`CopyButton::create()` works and the class can be replaced through the Injector. Up to 2.0.1
only `new CopyButton()` worked.
- Menu mode hides the "Copy" item from users who cannot create the record, the way the core
`GridFieldDeleteAction` hides itself; column mode already hid its button. Before, the item was
shown to everyone and refused only once clicked.
- The default branch is `main` (was `master`), and `dev-main` is the `3.x-dev` alias. The 2.0.x line
lives on branch `v2`.

### Added

- A behavioural test suite (`tests/`) and CI across Silverstripe 5 and 6
(`.github/workflows/ci.yml`).
- `funding` in `composer.json`.
- A `LICENSE` file. The licence is unchanged (BSD-3-Clause, as `composer.json` always declared); the
file names the original author and the contributors.
- README: requirements, compatibility table, ModelAdmin usage via `getGridFieldConfig()`, what a
copy does and does not do, configuration, and how to run the tests. The old example that passed
`'GridFieldEditButton'` as a short string to `addComponent()` is corrected: that string never
matched the namespaced class, so the button was appended at the end instead.

## 2.0.2 (2026-09-25)

Hotfix on the `v2` branch for projects that stay on `^2.0`: the fix for #3 (the permission-denied
path fatalled on Silverstripe 4 and 5), and the `LICENSE` file. Nothing else changes.

## 2.0.1

Declared Silverstripe 5 and 6 support (`^4 || ^5 || ^6`). The permission-denied path fatals on 4
and 5, see 3.0.0 "Fixed".

## 2.0.0

Silverstripe 4 line (`^4`; Silverstripe 5 and 6 were added in 2.0.1), forked from
`dhensby/silverstripe-copybutton`.
38 changes: 38 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
BSD 3-Clause License

Copyright (c) 2013-2016, Elvinas Liutkevicius / UniSolutions (unisolutions.eu)
Copyright (c) 2013-2026, the contributors to this module, including Andy Adiwidjaja,
Bart van Irsel, Daniel Hensby, Ivo Bathke, James Cocker, Jarkko Linnanvirta,
Shea Dawson, Thomas B. Nielsen, yusuf (shunan), helpfulrobot (Silverstripe)
and Michael van Schaik / Restruct.
See the git history for each contribution.
All rights reserved.

This licence is the one the module has always declared: composer.json says
BSD-3-Clause, and the original class docblock points at the Silverstripe BSD licence.
This file only writes it out; it does not relicense anyone's contribution.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Loading
Loading