Skip to content

Fresh-install audit (4.7.11): PHP warnings on every page load, dead migration code, and asset build gaps #2358

Description

@sanketio

Summary

Full review of develop @ b744727 against a clean WP 7.0.3 / PHP 8.3 / BuddyPress 14.5.2 install (DDEV).

Correction (see also findings 12 and 13 below). An earlier revision of this issue opened with "PHPCS is clean (13 violations across 129 files)". That was wrong twice over. The run had testVersion overridden to 7.4-, contradicting the 5.3- in phpcs.xml; as actually configured the repo reports 15 errors, not 13. And "clean" only describes one of the three rulesets this repo defines — bin/pre-commit gates on --standard=WordPress, which reports 24,879 violations. The linting posture is covered properly in finding 13.

Two PHP warnings fire on every page load of a default install. One of the CSS files we ship had silently drifted from its SCSS source. Security findings from the same review are being reported separately per SECURITY.md.

1. PHP warnings on a default install

1a. Every front-end page load

app/main/RTMediaUploadTerms.php:100

$general_upload_terms_error_message = apply_filters(
    'rtmedia_upload_terms_check_terms_message',
    $rtmedia->options['general_upload_terms_error_message']
);

Unguarded array access. general_upload_terms_error_message is not seeded by init_site_options(), so it only exists once an admin saves the Upload Terms settings. Every neighbouring line in the same method uses isset() / ! empty().

Reproduced on /, /members/, /activity/:
PHP Warning: Undefined array key "general_upload_terms_error_message"

1b. Every admin page load

app/importers/RTMediaMigration.php:229PHP Warning: Undefined variable $count

$count is first assigned at :171, inside if ( ! empty( $_SESSION['migration_user_album'] ) ). On an install with no legacy bp-media data none of the four accumulator branches run, so return $count; warns and returns null. Reached from add_migration_notice() on admin_init.

2. $_SESSION is used without session_start()

RTMediaMigration.php has 20+ $_SESSION reads/writes and the plugin calls session_start() nowhere. Values do not survive the request, so the migration's cross-request progress tracking cannot work — migrate_to_new_db() (:678) reads $_SESSION['migration_media'] expecting a previous request to have set it. This is also the root cause of 1b.

Worth noting readme.txt advertises WordPress.com VIP compatibility, and VIP prohibits PHP sessions.

3. grunt build does not build the non-minified CSS — and it had drifted

Gruntfile.js registers:

grunt.registerTask( 'build', [ 'sass:minify', 'shell:postcss', 'terser' ] );

sass:dist (which produces the expanded admin.css / rtmedia.css) never runs. Running it revealed the committed app/assets/css/rtmedia.css was missing an entire block of button reset and :focus-visible styles that exist in _rtm.scss — a 115-line diff.

Those expanded files ship whenever rtm_get_script_style_suffix() returns '' (RTMedia.php:1316-1319, RTMediaAdmin.php:858), so anyone running SCRIPT_DEBUG has been served stale, less accessible CSS.

Fix: [ 'sass', 'shell:postcss', 'terser' ].

4. Two SCSS sources are never compiled by any task

app/assets/admin/css/sass/widget.scss and sass/rtm-upload-terms.scss exist, and their .css outputs are enqueued (RTMediaAdmin.php:862, RTMediaUploadTerms.php:103) — but neither appears in the Gruntfile sass config. Editing either SCSS file currently has no effect on the shipped CSS.

Related: shell:postcss globs app/assets/**/css/*.css --replace, sweeping in already-minified and already-prefixed files. That is why widget.min.css and rtm-upload-terms.min.css show as modified after a build that never compiled them.

5. rtmedia_bp is not localized under SCRIPT_DEBUG

app/main/RTMedia.php:1341-1358wp_localize_script( 'rtmedia-main', 'rtmedia_bp', … ) sits only in the else (minified) branch. rtMedia.js:7 copies every rtmedia_bp key onto window, so with SCRIPT_DEBUG on, bp_template_pack is never set and template-pack-conditional JS diverges from production. Move the call outside the branch.

6. BuddyPress-derived settings are frozen at first activation

RTMedia.php:275 calls init_site_options() only when rtmedia-options is absent; that calls init_buddypress_options() (:744), which derives buddypress_enableOnActivity / enableOnGroup from bp_is_active().

Hit live during this review: activating rtMedia before BuddyPress had run its installer left both permanently false. Activity uploads are silently disabled with no indication why, and re-activating rtMedia does not recover — rtmedia-options already exists.

Suggest re-deriving these on bp_init when no explicit user preference is stored, or surfacing an admin notice when BP components are active but the rtMedia toggles are off.

7. GoDAM integration

Cross-checked against the GoDAM 2.1.1 release from wp.org (downloads.wordpress.org/plugin/godam.zip). GoDAM is also an rtCamp plugin, so these are coordinatable rather than third-party risks.

7a. Dead enqueue — godam-player-frontend-style does not exist

templates/media/godam-integration.php:27

wp_enqueue_style( 'godam-player-frontend-style' );

That handle appears nowhere in GoDAM 2.1.1. The three siblings around it are all real, registered in inc/classes/shortcodes/class-godam-player.php:137-190:

rtMedia enqueues Registered by GoDAM
godam-player-frontend-script yes (:137)
godam-player-analytics-script yes (:148, guarded by file_exists)
godam-player-frontend-style no — handle does not exist
godam-player-style yes (:157)
godam-player-{minimal,pills,bubble,classic}-skin yes (:164-190)

wp_enqueue_style() on an unregistered handle is a silent no-op, so this has never surfaced as an error. Either drop the line or correct it to whatever style was intended.

7b. The "multisite safe" comment is wrong — but the code is right

:33 is annotated "Skin detection — multisite safe (uses site option)" and the file header repeats the claim, while the call is get_option( 'rtgodam-settings', array() ).

The code is correct and must not be changed. GoDAM reads and writes this option with get_option() / update_option() throughout — inc/classes/rest-api/class-settings.php:348 is the writer, and all 12 read sites use get_option. Switching rtMedia to get_site_option() would read a key GoDAM never writes and silently break skin detection on every install.

Fix is to delete the two misleading comments, not to touch the call.

7c. Function naming

Line Function Status
23 godam_enqueue_player_assets() no collision in GoDAM 2.1.1, but see below
202 handle_get_single_activity_comment_html() entirely unprefixed, very generic, registered as wp_ajax_get_single_activity_comment_html
258 rtmedia_user_can_view_activity() fine
352 enqueue_rtmedia_magnific_popup_script() prefix buried mid-name

GoDAM's primary prefix is rtgodam_ (RTGODAM_URL, rtgodam_cache_delete(), …), so godam_enqueue_player_assets() does not collide today. It is not a free namespace though — GoDAM declares seven bare godam_ globals of its own (godam_embed_page_content(), godam_preview_page_content(), godam_is_audio_file(), godam_get_transcript_path(), godam_is_supported_document(), godam_is_audio_file_by_name(), godam_should_load_auth_detector_script()), none of them function_exists-guarded, and neither is rtMedia's. Two rtCamp plugins growing the same unguarded global namespace is a fatal-redeclare waiting to happen.

handle_get_single_activity_comment_html() is the one worth fixing regardless of GoDAM — it is a completely generic global name owned by no prefix at all. The handler itself is correctly secured (nonce + login + permission check); this is purely a naming risk.

7d. Verified correct, no action needed

is_plugin_active( 'godam/godam.php' ) in index.php:74 matches GoDAM's actual folder/file layout.
Enqueue ordering is sound: GoDAM registers on wp_enqueue_scripts at priority 10 (class-godam-player.php:27), rtMedia enqueues at 20.
The skin mapping matches GoDAM's own vocabulary exactly — Default, Minimal, Pills, Bubble, Classic — and correctly no-ops on Default.
godam-ajax-nonce is rtMedia's own nonce action, not a GoDAM one.

7e. Open question — should video-preview be excluded too?

:52 skips loading the integration script on GoDAM's embed page:

if ( 'video-embed' !== get_query_var( 'godam_page' ) ) {

GoDAM serves two bare-player contexts off that query var: video-embed (inc/classes/class-video-embed.php:54) and video-preview (inc/classes/class-video-preview.php:52). Only the first is excluded. Worth confirming with whoever wrote 9aa1f82 whether video-preview was deliberately left in.

7f. Unrelated to GoDAM: $activities_template stub

:231-233 replaces the global $activities_template with a bare stdClass carrying only ->activity, then renders bp_get_template_part( 'activity/entry' ) against it. The restore at :239 only puts ->activity back, and only when the original was truthy. Contained because the request ends in wp_send_json_success(), but fragile against BP template changes.

Note for scheduling: GoDAM already ships first-class rtMedia support in the other direction (admin/godam-transcoder-actions.php:240 hooks rtmedia_after_update_media, and calls rtmedia_type() / rtmedia_media_id()), so 7a/7b/7c are worth coordinating with the GoDAM team rather than fixing unilaterally.

8. Text domain inconsistency

RTMediaUploadTerms::load_translation() (:88) loads domain 'rtmedia'. All 1,016 translatable strings use 'buddypress-media' (5 use 'rtmedia'), so that load_plugin_textdomain() call is a no-op.

Separately, the template file is misnamed: languages/buddpress-media.pot (missing the "u").

9. Admin includes loaded on every front-end request

index.php:66-75 pulls in wp-admin/includes/plugin.php and calls is_plugin_active( 'godam/godam.php' ) at plugin-load time to define RTMEDIA_GODAM_ACTIVE. That loads an admin-only file on every public page view.

Correction to an earlier revision of this issue: it previously claimed is_plugin_active() ignores network activation, and proposed replacing it with a bare in_array( …, get_option( 'active_plugins' ) ). That was wrong on both counts. WP core (wp-admin/includes/plugin.php:539-541) is:

function is_plugin_active( $plugin ) {
    return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true ) || is_plugin_active_for_network( $plugin );
}

The network check is already there, and the proposed one-liner would have dropped multisite support for RTMEDIA_GODAM_ACTIVE. Do not apply it.

What remains is narrower: an admin-only file is loaded on every front-end request, and the constant is resolved at plugin-load time rather than lazily. If this is worth changing at all, the options are to defer the definition to plugins_loaded, or to inline both halves of core's check (active_plugins plus active_sitewide_plugins) — not to drop one of them. Low priority; correctness is fine as-is.

10. No automated test coverage

tests/ contains only bootstrap.php and rt-wp-test-case.php — zero test-*.php files, so phpunit.xml matches nothing.

.github/workflows/playwright.yml is gated on if: false and points at ./tests/wp-e2e-playwright, which no longer exists in the tree (removed in f9c4841). .github/ci/main.sh still references it too.

There is currently no automated verification of this plugin.

11. Stale version metadata and toolchain drift

No Requires PHP header in index.php or readme.txt.
phpcs.xml sets testVersion 5.3-; the plugin runs on PHP 8.3+ in practice.
Requires at least: 4.1, Tested up to: 6.9 (current WP is 7.0.x).
.nvmrc pins v20.18.1; CI sets NODE_VERSION: 22.
grunt-checktextdomain and grunt-wp-i18n are devDependencies with no Gruntfile config.
.bowerrc and app/assets/css/sass/.bowerrc are Bower leftovers.
No composer.json, so PHPCS cannot be run locally without a manual WPCS install.

Sass darken() deprecations are already tracked in #2119 and not repeated here.

12. JavaScript is never linted — 1,283 JSHint errors

bin/.jshintrc exists (symlinked to .jshintrc at the repo root) and bin/pre-commit runs JSHint against staged .js files. But jshint is not a devDependency, no npm script invokes it, and no CI job runs it. It has evidently never been run.

Running it against the six JS sources with the repo's own config:

jshint --config bin/.jshintrc \
  app/assets/js/rtMedia.js app/assets/js/godam-integration.js \
  app/assets/js/godam-ajax-refresh.js app/assets/admin/js/scripts.js \
  app/assets/admin/js/settings.js app/assets/admin/js/rtmedia-admin.js
→ 1283 errors

Not all cosmetic. A sample from app/assets/admin/js/rtmedia-admin.js:

line 83, col 21, 'ajaxurl' is not defined.
line 95, col 21, 'ajaxurl' is not defined.
line 105, col 16, 'ajaxurl' is not defined.
line 12, col 13, 'console' is not defined.

ajaxurl is a legitimate WP admin global and console is a browser global; both are simply missing from the globals block in bin/.jshintrc, which currently declares only jQuery and wp. So part of the count is config debt rather than code defects — but nobody can tell which part until someone triages it, and that is the point.

Decide whether JS linting is a gate or not. If yes, add the tooling and burn down the list. If no, delete bin/.jshintrc and the JSHint block in bin/pre-commit so it stops implying a check that does not happen.

13. Three PHPCS rulesets that disagree, and one that cannot pass

The repo defines three different PHPCS configurations, and no two agree:

Gate Standard Full-repo result
phpcs.xml WordPress-Core (−Files.FileName, −PreparedSQL.*) + Extra + Docs + PHPCompatibility 5.3- 15 errors
bin/pre-commit --standard=WordPress (plain) 24,879 violations
.github/workflows/phpcs_on_pull_request.yml WordPress,WordPress-Core,WordPress-Docs, excludes WordPress.Files.FileName, skips tests,.github,lib,node_modules,vendor diff-only, n/a

bin/pre-commit cannot pass in its current state, so it is dead in practice — consistent with there being no composer.json to install phpcs from (finding 11).

Two real violations of the repo's own phpcs.xml, missed until the config was honoured rather than overridden:

templates/media/godam-integration.php:31   null coalescing operator (??) is not present in PHP version 5.6
templates/media/godam-integration.php:228  null coalescing operator (??) is not present in PHP version 5.6

Either testVersion is stale and should be raised to match a real support floor, or ?? should not be in the codebase. Both cannot be true. This is the same question as the missing Requires PHP header in finding 11 — resolve them together.

13a. The clean security-sniff result is self-declared, not tool-verified

phpcs.xml globally disables WordPress.DB.PreparedSQL.InterpolatedNotPrepared and WordPress.DB.PreparedSQL.NotPrepared. Re-enabling them via --standard=WordPress produces zero Security, PreparedSQL, EscapeOutput or NonceVerification hits — but only because the source carries 409 inline suppressions of precisely those sniffs:

358  WordPress.DB.*   (96 DirectQuery, 92 NoCaching, 85 InterpolatedNotPrepared,
                        81 NotPrepared, 3 SchemaChange, 1 UnfinishedPrepare)
 45  WordPress.Security.NonceVerification.*
  6  WordPress.Security.EscapeOutput.OutputNotEscaped

Every one of those is an unreviewed assertion by whoever added it. No automated check currently backs the claim that this codebase is free of SQL injection or missing-nonce problems; the only thing supporting it is manual reading, which is exactly the weaker form of evidence this issue has already been burned by twice.

Suggested: audit the 409 suppressions in batches, and drop the two global PreparedSQL excludes from phpcs.xml so future code has to justify itself inline rather than inheriting a blanket exemption.

14. Dependency vulnerabilities in the dev toolchain

npm audit reports 6 vulnerabilities (5 high, 1 moderate), all with fixes available via npm audit fix:

Package Severity Route
js-yaml high transitive via grunt
nanoid high transitive
postcss moderate direct dependency in package.json

Dev-only, so not a runtime risk to sites, but postcss is declared directly and is trivially bumpable. Related to #2346 and #2240, which track the Dependabot alerts.

Proposed order of work

Grouped by what unblocks them, not by size. The split below is deliberate: items marked observed were reproduced on a running install, items marked inferred were read out of the code and have a worse track record in this issue (see the two corrections above, both of which were confident one-liners that would have broken something).

Observed — safe to fix directly (PR #2359, open)

  • 1a + 1b — two guard clauses, both firing on every page load
  • 7a + 7b — drop the dead godam-player-frontend-style enqueue, delete the incorrect "multisite safe" comments (leave get_option alone)
  • 8 (domain) — load_plugin_textdomain( 'rtmedia' )'buddypress-media'

Inferred — validate before designing the fix

  • 5 — confirm actual user-visible impact of the missing rtmedia_bp localize under SCRIPT_DEBUG; may be benign
  • 3 — one-word Gruntfile change, but it drags in Sass-version churn (fractional rgb()); sequence against Prepare for Dart Sass 3.0.0 – Resolve Deprecations #2119
  • 4 — diff compiled-vs-committed widget.css / rtm-upload-terms.css before adding them to the sass config, in case they were hand-edited while uncompiled
  • 7e — confirm whether video-preview should be excluded alongside video-embed
  • 7f — does the $activities_template stub break anything in BP Nouveau today?
  • 9 — decide whether it is worth touching at all now that the correctness claim is withdrawn

Blocked on a decision, not on testing

  • 2 — is the legacy bp-media migration path still supported? Dead → delete ~1,000 lines. Live → rewrite $_SESSION on transients/user meta. Nobody can act without this answer
  • 6 — back-compat risk: re-deriving on bp_init would flip settings on sites that deliberately disabled activity uploads. Needs an owner
  • 11 + 13 — Tested up to: 7.0 cannot honestly be written until someone tests on WP 7.0; same question as testVersion 5.3- vs the ?? usage and the missing Requires PHP header. Resolve as one
  • 12 — decide whether JS linting is a gate. If yes, add the tooling and burn down 1,283 errors. If no, delete bin/.jshintrc and the JSHint block in bin/pre-commit
  • 13 — reconcile the three PHPCS rulesets; bin/pre-commit cannot currently pass
  • 13a — audit the 409 inline security-sniff suppressions, then drop the two global PreparedSQL excludes from phpcs.xml
  • 10 — scope call on restoring a test harness
  • 7c + 8 (.pot) — rename handle_get_single_activity_comment_html(), agree a function_exists convention with the GoDAM team, rename buddpress-media.pot
  • 14 — npm audit fix for the 6 dev-toolchain vulnerabilities

How to test

Durable steps for verifying the items above. Environment used throughout: WP 7.0.3, PHP 8.3.30, BuddyPress 14.5.2, MariaDB 11.8, DDEV v1.25.2.

1a / 1b — the two page-load warnings

# On develop, both counters climb; on the fix branch they stay flat.
ddev logs | grep -cE 'general_upload_terms_error_message|Undefined variable \$count'
curl -sk -o /dev/null https://<ddev-host>/
curl -sk -o /dev/null https://<ddev-host>/members/
ddev logs | grep -cE 'general_upload_terms_error_message|Undefined variable \$count'

1b needs an authenticated admin request (it fires from add_migration_notice() on admin_init). Requires WP_DEBUG on and a fresh install with no legacy bp-media-key postmeta.

7a — the dead GoDAM handle

curl -sL -o godam.zip https://downloads.wordpress.org/plugin/godam.zip && unzip -q godam.zip
grep -rn 'godam-player-frontend-style' godam/   # no output — handle does not exist
grep -rn "'godam-player-style'" godam/          # registered, class-godam-player.php:157

7b — do not "fix" the get_option call

grep -rn "rtgodam-settings" godam/ | grep -c get_site_option   # 0
grep -rn "update_option( 'rtgodam-settings'" godam/            # class-settings.php:348

6 — activation-order bug

On a clean DB: activate rtMedia before BuddyPress has run its installer, then check wp option get rtmedia-options --format=json. buddypress_enableOnActivity will be false permanently, and re-activating rtMedia does not recover it because rtmedia-options already exists.

PHPCS

No composer.json ships with the plugin, so install the standards out-of-tree:

composer config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev wp-coding-standards/wpcs:^3.1 phpcompatibility/phpcompatibility-wp:^2.1 dealerdirect/phpcodesniffer-composer-installer

Run each gate as it is actually configured — do not override testVersion, which is what hid the two ?? errors in finding 13:

# phpcs.xml as configured        → 15 errors on develop @ b7447272
vendor/bin/phpcs --standard=phpcs.xml .

# bin/pre-commit gate            → 24,879 violations
vendor/bin/phpcs --standard=WordPress --ignore='*/tests/*,*/lib/*,*/node_modules/*,*/vendor/*,*/bin/*' .

# CI ruleset, per changed file   → what phpcs_on_pull_request.yml reviews
vendor/bin/phpcs --standard=WordPress,WordPress-Core,WordPress-Docs --exclude=WordPress.Files.FileName <file>

templates/media/godam-integration.php accounts for 6 of the 15 and is expected to stay dirty until 7c and 13 land.

Other gates

jshint --config bin/.jshintrc app/assets/js/*.js app/assets/admin/js/*.js   # 1283 errors (finding 12)
for f in $(find app templates index.php -name '*.php'); do php -l "$f"; done  # all pass
npm audit                                                                     # 6 vulns (finding 14)

PHPUnit has nothing to run — phpunit.xml matches tests/test-*.php and no such file exists (finding 10).

Environment

WordPress 7.0.3, PHP 8.3.30, MariaDB 11.8, BuddyPress 14.5.2, DDEV v1.25.2
rtMedia 4.7.11, develop @ b744727
PHPCS run with WPCS 3.4.1 + PHPCompatibility, using each gate's own configuration (no testVersion override). JSHint 2.x, Node 22, npm 11.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions