diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..0d603085
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,90 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ quality:
+ name: Quality
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.1"
+ tools: composer:v2
+
+ - name: Validate Composer files
+ working-directory: src
+ run: composer validate --no-check-publish
+
+ - name: Lint PHP
+ run: git ls-files -z '*.php' | xargs -0 -n1 php -l
+
+ - name: Validate Docker Compose configuration
+ run: docker compose config --quiet
+
+ php-syntax:
+ name: PHP ${{ matrix.php }} syntax
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ["7.4", "8.1"]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+
+ - name: Lint PHP
+ run: git ls-files -z '*.php' | xargs -0 -n1 php -l
+
+ wordpress-tests:
+ name: WordPress ${{ matrix.wordpress }} tests
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ wordpress: ["6.7", "latest"]
+ services:
+ mariadb:
+ image: mariadb:10.11
+ env:
+ MYSQL_DATABASE: wordpress_test
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="healthcheck.sh --connect --innodb_initialized"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.1"
+ extensions: mysqli
+ tools: composer:v2, phpunit:9.6
+
+ - name: Install Composer dependencies
+ working-directory: src
+ run: composer install --no-interaction --no-progress --prefer-dist
+
+ - name: Install WordPress test library
+ working-directory: src
+ run: bash tests/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wordpress }} true
+
+ - name: Run tests
+ working-directory: src
+ run: phpunit
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index bf365074..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-dist: trusty
-language: php
-
-php:
- - 5.6
- - 7.1
-
-env:
- - WP_VERSION=latest WP_MULTISITE=0
- - WP_VERSION=latest WP_MULTISITE=1
- - WP_VERSION=4.0 WP_MULTISITE=0
- - WP_VERSION=4.0 WP_MULTISITE=1
-
-before_script:
- - composer self-update
- - make deps
- - chmod +x src/tests/init.sh && bash src/tests/init.sh $TRAVIS_PHP_VERSION $WP_VERSION
-
-script: cd src && phpunit
diff --git a/src/assets/css/simply-rets-client.css b/src/assets/css/simply-rets-client.css
index fae3731d..4bdc54b2 100644
--- a/src/assets/css/simply-rets-client.css
+++ b/src/assets/css/simply-rets-client.css
@@ -36,6 +36,24 @@ sizes with this class.
```
*/
+/*
+Accessibility: Text meant only for screen readers.
+Matches the standard WordPress screen-reader-text class.
+*/
+.screen-reader-text {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ -webkit-clip-path: inset(50%);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ word-wrap: normal !important;
+}
+
.sr-listing {
position: relative;
display: block;
@@ -187,6 +205,8 @@ div#sr-contact-form-error {
background-color: #eee;
}
+.sr-primary-data h2,
+.sr-primary-data h3,
.sr-primary-data h4 {
padding-left: 15px;
margin-top: 10px !important;
@@ -615,6 +635,7 @@ Most of these elements also have id's for you to use specifically.
clear: both;
}
+#sr-search-wrapper h2,
#sr-search-wrapper h3 {
margin-top: 0px;
}
@@ -938,6 +959,8 @@ elsewhere, this should be a breeze.
clear: both;
}
+ .sr-primary-data h2,
+ .sr-primary-data h3,
.sr-primary-data h4 {
padding-right: 15px;
}
diff --git a/src/assets/js/simply-rets-client.js b/src/assets/js/simply-rets-client.js
index 615f1c92..15780b04 100644
--- a/src/assets/js/simply-rets-client.js
+++ b/src/assets/js/simply-rets-client.js
@@ -133,6 +133,18 @@ var normalizeListingPhotoUrl = function (url) {
}
}
+var escapeHtml = function (value) {
+ return String(value).replace(/[&<>"']/g, function (character) {
+ return {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ }[character];
+ });
+}
+
var genMarkerPopup = function (
listing,
@@ -155,7 +167,7 @@ var genMarkerPopup = function (
var sqft = listing.property.area || "n/a";
var mlnum = listing.listingId || "n/a";
var price = listing.listPrice || "Unknown";
- var addr = listing.address.full || "Unknown";
+ var addr = escapeHtml(listing.address.full || "Unknown");
var photo = listing.photos.length > 1
? normalizeListingPhotoUrl(listing.photos[0])
: 'https://s3-us-west-2.amazonaws.com/simplyrets/trial/properties/defprop.jpg';
@@ -194,7 +206,7 @@ var genMarkerPopup = function (
' ' +
'
' +
' ' +
'';
diff --git a/src/simply-rets-maps.php b/src/simply-rets-maps.php
index 6cd23f18..601da531 100644
--- a/src/simply-rets-maps.php
+++ b/src/simply-rets-maps.php
@@ -164,8 +164,9 @@ public static function infoWindowMarkup(
diff --git a/src/simply-rets-openhouses.php b/src/simply-rets-openhouses.php
index 53430299..f6f158df 100644
--- a/src/simply-rets-openhouses.php
+++ b/src/simply-rets-openhouses.php
@@ -105,6 +105,18 @@ public static function openHousesSearchResults($search_response, $settings) {
return SrMessages::noResultsMsg($res);
} else {
+ $results_heading_raw = apply_filters('simplyrets_results_heading_tag', 'h2');
+ $results_heading = tag_escape($results_heading_raw);
+ if (empty($results_heading)) {
+ $results_heading = 'h2';
+ }
+ $show_results_heading = apply_filters('simplyrets_show_results_heading', false);
+ $results_heading_text = apply_filters('simplyrets_openhouses_heading_text', 'Open Houses');
+
+ if ($show_results_heading && !empty($res)) {
+ $markup .= "<{$results_heading} class='sr-listings-heading screen-reader-text'>" . esc_html($results_heading_text) . "{$results_heading}>";
+ }
+
// Generate markup for each open house result
foreach ($res as $idx => $oh) {
$markup .= SimplyRetsOpenHouses::openHouseSearchResultMarkup(
@@ -191,6 +203,12 @@ public static function openHouseSearchResultMarkup($openhouse, $settings) {
$sqft = !empty($living_area) ? "SqFt: {$living_area} sqft
" : "";
+ $listing_heading_raw = apply_filters('simplyrets_listing_heading_tag', 'h4');
+ $listing_heading = tag_escape($listing_heading_raw);
+ if (empty($listing_heading)) {
+ $listing_heading = 'h4';
+ }
+
ob_start();
?>
@@ -207,12 +225,12 @@ class="sr-photo"
@@ -231,7 +249,7 @@ class="sr-photo"
clearfix
- More details
+ More details
diff --git a/src/simply-rets-renderer.php b/src/simply-rets-renderer.php
index 31443f98..5689cc60 100644
--- a/src/simply-rets-renderer.php
+++ b/src/simply-rets-renderer.php
@@ -1052,6 +1052,20 @@ public static function srResidentialResultsGenerator($request_response, $setting
$map->setAutoZoom(true);
}
+ $listing_heading_raw = apply_filters('simplyrets_listing_heading_tag', 'h4');
+ $listing_heading = tag_escape($listing_heading_raw);
+ if (empty($listing_heading)) {
+ $listing_heading = 'h4';
+ }
+
+ $results_heading_raw = apply_filters('simplyrets_results_heading_tag', 'h2');
+ $results_heading = tag_escape($results_heading_raw);
+ if (empty($results_heading)) {
+ $results_heading = 'h2';
+ }
+ $show_results_heading = apply_filters('simplyrets_show_results_heading', false);
+ $results_heading_text = apply_filters('simplyrets_results_heading_text', 'Listings');
+
$resultsMarkup = "";
foreach ($response as $listing) {
$listing_uid = $listing->mlsId;
@@ -1258,11 +1272,11 @@ public static function srResidentialResultsGenerator($request_response, $setting
. ' '
. '
'
. '
'
@@ -1280,7 +1294,7 @@ public static function srResidentialResultsGenerator($request_response, $setting
. '
'
. '
'
. '
'
- . ' More details'
+ . ' More details ' . esc_html('for ' . $full_address) . ''
. ' '
. '
'
. $compliance_markup
@@ -1304,11 +1318,11 @@ public static function srResidentialResultsGenerator($request_response, $setting
. ' '
. '
'
. '
'
@@ -1326,7 +1340,7 @@ public static function srResidentialResultsGenerator($request_response, $setting
. '
'
. '
'
. '
'
- . ' More details'
+ . ' More details ' . esc_html('for ' . $full_address) . ''
. ' '
. '
'
. $compliance_markup
@@ -1336,8 +1350,12 @@ public static function srResidentialResultsGenerator($request_response, $setting
}
}
+ $results_heading_markup = ($show_results_heading && !empty($response))
+ ? "<{$results_heading} class='sr-listings-heading screen-reader-text'>" . esc_html($results_heading_text) . "{$results_heading}>"
+ : "";
$markupGridViewClass = $grid_view == true ? "sr-listings-grid-view" : "";
$resultsMarkup = ""
+ . $results_heading_markup
. "{$resultsMarkup}"
. "
";
$markerCount > 0 ? $mapMarkup = $mapHelper->render($map) : $mapMarkup = '';
@@ -1474,6 +1492,8 @@ public static function srWidgetListingGenerator($response, $settings) {
public static function srContactFormMarkup($listing) {
$custom_form = get_option('sr_leadcapture_custom_form', false);
+ static $form_instance = 0;
+ $form_id = 'sr-contact-form-' . ++$form_instance;
// Use custom form if configured
if ($custom_form != false) {
@@ -1485,36 +1505,42 @@ public static function srContactFormMarkup($listing) {
// Process any form submissions
$submission_message = SimplyRetsContactForm::srContactFormDeliver();
+ $contact_heading_raw = apply_filters('simplyrets_contact_heading_tag', 'h3');
+ $contact_heading = tag_escape($contact_heading_raw);
+ if (empty($contact_heading)) {
+ $contact_heading = 'h3';
+ }
+
// Default lead capture form
$markup = '';
$markup .= '
';
$markup .= '