diff --git a/.github/workflows/php_unit.yml b/.github/workflows/php_unit.yml
index 997c762..f168225 100644
--- a/.github/workflows/php_unit.yml
+++ b/.github/workflows/php_unit.yml
@@ -102,17 +102,28 @@ jobs:
cp phpcs.xml dist/phpcs.xml
cp phpunit.xml dist/phpunit.xml
cp -r tests dist/tests
+ cp -r utils dist/utils
- name: Install Dist Composer Dependencies
if: matrix.php != '8.5'
working-directory: dist
run: composer install --no-interaction --no-progress --no-suggest --optimize-autoloader
+ - name: Build Conflict Plugin
+ if: matrix.php != '8.5'
+ working-directory: dist
+ run: npm run build-conflict
+
- name: Start Dist Docker environment
if: matrix.php != '8.5'
working-directory: dist
run: npm run wp-env start
+ - name: Install Conflict Plugin
+ if: matrix.php != '8.5'
+ working-directory: dist
+ run: npm run install-conflict
+
- name: Log running Dist Docker containers
if: matrix.php != '8.5'
working-directory: dist
diff --git a/.wp-env.json b/.wp-env.json
index 80bbdbf..326887d 100644
--- a/.wp-env.json
+++ b/.wp-env.json
@@ -1,18 +1,19 @@
{
- "core": "WordPress/WordPress",
- "plugins": [ "." ],
- "config": {
+ "core": "WordPress/WordPress",
+ "plugins": [ "." ],
+ "config": {
"WP_ENVIRONMENT_TYPE": "production",
"WP_ENV": "production",
- "WP_DEBUG": true,
- "WP_DEBUG_LOG": true,
- "WP_DEBUG_DISPLAY": true
- },
- "env": {
- "tests": {
- "mappings": {
- "wp-content/plugins/rollbar": "./"
- }
- }
- }
+ "WP_DEBUG": true,
+ "WP_DEBUG_LOG": true,
+ "WP_DEBUG_DISPLAY": true
+ },
+ "env": {
+ "tests": {
+ "mappings": {
+ "wp-content/plugins/rollbar": "./",
+ "wp-content/plugins/conflict": "./tests/conflict"
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 1173788..26b9486 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,8 @@
},
"autoload-dev": {
"psr-4": {
- "Rollbar\\WordPress\\Tests\\": "tests/"
+ "Rollbar\\WordPress\\Tests\\": "tests/",
+ "Rollbar\\WordPress\\BuildUtils\\": "utils/"
}
},
"extra": {
diff --git a/package-lock.json b/package-lock.json
index 1ada56b..ac1ec24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -527,7 +527,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
"integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
diff --git a/package.json b/package.json
index a4f12ed..9989549 100644
--- a/package.json
+++ b/package.json
@@ -12,11 +12,12 @@
"test": "npm-run-all test:php test:phpcs",
"tests-composer-update": "WP_ENV_PHP_VERSION='8.1' npm run wp-env -- run --env-cwd='wp-content/plugins/rollbar' tests-cli composer update",
"tests-composer-install": "WP_ENV_PHP_VERSION='8.1' npm run wp-env -- run --env-cwd='wp-content/plugins/rollbar' tests-cli composer install --no-interaction",
- "pretest:php": "npm-run-all tests-composer-install",
"test:php": "npm run wp-env -- run tests-wordpress /var/www/html/wp-content/plugins/rollbar/vendor/bin/phpunit -c /var/www/html/wp-content/plugins/rollbar/phpunit.xml",
"test:phpcs": "npm run wp-env -- run tests-wordpress /var/www/html/wp-content/plugins/rollbar/vendor/bin/phpcs --standard=/var/www/html/wp-content/plugins/rollbar/phpcs.xml -p",
"start": "npm run wp-env start",
- "build": "cp -f node_modules/rollbar/dist/rollbar.snippet.js public/js/rollbar.snippet.js"
+ "build": "cp -f node_modules/rollbar/dist/rollbar.snippet.js public/js/rollbar.snippet.js",
+ "build-conflict": "cd tests/conflict && composer install --no-interaction --no-progress --no-suggest --optimize-autoloader",
+ "install-conflict": "npm run wp-env -- run tests-cli wp plugin activate conflict"
},
"repository": {
"type": "git",
diff --git a/phpcs.xml b/phpcs.xml
index b4537fa..575cd8d 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -13,6 +13,7 @@
./mu-plugin
./src
./tests
+ ./utils
./rollbar.php
diff --git a/scoper.inc.php b/scoper.inc.php
index d6e239e..09047a3 100644
--- a/scoper.inc.php
+++ b/scoper.inc.php
@@ -2,7 +2,10 @@
declare(strict_types=1);
+require_once './utils/NamespaceTokenFixer.php';
+
$finder = Isolated\Symfony\Component\Finder\Finder::class;
+$fixer = new Rollbar\WordPress\BuildUtils\NamespaceTokenFixer();
return [
'prefix' => 'RollbarWP',
@@ -46,14 +49,10 @@
'zend_monitor_custom_event',
],
'patchers' => [
- static function (string $filePath, string $prefix, string $content): string {
- // Fix ClassLoader in string not being prefixed.
- if ($filePath === __DIR__ . '/build/vendor/composer/autoload_real.php') {
- $content = str_replace(
- '(\'Composer\\\\Autoload\\\\ClassLoader\' === $class)',
- '(\'RollbarWP\Composer\Autoload\ClassLoader\' === $class)',
- $content,
- );
+ static function (string $filePath, string $prefix, string $content) use ($fixer): string {
+ if ($fixer->fileIsFixable($filePath)) {
+ $fixer->configure($filePath, $content);
+ $content = $fixer->fix();
}
return $content;
diff --git a/tests/conflict/composer.json b/tests/conflict/composer.json
new file mode 100644
index 0000000..3a1deb6
--- /dev/null
+++ b/tests/conflict/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "conflict/conflict",
+ "description": "This is a test plugin that has dependency conflicts.",
+ "minimum-stability": "stable",
+ "license": "MIT",
+ "require": {
+ "psr/log": "^1.0",
+ "monolog/monolog": "^1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Conflict\\": "src/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/conflict/composer.lock b/tests/conflict/composer.lock
new file mode 100644
index 0000000..892b75b
--- /dev/null
+++ b/tests/conflict/composer.lock
@@ -0,0 +1,155 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "7526d404b95e868fb43368e3813b71de",
+ "packages": [
+ {
+ "name": "monolog/monolog",
+ "version": "1.27.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "psr/log": "~1.0"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "graylog2/gelf-php": "~1.0",
+ "php-amqplib/php-amqplib": "~2.4",
+ "php-console/php-console": "^3.1.3",
+ "phpstan/phpstan": "^0.12.59",
+ "phpunit/phpunit": "~4.5",
+ "ruflin/elastica": ">=0.90 <3.0",
+ "sentry/sentry": "^0.13",
+ "swiftmailer/swiftmailer": "^5.3|^6.0"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "php-console/php-console": "Allow sending log messages to Google Chrome",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+ "sentry/sentry": "Allow sending log messages to a Sentry server"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "http://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/1.27.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-09T08:53:42+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/tests/conflict/conflict.php b/tests/conflict/conflict.php
new file mode 100644
index 0000000..784cda1
--- /dev/null
+++ b/tests/conflict/conflict.php
@@ -0,0 +1,24 @@
+pushHandler(new StreamHandler(WP_CONTENT_DIR . '/logs/conflict.log', Logger::WARNING));
+
+ add_action('init', self::onInit(...));
+ }
+
+ /**
+ * Logs when the plugin is initialized.
+ *
+ * @return void
+ */
+ public static function onInit(): void
+ {
+ self::$logger->warning('Conflict plugin initialized.');
+ }
+}
diff --git a/tests/utils/NamespaceTokenFixerTest.php b/tests/utils/NamespaceTokenFixerTest.php
new file mode 100644
index 0000000..84fa573
--- /dev/null
+++ b/tests/utils/NamespaceTokenFixerTest.php
@@ -0,0 +1,87 @@
+subject = new NamespaceTokenFixer();
+ }
+
+ public function testFixUnscopedNamespaceStringsNoOp(): void
+ {
+ $input = 'subject->configure('./build/vendor/composer/autoload_psr4.php', $input);
+
+ self::assertSame($input, $this->subject->fix());
+ }
+
+ public function testFixUnscopedNamespaceStringsUnprefixed(): void
+ {
+ // Because we have a string within a string, we need two levels of escaping. It is ugly, but it produces the
+ // equivalent of the following PHP code:
+ // subject->configure('./build/vendor/composer/autoload_psr4.php', $input);
+
+ self::assertSame('subject->fix());
+ }
+
+ public function testFixUnscopedNamespaceStringsPrefixed(): void
+ {
+ $input = 'subject->configure('./build/vendor/composer/autoload_psr4.php', $input);
+
+ self::assertSame('subject->fix());
+ }
+
+ public function testFixUnscopedNamespaceStringsInvalidFile(): void
+ {
+ $input = 'subject->configure('foo.php', $input);
+
+ self::assertSame('subject->fix());
+ }
+
+ public function testFixAutoloadStaticPrefixLengthsPsr4(): void
+ {
+ $input = ' array(\'Rollbar\\\\WordPress\\\\\' => 18, \'Rollbar\\\\\' => 8),
+ \'P\' => array(\'Psr\\\\Log\\\\\' => 8),
+ \'M\' => array(\'Monolog\\\\\' => 8),
+ );
+ $foo = 42;';
+
+ $expected = ' array(
+ \'Rollbar\\\\WordPress\\\\\' => 18,
+ \'Rollbar\\\\\' => 8,
+ \'RollbarWP\\\\Psr\\\\Log\\\\\' => 18,
+ \'RollbarWP\\\\Monolog\\\\\' => 18,
+ ),
+ );
+ $foo = 42;';
+
+ $this->subject->configure('./build/vendor/composer/autoload_static.php', $input);
+
+ self::assertSame(self::normalizeString($expected), self::normalizeString($this->subject->fix()));
+ }
+
+ /**
+ * Removes all whitespace from a string.
+ *
+ * @param string $string The string to normalize.
+ * @return string
+ */
+ private static function normalizeString(string $string): string
+ {
+ return str_replace([' ', "\n", "\r", "\t"], '', $string);
+ }
+}
diff --git a/utils/NamespaceTokenFixer.php b/utils/NamespaceTokenFixer.php
new file mode 100644
index 0000000..4a528e6
--- /dev/null
+++ b/utils/NamespaceTokenFixer.php
@@ -0,0 +1,213 @@
+tokens = PhpToken::tokenize($contents);
+ }
+
+ /**
+ * Configures the file to fix.
+ *
+ * @param string $filename The path and filename of the file to fix.
+ * @param string $contents The contents of the file to fix.
+ * @return void
+ */
+ public function configure(string $filename, string $contents): void
+ {
+ $this->filename = $filename;
+ $this->tokens = PhpToken::tokenize($contents);
+ }
+
+ /**
+ * Checks if the file is fixable.
+ *
+ * @param string $path The path to the file.
+ * @return bool
+ */
+ public function fileIsFixable(string $path): bool
+ {
+ foreach ($this->fixes() as $fixablePath => $fixes) {
+ if (str_ends_with($path, $fixablePath)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Fixes the file.
+ *
+ * @return string The fixed contents of the file.
+ */
+ public function fix(): string
+ {
+ foreach ($this->fixes() as $fixablePath => $fixFunctions) {
+ if (str_ends_with($this->filename, $fixablePath)) {
+ foreach ($fixFunctions as $fixFunction) {
+ $fixFunction();
+ }
+ }
+ }
+
+ return implode('', $this->tokens);
+ }
+
+ /**
+ * Returns an array of fixes for the file.
+ *
+ * @return array>
+ */
+ private function fixes(): array
+ {
+ return [
+ '/build/vendor/composer/autoload_static.php' => [
+ $this->fixAutoloadStaticPrefixLengthsPsr4(...),
+ $this->fixUnscopedNamespaceStrings(...),
+ ],
+ '/build/vendor/composer/autoload_real.php' => [
+ $this->fixUnscopedNamespaceStrings(...),
+ ],
+ '/build/vendor/composer/autoload_psr4.php' => [
+ $this->fixUnscopedNamespaceStrings(...),
+ ],
+ ];
+ }
+
+ /**
+ * Fixes the `$prefixLengthsPsr4` property in the `autoload_static.php` file.
+ *
+ * @return void
+ *
+ * @throws RuntimeException If the `$prefixLengthsPsr4` variable could not be found.
+ */
+ private function fixAutoloadStaticPrefixLengthsPsr4(): void
+ {
+ // Collect all tokens belonging to the `$prefixLengthsPsr4` variable expression.
+ $startIndex = -1;
+ $endIndex = -1;
+ $tokens = [];
+ foreach ($this->tokens as $i => $token) {
+ if (T_VARIABLE === $token->id && '$prefixLengthsPsr4' === (string)$token && -1 === $startIndex) {
+ $startIndex = $i;
+ continue;
+ }
+ if (-1 !== $startIndex && -1 === $endIndex) {
+ if (59 === $token->id) { // ";" character
+ $endIndex = $i;
+ continue;
+ }
+ $tokens[] = $token;
+ }
+ }
+
+ if (-1 === $endIndex) {
+ throw new RuntimeException('Could not find $prefixLengthsPsr4 variable in autoload_static.php');
+ }
+
+ // Extract all namespace strings from the tokens.
+ $namespaces = [];
+ foreach ($tokens as $token) {
+ //
+ if (T_CONSTANT_ENCAPSED_STRING !== $token->id) {
+ continue;
+ }
+ $string = (string)$token;
+ // The first level keys of the array are always single characters (such as 'R' for 'Rollbar\'). Skip them.
+ if (strlen($string) < 3) {
+ continue;
+ }
+ // Remove quotes from the token string.
+ $string = substr($token, 1, -1);
+ // Skip strings that don't contain a backslash.
+ if (!str_contains($string, '\\')) {
+ continue;
+ }
+ // Fix escaped backslashes.
+ $string = str_replace('\\\\', '\\', $string);
+
+ // Prepend "RollbarWP\\" to all namespace strings that are not already correctly prefixed.
+ if (!str_starts_with($string, 'Rollbar\\') && !str_starts_with($string, 'RollbarWP\\')) {
+ $string = 'RollbarWP\\' . $string;
+ }
+ $namespaces[] = $string;
+ }
+
+ $prefixLengthsPsr4 = [];
+
+ foreach ($namespaces as $namespace) {
+ $firstChar = substr($namespace, 0, 1);
+ $prefixLengthsPsr4[$firstChar][$namespace] = strlen($namespace);
+ }
+
+ $result = var_export($prefixLengthsPsr4, true);
+
+ $newTokens = PhpToken::tokenize('tokens,
+ $startIndex,
+ $endIndex - $startIndex + 1,
+ $newTokens,
+ );
+ }
+
+ /**
+ * Fixes unscoped namespace strings in the file.
+ *
+ * @return void
+ */
+ private function fixUnscopedNamespaceStrings(): void
+ {
+ foreach ($this->tokens as $i => $token) {
+ if (T_CONSTANT_ENCAPSED_STRING !== $token->id) {
+ continue;
+ }
+ // Cast the token to a string to get the token value.
+ $string = (string)$token;
+ // Remove quotes from the token string.
+ $string = substr($string, 1, -1);
+ if (!str_contains($string, '\\')) {
+ continue;
+ }
+ // Skip strings that are already prefixed.
+ if (str_starts_with($string, 'Rollbar\\') || str_starts_with($string, 'RollbarWP\\')) {
+ continue;
+ }
+ // Ensure the first character is uppercase.
+ $firstChar = substr($string, 0, 1);
+ if (!ctype_upper($firstChar)) {
+ continue;
+ }
+ $newString = '\'RollbarWP\\' . $string . '\'';
+ $this->tokens[$i] = new PhpToken(T_CONSTANT_ENCAPSED_STRING, $newString, $token->line, $token->pos);
+ }
+ }
+}