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: 4 additions & 3 deletions css/bootstrap.css

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions inc/bs_tabs.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ function bs_tab( $params, $content=null ) {
add_shortcode( 'bs_tab', 'bs_tab' );

function bs_dropdown( $params, $content=null ) {
global $bs_timestamp;
extract( shortcode_atts( array(
'title' => '',
'id' => '',
Expand Down Expand Up @@ -81,7 +80,7 @@ function bs_tcontent( $params, $content=null ) {
), $params ) );
$content = preg_replace( '/<br class="nc".\/>/', '', $content );
$class = ($class=='active')? 'active in': '';
$result = '<div class="tab-pane fade ' . esc_attr($class) . '" id=' . esc_attr($id) . '>';
$result = '<div class="tab-pane fade ' . esc_attr($class) . '" id="' . esc_attr($id) . '">';
$result .= do_shortcode( $content );
$result .= '</div>';
return force_balance_tags( $result );
Expand Down
7 changes: 3 additions & 4 deletions js/bootstrap.js

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions js/plugins/tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
var type = $('input[name=type]:checked').val();
var mparent = $('#sl_parent').val();
var content = $('#txt_content').val();
var id = 'bs_tab' + e.timeStamp;
var id = 'bs_tab_' + guid();

if (type == 'tab') {
if (mparent == '') {
Expand Down Expand Up @@ -276,7 +276,7 @@
<button class="tab-delete btn"><i class="icon-trash"></i> Remove</button>';


var id = 'bs_tab' + e.timeStamp;
var id = 'bs_tab_' + guid();

if (type != 1) {
if (mparent == '') {
Expand All @@ -293,6 +293,13 @@
$('#' + id).trigger('click')
}
});

function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + '-' + s4();
}
});
</script>
</head>
Expand Down
5 changes: 5 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Please report issues directly to our [Github repository](https://github.com/TheW

== Changelog ==

= 3.4.6 =
* Fix stored XSS via the bs_tcontent id attribute (unquoted attribute)
* Update bundled Bootstrap from 3.3.5 to 3.4.1 (fixes XSS in tooltip and collapse data attributes)
* Fix tab IDs generated by the editor containing dots

= 3.4.2 =
* Fix remaining XSS vulnerabilities (e.g. tooltip title escaping)

Expand Down
51 changes: 21 additions & 30 deletions specs/editor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,21 @@ test.describe('Classic Editor Integration', () => {

test('Should render Bootstrap Shortcodes buttons in the toolbar', async ({ admin, page, editor }) => {

// Dump HTML for debugging
const html = await page.content();
const fs = require('fs');
fs.writeFileSync('dom-debug.html', html);

// Switch to Visual mode if not already there, since WP remembers mode per user
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) {
await page.locator('#content-tmce').click();
}

// Dump HTML for debugging AFTER click
const html2 = await page.content();
fs.writeFileSync('dom-debug-after.html', html2);

// Wait for the TinyMCE editor to be ready
await page.waitForSelector('#wp-content-wrap.tmce-active');

// The Toolbar Toggle (kitchen sink) might be required to see the row of bootstrap buttons.
// It uses aria-label="Toolbar Toggle (Shift+Alt+Z)"
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
// It uses aria-label="Toolbar Toggle (Shift+Alt+Z)", renamed to "Extended Toolbar" in newer WordPress.
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();

if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) {
await toggleButton.click();
}
Expand All @@ -57,9 +48,9 @@ test.describe('Classic Editor Integration', () => {
await page.waitForSelector('#wp-content-wrap.tmce-active');

// Ensure kitchen sink is open
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) {
await toggleButton.click();
}
Expand Down Expand Up @@ -100,9 +91,9 @@ test.describe('Classic Editor Integration', () => {
await page.waitForSelector('#wp-content-wrap.tmce-active');

// Ensure kitchen sink is open
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) {
await toggleButton.click();
}
Expand Down Expand Up @@ -132,9 +123,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -158,9 +149,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -177,9 +168,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -198,9 +189,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -222,9 +213,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -244,9 +235,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand All @@ -267,9 +258,9 @@ test.describe('Classic Editor Integration', () => {
const htmlMode = await page.locator('#wp-content-wrap.html-active').count();
if (htmlMode > 0) { await page.locator('#content-tmce').click(); }
await page.waitForSelector('#wp-content-wrap.tmce-active');
const isKitchenSinkActive = await page.locator('.mce-active[aria-label^="Toolbar Toggle"]').count();
const isKitchenSinkActive = await page.locator('.mce-active:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"])').count();
if (isKitchenSinkActive === 0) {
const toggleButton = page.locator('div[aria-label^="Toolbar Toggle"] button').first();
const toggleButton = page.locator('div:is([aria-label^="Toolbar Toggle"], [aria-label^="Extended Toolbar"]) button').first();
if (await toggleButton.isVisible()) { await toggleButton.click(); }
}

Expand Down
14 changes: 13 additions & 1 deletion tests/phpunit/ShortcodeTabsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,19 @@ public function test_tcontents_wrapper() {
public function test_tcontent_item() {
$content = '[bs_tcontent id="home" class="active"]Content[/bs_tcontent]';
$output = do_shortcode($content);
$this->assertStringContainsString('<div class="tab-pane fade active in" id=home>', $output);
$this->assertStringContainsString('<div class="tab-pane fade active in" id="home">', $output);
$this->assertStringContainsString('Content', $output);
}

public function test_tcontent_id_cannot_inject_attributes() {
$content = '[bs_tcontent id="xss onmouseover=alert(1) style=padding:50px" class="active"]Content[/bs_tcontent]';
$output = do_shortcode($content);

$doc = new DOMDocument();
$doc->loadHTML($output, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$pane = $doc->getElementsByTagName('div')->item(0);
$this->assertFalse($pane->hasAttribute('onmouseover'));
$this->assertFalse($pane->hasAttribute('style'));
$this->assertSame('xss onmouseover=alert(1) style=padding:50px', $pane->getAttribute('id'));
}
}
Loading