Compare commits

...

91 Commits

Author SHA1 Message Date
bc8cc41349
prepare release of v1.3.0 (#31)
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2025-03-10 17:22:36 +01:00
a6a48dad58
add a note on cross-site-scripting to JS feature description (#30)
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-04 17:49:22 +01:00
d6ae2fe437
introduce new block attribute to control sorting direction (#25) (#27)
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-03 18:59:08 +01:00
b29a2b169c
Merge branch 'stable' into develop
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-04 18:33:17 +01:00
80946232ca
prepare release of v1.2.3 (#29)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-02-04 18:23:28 +01:00
313159c74a
fix: escape ticker ID in shortcode output (#28)
Ticker ID is user input and may contain literally anything. While this
is properly escaped in the internal query, we should also escape it in
the generated HTML output.
2025-02-04 18:16:30 +01:00
a0573a326c
declare compatibility with WordPress 6.7 2025-02-04 18:16:21 +01:00
bfd27d5f49
update dev-dependencies and ci environment 2025-02-04 18:14:11 +01:00
fceea65dfb
fix: escape ticker ID in shortcode output (#28)
All checks were successful
continuous-integration/drone/push Build is passing
Ticker ID is user input and may contain literally anything. While this
is properly escaped in the internal query, we should also escape it in
the generated HTML output.
2025-02-04 17:58:19 +01:00
4635a47ca9
ci: remove SVN setup from deployment workflows
All checks were successful
continuous-integration/drone/push Build is passing
Current action versions install subversion as required, so we do not
need to add another step.
2025-01-26 19:17:51 +01:00
e5a76fc18c
ci: explicitly install subversion
All checks were successful
continuous-integration/drone/push Build is passing
SVN is required both for WP test setup and deployment actions, but no
longer present on default "ubuntu-latest" environments. Add a step to
install it.
2025-01-07 18:34:03 +01:00
0979f070cf
update dev-dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-07 18:08:03 +01:00
b237b731d5
ci: explicitly enable xdebug coverage and add condition for analysis
Some checks failed
continuous-integration/drone/push Build is passing
CI / integration (7.4, 5.9) (push) Failing after 1m7s
CI / integration (8.0, 6.0) (push) Failing after 1m6s
CI / integration (8.1, 6.2) (push) Failing after 1m4s
CI / integration (8.2, 6.4) (push) Failing after 1m5s
CI / integration (8.3, 6.6) (push) Failing after 55s
CI / analysis (push) Successful in 5s
CI / quality (push) Successful in 2m1s
2024-11-12 17:09:25 +01:00
1cc8869018
ci: add workflow to run wp plugin checks
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-12 16:44:44 +01:00
f662df3f11
ci: update actions
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-12 16:32:34 +01:00
f4f210f105
update dev-dependencies, build with Node 22
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-07 18:34:48 +01:00
c05ee5b5ff
update dev-dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-16 18:16:02 +02:00
9018d7bdca
declare compatibility with WordPress 6.6
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-08 17:02:36 +02:00
78ad69c897
ci: use PHP 8.2 to build before deployment 2024-08-08 17:01:13 +02:00
5d9473eb3f
update test bootstrap script
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-08 16:55:48 +02:00
8b0f89a847
deps: update dev-dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-18 09:40:34 +02:00
ad8293fa3a
add screen reader text and label to checkboxes in settings
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-24 14:43:12 +01:00
a827536da7
extract settings handling into separate class 2024-03-24 14:17:01 +01:00
d650ae08ee
remove unused parameter from update_options() 2024-03-24 14:17:27 +01:00
14ec07e423
remove some default arguments 2024-03-24 13:50:46 +01:00
e738da4f8f
remove unused method Admin::settings_uninstall_section() 2024-03-24 13:50:24 +01:00
20401382b7
use null coalescing operator where applicable
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-24 13:28:47 +01:00
b1d42dadd8
remove redundante test for has_block as we require WP 5.0 now 2024-03-24 13:27:51 +01:00
fc957fd3b4
test: migrate to Yoast WP test utils and work around incompatibilities 2024-03-24 13:24:37 +01:00
5f1e1a7879
add type hints to PHP methods 2024-03-24 11:17:17 +01:00
ad245ebf43
raise requirements to PHP 7.2 and WordPress 5.0 2024-03-24 11:09:06 +01:00
9d1934a6d7
prepare release of v1.2.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-02 11:14:15 +01:00
167433a80b
ci: update actions, use Node v20, add PHP 8.3 2024-03-02 11:03:35 +01:00
8cb71df6fe
deps: update dev-dependencies 2024-03-02 11:02:08 +01:00
b9491a4260
ci: update Drone CI pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-09 18:25:01 +01:00
deedcf4a87
update deprecated get_terms() syntax
Some checks reported errors
continuous-integration/drone/push Build was killed
The old-style syntax was deprecated in WP 4.5. We already require 4.7,
so there is no need to keep the deprecated parameter style.
2023-11-09 18:24:33 +01:00
3f8c9acdc0
add more output sanitization 2023-11-09 18:24:24 +01:00
61ba951f0b
update code style to WPCS v3.0 2023-11-09 18:19:42 +01:00
a1a3117141
ci: test against WP 6.4
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-09 18:11:03 +01:00
4311494e4a
declare compatibility with WP 6.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-01 18:21:01 +02:00
30ad3e9408
dev-dependency updates 2023-05-01 18:19:08 +02:00
2262eba822
declare compatibility with WP 6.1
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-03 17:36:11 +01:00
1f32adbd72
test: use stub method set_up() in unit tests
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-03 17:34:40 +01:00
8c9f98e80b
update dev-dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-03 17:12:42 +01:00
01cb47315a
declare compatibility with WP 6.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-28 19:00:01 +02:00
5a1320c365
add testbed for WP 5.9 and PHP 8.0, update actions
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-14 18:26:01 +02:00
aa1a49c575
bump version to 1.2.1
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-01 18:29:29 +01:00
Mahammed Zishan Khan
b7c236376b
fix: cast string to Number when setting attribute (#21)
Co-authored-by: zishan <zishan.khan@roularta.be>
2022-02-01 18:23:37 +01:00
e3a976fe7a
declare compatibility with WordPress 5.9
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-26 18:05:23 +01:00
3679dce1e2
ci: update slug for automatic deployment
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-23 17:42:42 +01:00
39689dcbcb
ci: use PHP 7.4 for deployment preparation
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-23 17:34:14 +01:00
a2b001a581
bump version to 1.2.0
Some checks failed
continuous-integration/drone/push Build is failing
2022-01-23 17:28:47 +01:00
e007f53e8c
make script evaluation optional
Some checks failed
continuous-integration/drone/push Build is failing
2022-01-23 17:07:37 +01:00
6ac1f85739
add support for embedded script execution in tick content (#14) (#20)
Evaluate embedded SCRIPT-tags in tick content or move them to the
document head for referenced resources.
2022-01-23 16:50:58 +01:00
f50dfe92c7 add optional shortcode support for tick content (#18) (#19)
Some checks failed
continuous-integration/drone/push Build is failing
If the new option is enabled, tick content will be piped through
do_shortcode() to parse nested shortcode content. Disabled by default
as it is not necessary in most cases.
2022-01-23 12:41:29 +01:00
109efd341b
ci: run tests for pull requests, too 2022-01-23 12:37:27 +01:00
fa936d3f54 fix date rendering in API callback
Some checks failed
continuous-integration/drone/push Build is failing
THe callback function gets a prepared data array as input while
get_the_modified_date expects an ID or WP_Post object. We now use
DateTime::parse directly instead of taking another round loading the
post data.
2022-01-22 19:53:16 +01:00
e607ae270f update dev environment
All checks were successful
continuous-integration/drone/push Build is passing
* remove remaining Robo calls from Composer scripts
* update WP test bootstrap
* update dev dependencies
2021-10-18 20:00:49 +02:00
caf471d3bf ci: add WP 5.8 to automated test roster
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 19:38:10 +02:00
2cc277638f re-enable default pagination in API if "limit" is not specified
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-22 19:17:17 +02:00
2a228fc39c remove package locks and Robo configuration
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-22 18:27:18 +02:00
2cb4b62df3 start unit test implementation 2021-05-22 18:15:07 +02:00
b054dfaba2 fix typehint for lastPoll field in JavaScript
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-19 19:31:44 +02:00
718dc18c35 use GitHub actions for CI and automate Sonarcloud analysis 2021-05-18 20:22:20 +02:00
c2c7846beb dev-dependency updates 2021-05-18 20:17:32 +02:00
d88b28343b use "stable" branch instead of "master" 2021-05-18 20:01:40 +02:00
944c2dd6c1 rework update schedule, update existing ticks, use insertBefore()
All checks were successful
continuous-integration/drone/push Build is passing
Re-triggering the update is now done globally again with lock on
ticker level to avoid concurrent updates.

If a tick with a known ID is received, the markup is now updated, so no
duplicates should appear.

The prepend() function used to update the markup is replaced by
insertBefore() for Internet Explorer compatibility.
2021-04-12 18:09:03 +02:00
b2058dfe96 add unique IDs to ticker containers and ticks in HTML markup
Ticker containers get a consecutive number unique within a markup.
Ticks will get an ID based on this number and the post ID.
2021-04-12 18:09:03 +02:00
8e22b37b46 raise minimum supported WP version to 4.7 2021-04-12 18:09:03 +02:00
68efc83273 refactor frontend to use WP REST API instead of WP AJAX
Query new ticks using GET request to the REST API.
Also we now trigger one request per ticker, if more than one is
available on the same page.

HTML markup (list element, content container) is generated client side,
s.t. we can work on the generic response data model
2021-04-12 18:09:03 +02:00
547fc521d0 add formatted date to REST response and use RFC3339 for the query 2021-04-12 18:09:01 +02:00
ca0311622e introduce query filter for REST API
Enabled WP REST API to query for ticker slug, GMT timestamp and limit
parameter as used by AJAX calls. This is done in preparation for
migration to WP REST instead of WP AJAX.
2021-04-12 18:08:32 +02:00
af609d8928 exclude eslint/stylelint configuration from deployment
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-20 11:51:59 +01:00
b245b79bc2 introduce GitHub actions for automated plugin/asset deployment
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-20 11:27:22 +01:00
7baff2f5ff update screenshots 2021-03-20 11:23:46 +01:00
5ebd3c55d3 prepare release of v1.1.1 2021-03-20 11:23:41 +01:00
3c52c9eba9 declare compatibility with WP 5.7 2021-03-20 11:13:58 +01:00
dependabot[bot]
6510e4e532 Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-19 11:55:08 +01:00
b907d2dc89 add PHP 8.0 to CI roster + minor dev dependency updates
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-06 12:29:30 +01:00
7817f460d2 declare compatibility with WP 5.5
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-27 18:33:54 +01:00
0b53709849 drop PHP 7.2 from CI builds and add nightly 2020-09-09 09:52:18 +02:00
7b57b59861 use Node v12 for CI builds 2020-09-09 09:45:14 +02:00
790ef303bb update devenv 2020-09-09 09:40:35 +02:00
3511146c98 docs: update Travis CI badge 2020-09-09 09:39:27 +02:00
c7d7d27f1c add missing textdomain to taxonomy name 2020-05-22 11:28:54 +02:00
dc366abcc4 Merge branch 'master' into develop 2020-05-02 19:32:01 +02:00
00f05f3cf6 prepare release 1.1.0 2020-05-02 18:03:53 +02:00
0d26b15afc use colon instead of dot for hour/minute separation 2020-05-01 15:18:54 +02:00
6ed625dab2
prepend new elements instead of replacing HTML on AJAX update (#9) (#12)
Replacing the body by prepending HTML results in the full content
being re-rendered. This can be a performance issue, but is definitely
annoying when embedding media in ticks.
2020-05-01 14:52:25 +02:00
ee60444932
expose ticks through REST API (#7)
Tickers are already exposed for JS integration in Gutenberg. Now ticks
themselves are also available for use with external systems.
2020-04-28 18:21:16 +02:00
cb8cbbf761
use time() instead of current_datetime() for WP 4.x compatibility (#6)
The function current_datetime() has been introduced in WP 5.3, but
the Plugin should maintain compatibility with 4.x for now.
2020-04-28 17:36:57 +02:00
38 changed files with 1446 additions and 1018 deletions

23
.distignore Normal file
View File

@ -0,0 +1,23 @@
/.git
/.github
/assets
/bin
/dist
/node_modules
/tests
/vendor
/.distignore
/.drone.yml
/.eslintrc.json
/.gitattributes
/.gitignore
/.phpunit.result.cache
/.stylelintrc.json
/.travis.yml
/composer.json
/composer.lock
/CONTRIBUTING.md
/package.json
/package-lock.json
/phpcs.xml
/phpunit.xml

View File

@ -3,15 +3,26 @@ name: default
type: docker
steps:
- name: pre-build
image: composer
- name: composer-install
image: composer:2
commands:
- composer install
- name: test
image: composer
- composer install --ignore-platform-req=php
- name: lint-php
image: php:8.2
commands:
- composer test
- name: lint
image: composer
- ./vendor/bin/phpcs
depends_on:
- composer-install
- name: node-install
image: node:22
commands:
- composer lint-php
- npm install
- name: lint-assets
image: node:22
commands:
- npx eslint scripts/block.js
- npx eslint scripts/liveticker.js
- npx stylelint styles/block.css
- npx stylelint styles/liveticker.css
depends_on:
- node-install

View File

@ -4,13 +4,22 @@
"browser": true
},
"globals": {
"sclivetickerAjax": "readonly",
"scliveticker": "readonly",
"wp": "readonly"
},
"extends": [
"plugin:@wordpress/eslint-plugin/recommended",
"plugin:@wordpress/eslint-plugin/es5"
"plugin:@wordpress/eslint-plugin/custom",
"plugin:@wordpress/eslint-plugin/es5",
"plugin:@wordpress/eslint-plugin/i18n"
],
"rules": {
"@wordpress/i18n-text-domain": [
"error",
{
"allowedTextDomain": [ "stklcode-liveticker" ]
}
]
},
"overrides": [
{
"files": [

33
.gitattributes vendored
View File

@ -1,15 +1,20 @@
/assets export-ignore
.drone.yml export-ignore
.eslintrc.json export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.stylelintrc.json export-ignore
.travis.yml export-ignore
composer.json export-ignore
composer.lock export-ignore
CONTRIBUTING.md export-ignore
package.json export-ignore
package-lock.json export-ignore
phpcs.xml export-ignore
phpunit.xml export-ignore
RoboFile.php export-ignore
/.github export-ignore
/.distignore export-ignore
/.drone.yml export-ignore
/.eslintrc.json export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.stylelintrc.json export-ignore
/.travis.yml export-ignore
/bin export-ignore
/composer.json export-ignore
/composer.lock export-ignore
/CONTRIBUTING.md export-ignore
/dist export-ignore
/package.json export-ignore
/package-lock.json export-ignore
/phpcs.xml export-ignore
/phpunit.xml export-ignore
/tests export-ignore

85
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: CI
on: [push, pull_request]
jobs:
integration:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- php: '8.3'
wordpress: '6.6'
- php: '8.2'
wordpress: '6.4'
- php: '8.1'
wordpress: '6.2'
- php: '8.0'
wordpress: '6.0'
- php: '7.4'
wordpress: '5.9'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install SVN
run: |
if ! command -v svn > /dev/null; then
sudo apt-get update
sudo apt-get install -y subversion
fi
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
tools: composer
- name: Setup DB
run: sudo sudo systemctl start mysql.service
- name: Setup WP
run: bash bin/install-wp-tests.sh wordpress root root localhost "${{ matrix.wordpress }}"
- name: Install
run: composer install --no-interaction
- name: Test
run: composer test
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install
run: |
composer install --no-interaction
npm install
- name: Code style checks for PHP, JS and CSS
run: |
composer lint-php
composer lint-js
composer lint-css
analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Analyze with SonarCloud
if: env.SONAR_TOKEN != ''
uses: sonarsource/sonarcloud-github-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=stklcode-github
-Dsonar.projectKey=stklcode:wp-liveticker
-Dsonar.sources=includes,scripts,views,stklcode-liveticker.php
-Dsonar.exclusions=scripts/*.min.js,styles/*.min.css

View File

@ -0,0 +1,29 @@
name: Plugin asset/readme update
on:
push:
branches:
- stable
jobs:
master:
name: Push to stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
- name: Install
run: composer install --no-interaction
- name: Clean README.md
run: tail -n +7 README.md > README.md.tmp && mv README.md.tmp README.md
- name: WordPress.org plugin asset/readme update
uses: 10up/action-wordpress-plugin-asset-update@stable
env:
SLUG: stklcode-liveticker
ASSETS_DIR: assets
README_NAME: README.md
SVN_PASSWORD: ${{ secrets.WP_SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.WP_SVN_USERNAME }}

View File

@ -0,0 +1,23 @@
name: Plugin check
on:
push:
branches: [ 'stable', 'release/*' ]
pull_request:
branches: [ 'stable' ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Package plugin
run: |
mkdir -p ./dist
tail -n +7 README.md > README.md.tmp && mv README.md.tmp README.md
rsync -rc --exclude-from=.distignore ./ ./dist/stklcode-liveticker --delete --delete-excluded
- name: Check WP plugin
uses: wordpress/plugin-check-action@v1
with:
build-dir: ./dist/stklcode-liveticker

View File

@ -0,0 +1,29 @@
name: Deploy to WordPress.org
on:
push:
tags:
- "v*"
- "!v*-*"
jobs:
tag:
name: New tag
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
- name: Install
run: composer install --no-interaction
- name: Clean README.md
run: tail -n +7 README.md > README.md.tmp && mv README.md.tmp README.md
- name: WordPress Plugin Deploy
uses: 10up/action-wordpress-plugin-deploy@stable
env:
SLUG: stklcode-liveticker
ASSETS_DIR: assets
SVN_PASSWORD: ${{ secrets.WP_SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.WP_SVN_USERNAME }}

7
.gitignore vendored
View File

@ -1,9 +1,8 @@
composer.lock
package-lock.json
/vendor/
/node_modules/
/dist/
.idea
.phpunit.result.cache
/.phpunit.result.cache
/composer.lock
/package-lock.json
**/*.min.css
**/*.min.js

View File

@ -1,3 +1,3 @@
{
"extends": "stylelint-config-wordpress"
"extends": "@wordpress/stylelint-config"
}

View File

@ -1,14 +0,0 @@
language: php
php:
- '7.2'
- '7.3'
- '7.4'
before_script:
- composer install
- npm install
script:
- composer test
- composer lint-all
notifications:
slack:
secure: "R40BhRCETuDule7lz4oGN+qyLvd7dBmuEu6hVELNhWg3DgCgYOXyrWR2dgxsWsAZ3sldpWGfTJKzSShdDanGCpygpYzuvXxjt23YYJ2ihrohYJwiGIhkR9c24LF2yvWBQDBNZaeLBQ3o6FSnbkTBsmRy5ShgKehfKCOQTKmI1yWHi3fvkMElTorrJc710O41yy/bRKBnoIYd4ZfpLMSSVGCPzR5lZPZy3EiGWXPgYdY7jGMI7ADsy+T5VWHyFqgSSJz/U2bcryKzF08FAry8pyu9lN3r61kXHfVCCJX+kcsFxW9yCfuPLnLu14O776y3U6zrX9is+8mEfkMuTXFaL5o8+iq32AmFjTIDQn6o9BKHsknfmppjwZiLgFTp1T7Z/XR6I4nyK9Z5HXDU2HS0eCUknbgXlMLhxWpKhkyx4rQELuvVlgD+u7yRYraawc3v1ycqaPj0S0G5QBFljSuxsZgNnX1hs8VmgafIvOq5qm4ZVVBhhbz+LgvW1m9COr8DDPVhWWdpcWzF8jtkqC3m4Q/1Ssc6T/MbJMgcXRq/C4DlfEs4aYGYfSl7gLtF2PwlEQCppKJwx0fEPkcbZZ1PjpzF+JMwwRmWS88R0oRyThOyCwlG50c+ktB94pJC+sP1aQZrLAd4WDKUPD9vJTas86V3XBjTUJPs8HQaBDFqFdg="

View File

@ -1,19 +1,19 @@
[![Build Status](https://travis-ci.org/stklcode/wp-liveticker.svg?branch=master)](https://travis-ci.org/stklcode/wp-liveticker)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=de.stklcode.web.wordpress.plugins%3Awp-liveticker&metric=alert_status)](https://sonarcloud.io/dashboard?id=de.stklcode.web.wordpress.plugins%3Awp-liveticker)
[![CI](https://github.com/stklcode/wp-liveticker/actions/workflows/test.yml/badge.svg?branch=stable)](https://github.com/stklcode/wp-liveticker/actions/workflows/test.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=stklcode%3Awp-liveticker&metric=alert_status)](https://sonarcloud.io/dashboard?id=stklcode%3Awp-liveticker)
[![WP Plugin Version](https://img.shields.io/wordpress/plugin/v/stklcode-liveticker.svg)](https://wordpress.org/plugins/stklcode-liveticker/)
[![Packagist Version](https://img.shields.io/packagist/v/stklcode/stklcode-liveticker.svg)](https://packagist.org/packages/stklcode/stklcode-liveticker)
[![License](https://img.shields.io/badge/license-GPL%20v2-blue.svg)](https://github.com/stklcode/wp-liveticker/blob/master/LICENSE.md)
[![License](https://img.shields.io/badge/license-GPL%20v2-blue.svg)](https://github.com/stklcode/wp-liveticker/blob/stable/LICENSE.md)
# Liveticker (by stklcode)
* Contributors: Stefan Kalscheuer
* Contributors: stklcode
* Tags: liveticker, feed, rss
* Requires at least: 4.0
* Tested up to: 5.4
* Requires PHP: 5.6
* Stable tag: 1.1.0
* Requires at least: 5.0
* Tested up to: 6.7
* Requires PHP: 7.2
* Stable tag: 1.3.0
* License: GPLv2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
A simple ajaxified liveticker plugin for WordPress.
@ -42,8 +42,8 @@ Alternatively you can also use _Copmposer_.
### Requirements ###
* PHP 5.6 or above
* WordPress 4.0 or above
* PHP 7.2 or above
* WordPress 5.0 or above
## Frequently asked questions
@ -80,6 +80,38 @@ caching time of 12 hours obviously makes no sense.
## Changelog
### 1.3.0 - 2025-03-10
* Requires at least PHP 7.2 and WordPress 5.0
* Sorting direction can now be changed for liveticker blocks
### 1.2.3 - 2025-02-04
* Escape ticker ID in shortcode output
* Tested with WP 6.7
### 1.2.2 - 2024-03-02
* Update use of deprecated WP core functions
* Extend output sanitization
* Tested with WP 6.4
### 1.2.1 - 2022-02-01
* Fix issue with limit in Gutenberg block.
### 1.2.0 - 2022-01-23
* Requires WordPress 4.7 or above
* Migrated AJAX to REST API
* Resolved Javascript compatibility issues with IE11
* Added optional shortcode support for tick content
* Support embedded JavaScript execution in tick content (e.g. for social media integrations)
### 1.1.1 - 2021-03-20
* "Ticker" taxonomy name is now translatable
### 1.1.0 - 2020-05-02
* Requires PHP 5.6 or above

View File

@ -1,527 +0,0 @@
<?php
/**
* Liveticker Robo build script.
*
* This file contains the Robo tasks for building a distributable plugin package.
* Should not be included in final package.
*
* @author Stefan Kalscheuer <stefan@stklcode.de>
*
* @package Liveticker
* @version 1.0.0
*/
use Robo\Exception\TaskException;
use Robo\Tasks;
use Symfony\Component\Finder\Finder;
/**
* Class RoboFile
*/
class RoboFile extends Tasks {
const PROJECT_NAME = 'stklcode-liveticker';
const SVN_URL = 'https://plugins.svn.wordpress.org/stklcode-liveticker';
const OPT_TARGET = 'target';
const OPT_SKIPTEST = 'skipTests';
const OPT_SKIPSTYLE = 'skipStyle';
const OPT_MINIFY = 'minify';
const OPT_NODE = 'node';
/**
* Version tag (read from composer.json).
*
* @var string
*/
private $version;
/**
* Target directory path.
*
* @var string
*/
private $target_dir;
/**
* Final package name.
*
* @var string
*/
private $final_name;
/**
* RoboFile constructor
*
* @param array $opts Options.
*
* @return void
*/
public function __construct( $opts = array( self::OPT_TARGET => 'dist' ) ) {
// Read composer configuration and extract version number..
$composer = json_decode( file_get_contents( __DIR__ . '/composer.json' ) );
// Extract parameter from options.
$this->version = $composer->version;
$this->target_dir = $opts[ self::OPT_TARGET ];
$this->final_name = self::PROJECT_NAME . '.' . $this->version;
}
/**
* Clean up target directory
*
* @param array $opts Options.
*
* @return void
*/
public function clean( $opts = array( self::OPT_TARGET => 'dist' ) ) {
$this->say( 'Cleaning target directory...' );
if ( is_dir( $this->target_dir . '/' . $this->final_name ) ) {
$this->_deleteDir( array( $this->target_dir . '/' . $this->final_name ) );
}
if ( is_file( $this->target_dir . '/' . $this->final_name . '.zip' ) ) {
$this->_remove( $this->target_dir . '/' . $this->final_name . '.zip' );
}
}
/**
* Run PHPUnit tests
*
* @return void
*/
public function test() {
$this->say( 'Executing PHPUnit tests...' );
$this->taskPhpUnit()->configFile( __DIR__ . '/phpunit.xml' )->run();
}
/**
* Run code style tests
*
* @param array $opts Options.
*
* @return void
*/
public function testCS(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
$this->say( 'Executing PHPCS...' );
$this->_exec( __DIR__ . '/vendor/bin/phpcs --standard=phpcs.xml -s' );
if ( $opts[ self::OPT_NODE ] ) {
$this->say( 'Executing ESLint...' );
$this->_exec( __DIR__ . '/node_modules/eslint/bin/eslint.js ' . __DIR__ . '/scripts/block.js' );
$this->_exec( __DIR__ . '/node_modules/eslint/bin/eslint.js ' . __DIR__ . '/scripts/liveticker.js' );
$this->say( 'Executing StyleLint...' );
$this->_exec( __DIR__ . '/node_modules/stylelint/bin/stylelint.js ' . __DIR__ . '/styles/block.css' );
$this->_exec( __DIR__ . '/node_modules/stylelint/bin/stylelint.js ' . __DIR__ . '/styles/liveticker.css' );
}
}
/**
* Build a distributable bundle.
*
* @param array $opts Options.
*
* @return void
*/
public function build(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
$this->clean( $opts );
if ( isset( $opts[ self::OPT_SKIPTEST ] ) && true === $opts[ self::OPT_SKIPTEST ] ) {
$this->say( 'Tests skipped' );
} else {
$this->test();
}
if ( isset( $opts[ self::OPT_SKIPSTYLE ] ) && true === $opts[ self::OPT_SKIPSTYLE ] ) {
$this->say( 'Style checks skipped' );
} else {
$this->testCS( $opts );
}
$this->bundle();
}
/**
* Bundle global resources.
*
* @return void
*/
private function bundle() {
$this->say( 'Bundling resources...' );
$this->taskCopyDir(
array(
'includes' => $this->target_dir . '/' . $this->final_name . '/includes',
'scripts' => $this->target_dir . '/' . $this->final_name . '/scripts',
'styles' => $this->target_dir . '/' . $this->final_name . '/styles',
'views' => $this->target_dir . '/' . $this->final_name . '/views',
)
)->run();
$this->_copy( 'stklcode-liveticker.php', $this->target_dir . '/' . $this->final_name . '/stklcode-liveticker.php' );
$this->_copy( 'README.md', $this->target_dir . '/' . $this->final_name . '/README.md' );
$this->_copy( 'LICENSE.md', $this->target_dir . '/' . $this->final_name . '/LICENSE.md' );
// Remove content before title (e.g. badges) from README file.
$this->taskReplaceInFile( $this->target_dir . '/' . $this->final_name . '/README.md' )
->regex( '/^[^\\#]*/' )
->to( '' )
->run();
}
/**
* Minify JavaScript and CSS assets in target director.
*
* @param array $opts Options.
*
* @return void
*/
public function minify(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
)
) {
if ( $opts[ self::OPT_MINIFY ] ) {
$this->minifyJS( $opts );
$this->minifyCSS( $opts );
} else {
$this->say( 'Minification skipped.' );
}
}
/**
* Minify CSS assets.
*
* @param array $opts Options.
*
* @return void
*/
public function minifyCSS(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
if ( ! isset( $opts[ self::OPT_MINIFY ] ) ) {
$this->say( 'CSS minification skipped.' );
return;
}
$this->say( 'Minifying CSS...' );
$finder = Finder::create()->name( '*.css*' )
->notName( '*.min.css' )
->in( $this->target_dir . '/' . $this->final_name . '/styles' );
foreach ( $finder as $file ) {
$this->taskMinify( $file )->run();
// Replace original file for in-place minification.
$abspath = $file->getPath() . '/' . $file->getFilename();
$this->_rename( str_replace( '.css', '.min.css', $abspath ), $abspath, true );
}
}
/**
* Minify JavaScript assets.
*
* @param array $opts Options.
*
* @return void
*/
public function minifyJS(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
if ( ! isset( $opts[ self::OPT_MINIFY ] ) ) {
$this->say( 'JS minification skipped.' );
return;
}
$this->say( 'Minifying JavaScript...' );
// Minify global JavaScripts files except already minified.
$finder = Finder::create()->name( '*.js*' )
->notName( '*.min.js' )
->in( $this->target_dir . '/' . $this->final_name . '/scripts' );
foreach ( $finder as $file ) {
$this->taskMinify( $file )->run();
// Replace original file for in-place minification.
$abspath = $file->getPath() . '/' . $file->getFilename();
$this->_rename( str_replace( '.js', '.min.js', $abspath ), $abspath, true );
}
}
/**
* Create ZIP package from distribution bundle.
*
* @param array $opts Options.
*
* @return void
*/
public function package(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
$this->build( $opts );
$this->say( 'Packaging...' );
$this->taskPack( $this->target_dir . '/' . $this->final_name . '.zip' )
->addDir( '', $this->target_dir . '/' . $this->final_name )
->run();
}
/**
* Deploy development version (trunk).
*
* @param array $opts Options.
*
* @return void
* @throws TaskException On errors.
*/
public function deployTrunk(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
// First execute build job.
$this->build( $opts );
// Prepare VCS, either checkout or update local copy.
$this->prepareVCS();
$this->say( 'Preparing deployment directory...' );
$this->updateVCStrunk();
// Update remote repository.
$this->say( 'Deploying...' );
$this->commitVCS(
'--force trunk/*',
'Updated ' . self::PROJECT_NAME . ' trunk'
);
}
/**
* Deploy current version tag.
*
* @param array $opts Options.
*
* @return void
* @throws TaskException On errors.
*/
public function deployTag(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
// First execute build job.
$this->build( $opts );
// Prepare VCS, either checkout or update local copy.
$this->prepareVCS();
$this->say( 'Preparing deployment directory...' );
$this->updateVCStag();
// Update remote repository.
$this->say( 'Deploying...' );
$this->commitVCS(
'tags/' . $this->version,
'Updated ' . self::PROJECT_NAME . ' v' . $this->version
);
}
/**
* Deploy current version tag.
*
* @param array $opts Options.
*
* @return void
* @throws TaskException On errors.
*/
public function deployReadme(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
// First execute build job.
$this->build( $opts );
// Prepare VCS, either checkout or update local copy.
$this->prepareVCS();
$this->updateVCSreadme();
// Update remote repository.
$this->say( 'Deploying...' );
$this->commitVCS(
'--force trunk/README.md',
'Updated ' . self::PROJECT_NAME . ' ReadMe'
);
}
/**
* Deploy current version tag and trunk.
*
* @param array $opts Options.
*
* @return void
* @throws TaskException On errors.
*/
public function deployAll(
$opts = array(
self::OPT_TARGET => 'dist',
self::OPT_SKIPTEST => false,
self::OPT_SKIPSTYLE => false,
self::OPT_MINIFY => true,
self::OPT_NODE => false,
)
) {
// First execute build job.
$this->build( $opts );
// Prepare VCS, either checkout or update local copy.
$this->prepareVCS();
$this->say( 'Preparing deployment directory...' );
$this->updateVCStrunk();
$this->updateVCStag();
// Update remote repository.
$this->say( 'Deploying...' );
$this->commitVCS(
array(
'--force trunk/*',
'--force tags/' . $this->version,
),
'Updated ' . self::PROJECT_NAME . ' v' . $this->version
);
}
/**
* Prepare VCS direcory.
*
* Checkout or update local copy of SVN repository.
*
* @return void
* @throws TaskException On errors.
*/
private function prepareVCS() {
if ( is_dir( $this->target_dir . '/svn' ) ) {
$this->taskSvnStack()
->stopOnFail()
->dir( $this->target_dir . '/svn/stklcode-liveticker' )
->update()
->run();
} else {
$this->_mkdir( $this->target_dir . '/svn' );
$this->taskSvnStack()
->dir( $this->target_dir . '/svn' )
->checkout( self::SVN_URL )
->run();
}
}
/**
* Commit VCS changes
*
* @param string|array $to_add Files to add.
* @param string $msg Commit message.
*
* @return void
* @throws TaskException On errors.
*/
private function commitVCS( $to_add, $msg ) {
$task = $this->taskSvnStack()
->stopOnFail()
->dir( $this->target_dir . '/svn/stklode-liveticker' );
if ( is_array( $to_add ) ) {
foreach ( $to_add as $ta ) {
$task = $task->add( $ta );
}
} else {
$task = $task->add( $to_add );
}
$task->commit( $msg )->run();
}
/**
* Update SVN readme file.
*
* @return void
*/
private function updateVCSreadme() {
$trunk_dir = $this->target_dir . '/svn/stklcode-liveticker/trunk';
$this->_copy( $this->target_dir . '/' . $this->final_name . 'README.md', $trunk_dir . 'README.md' );
}
/**
* Update SVN development version (trunk).
*
* @return void
*/
private function updateVCStrunk() {
// Clean trunk directory.
$trunk_dir = $this->target_dir . '/svn/stklcode-liveticker/trunk';
$this->taskCleanDir( $trunk_dir )->run();
// Copy built bundle to trunk.
$this->taskCopyDir( array( $this->target_dir . '/' . $this->final_name => $trunk_dir ) )->run();
}
/**
* Update current SVN version tag.
*
* @return void
*/
private function updateVCStag() {
// Clean tag directory if it exists.
$tag_dir = $this->target_dir . '/svn/stklcode-liveticker/tags/' . $this->version;
if ( is_dir( $tag_dir ) ) {
$this->taskCleanDir( $this->target_dir . '/svn/stklcode-liveticker/tags/' . $this->version )->run();
} else {
$this->_mkdir( $tag_dir );
}
// Copy built bundle to trunk.
$this->taskCopyDir( array( $this->target_dir . '/' . $this->final_name => $tag_dir ) )->run();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 26 KiB

194
bin/install-wp-tests.sh Executable file
View File

@ -0,0 +1,194 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]"
exit 1
fi
DB_NAME=$1
DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
SKIP_DB_CREATE=${6-false}
TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress}
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
else
echo "Error: Neither curl nor wget is installed."
exit 1
fi
}
# Check if svn is installed
check_svn_installed() {
if ! command -v svn > /dev/null; then
echo "Error: svn is not installed. Please install svn and try again."
exit 1
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${WP_VERSION%\-*}
WP_TESTS_TAG="branches/$WP_BRANCH"
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
WP_TESTS_TAG="tags/${WP_VERSION%??}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
# http serves a single offer, whereas https serves multiple. we only want one
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
if [[ -z "$LATEST_VERSION" ]]; then
echo "Latest WordPress version could not be found"
exit 1
fi
WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
set -ex
install_wp() {
if [ -d $WP_CORE_DIR ]; then
return;
fi
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $TMPDIR/wordpress-trunk
rm -rf $TMPDIR/wordpress-trunk/*
check_svn_installed
svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
# https serves multiple offers, whereas http serves single.
download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
LATEST_VERSION=${WP_VERSION%??}
else
# otherwise, scan the releases and get the most up to date minor version of the major release
local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
fi
if [[ -z "$LATEST_VERSION" ]]; then
local ARCHIVE_NAME="wordpress-$WP_VERSION"
else
local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi
download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_test_suite() {
# portable in-place argument for both GNU sed and Mac OSX sed
if [[ $(uname -s) == 'Darwin' ]]; then
local ioption='-i.bak'
else
local ioption='-i'
fi
# set up testing suite if it doesn't yet exist
if [ ! -d $WP_TESTS_DIR ]; then
# set up testing suite
mkdir -p $WP_TESTS_DIR
rm -rf $WP_TESTS_DIR/{includes,data}
check_svn_installed
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
fi
}
recreate_db() {
shopt -s nocasematch
if [[ $1 =~ ^(y|yes)$ ]]
then
mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
create_db
echo "Recreated the database ($DB_NAME)."
else
echo "Leaving the existing database ($DB_NAME) in place."
fi
shopt -u nocasematch
}
create_db() {
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
}
install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi
# parse DB_HOST for port or socket references
local PARTS=(${DB_HOST//\:/ })
local DB_HOSTNAME=${PARTS[0]};
local DB_SOCK_OR_PORT=${PARTS[1]};
local EXTRA=""
if ! [ -z $DB_HOSTNAME ] ; then
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
EXTRA=" --socket=$DB_SOCK_OR_PORT"
elif ! [ -z $DB_HOSTNAME ] ; then
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
fi
fi
# create database
if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]
then
echo "Reinstalling will delete the existing test database ($DB_NAME)"
read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
recreate_db $DELETE_EXISTING_DB
else
create_db
fi
}
install_wp
install_test_suite
install_db

View File

@ -1,6 +1,6 @@
{
"name": "stklcode/stklcode-liveticker",
"version": "1.1.0",
"version": "1.3.0",
"description": "A simple Liveticker for Wordpress.",
"keywords": [
"wordpress",
@ -17,22 +17,17 @@
],
"type": "wordpress-plugin",
"require": {
"php": ">=5.6",
"composer/installers": "~1.7"
"php": ">=7.2",
"composer/installers": "~v2.3.0"
},
"require-dev": {
"php": ">=7",
"consolidation/robo": "^2",
"phpunit/phpunit": "^8",
"phpunit/php-code-coverage": "^7",
"dealerdirect/phpcodesniffer-composer-installer": "^0.6",
"dealerdirect/phpcodesniffer-composer-installer": "^v1.0",
"matthiasmullie/minify": "^1.3",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"slowprog/composer-copy-file": "~0.3",
"squizlabs/php_codesniffer": "^3.5",
"phpcompatibility/php-compatibility": "^9.3",
"wp-coding-standards/wpcs": "^2.2",
"patchwork/jsqueeze": "^2.0",
"natxet/cssmin": "^3.0",
"matthiasmullie/minify": "^1.3"
"squizlabs/php_codesniffer": "^3.11",
"wp-coding-standards/wpcs": "^3.1",
"yoast/wp-test-utils": "^1.2"
},
"scripts": {
"post-install-cmd": [
@ -42,16 +37,7 @@
"@minify"
],
"build": [
"@minify",
"robo build"
],
"package": [
"@minify",
"robo package"
],
"deploy": [
"@minify",
"robo deploy:all"
"@minify"
],
"test": [
"phpunit"
@ -65,12 +51,12 @@
"phpcs --standard=phpcs.xml -s"
],
"lint-css": [
"./node_modules/stylelint/bin/stylelint.js styles/block.css",
"./node_modules/stylelint/bin/stylelint.js styles/liveticker.css"
"npx stylelint styles/block.css",
"npx stylelint styles/liveticker.css"
],
"lint-js": [
"./node_modules/eslint/bin/eslint.js scripts/block.js",
"./node_modules/eslint/bin/eslint.js scripts/liveticker.js"
"npx eslint scripts/block.js",
"npx eslint scripts/liveticker.js"
],
"minify": [
"minifycss styles/block.css > styles/block.min.css",
@ -78,5 +64,11 @@
"minifyjs scripts/block.js > scripts/block.min.js",
"minifyjs scripts/liveticker.js > scripts/liveticker.min.js"
]
},
"config": {
"allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@ -23,7 +23,7 @@ class Admin extends SCLiveticker {
*
* @return void
*/
public static function dashboard_right_now() {
public static function dashboard_right_now(): void {
$total_files = wp_count_posts( 'scliveticker_tick' );
echo '<tr>';
@ -37,181 +37,24 @@ class Admin extends SCLiveticker {
*
* @return void
*/
public static function register_settings_page() {
public static function register_settings_page(): void {
add_submenu_page(
'edit.php?post_type=scliveticker_tick',
'Liveticker ' . __( 'Settings', 'stklcode-liveticker' ),
__( 'Settings', 'stklcode-liveticker' ),
'manage_options',
'scliveticker_settings',
array(
__CLASS__,
'settings_page',
)
array( Settings::class, 'render_settings_page' )
);
}
/**
* Register settings API
*
* @return void
*/
public static function register_settings() {
register_setting(
'scliveticker_settings',
self::OPTION,
array( __CLASS__, 'validate_settings' )
);
// Form sections.
add_settings_section(
'scliveticker_settings_general',
__( 'General', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_general_section' ),
'scliveticker-settings-page'
);
// Form fields.
add_settings_field(
'enable_ajax',
__( 'Use AJAX', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_ajax_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-ajax' )
);
add_settings_field(
'poll_interval',
__( 'AJAX poll interval', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_poll_interval_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-poll-interval' )
);
add_settings_field(
'enable_css',
__( 'Default CSS Styles', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_css_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-css' )
);
add_settings_field(
'show_feed',
__( 'Show RSS feed', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_show_feed_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-show-feed' )
);
}
/**
* Render general section.
*
* @return void
*/
public static function settings_general_section() {
}
/**
* Render uninstall section.
*
* @return void
*/
public static function settings_uninstall_section() {
}
/**
* Render enable AJAX field.
*
* @return void
*/
public static function settings_enable_ajax_field() {
$checked = self::$options['enable_ajax'];
echo '<input id="' . esc_attr( self::OPTION ) . '-enable-ajax" type="checkbox" name="' . esc_attr( self::OPTION ) . '[enable_ajax]" value="1" ' . checked( $checked, 1, false ) . '> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Disable this option to not use AJAX update. This means all liveticker widgets and shortcodes are only updated once on site load.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render AJAX poll interval field.
*
* @return void
*/
public static function settings_poll_interval_field() {
$poll_interval = self::$options['poll_interval'];
echo '<input id="' . esc_attr( self::OPTION ) . '-poll-interval" type="number" name="' . esc_attr( self::OPTION ) . '[poll_interval]" value="' . esc_attr( $poll_interval ) . '"> ';
esc_html_e( 'seconds', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Interval (in seconds) to update ticker if AJAX is enabled.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render enable css field.
*
* @return void
*/
public static function settings_enable_css_field() {
$checked = self::$options['enable_css'];
echo '<input id="' . esc_attr( self::OPTION ) . '-enable-css" type="checkbox" name="' . esc_attr( self::OPTION ) . '[enable_css]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Disable this option to remove the default styling CSS file.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render enable css field.
*
* @return void
*/
public static function settings_show_feed_field() {
$checked = self::$options['show_feed'];
echo '<input id="' . esc_attr( self::OPTION ) . '-show-feed" type="checkbox" name="' . esc_attr( self::OPTION ) . '[show_feed]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Can be overwritten in shortcode.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render the settings page.
*
* @return void
*/
public static function settings_page() {
include SCLIVETICKER_DIR . 'views/settings-page.php';
}
/**
* Validate settings callback.
*
* @param array $input Input arguments.
*
* @return array Parsed arguments.
*/
public static function validate_settings( $input ) {
$defaults = self::default_options();
$result['enable_ajax'] = isset( $input['enable_ajax'] ) ? intval( $input['enable_ajax'] ) : 0;
$result['poll_interval'] = isset( $input['poll_interval'] ) ? intval( $input['poll_interval'] ) : $defaults['poll_interval'];
$result['enable_css'] = isset( $input['enable_css'] ) ? intval( $input['enable_css'] ) : 0;
$result['show_feed'] = isset( $input['show_feed'] ) ? intval( $input['show_feed'] ) : 0;
return $result;
}
/**
* Register custom Gutenberg block type.
*
* @return void
* @since 1.1
*/
public static function register_block() {
public static function register_block(): void {
wp_register_script(
'scliveticker-editor',
SCLIVETICKER_BASE . 'scripts/block.min.js',

102
includes/class-api.php Normal file
View File

@ -0,0 +1,102 @@
<?php
/**
* Liveticker: Plugin API class.
*
* This file contains the plugin's REST API extensions.
*
* @package SCLiveticker
*/
namespace SCLiveticker;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use DateTime;
use WP_REST_Request;
/**
* Liveticker.
*
* @since 1.2
*/
class Api {
/**
* Initialize custom fields for REST API responses.
*
* @return void
*/
public static function init(): void {
// Add rendered modification date to WP_Post object.
register_rest_field(
'scliveticker_tick',
'modified_rendered',
array(
'get_callback' => function ( $post ) {
return ( new DateTime( $post['modified'] ) )->format( 'd.m.Y H:i' );
},
'schema' => array(
'description' => __( 'Rendered modification date and time.', 'stklcode-liveticker' ),
'type' => 'string',
),
)
);
}
/**
* Filter tick queries by ticker slug and date.
*
* @param array $args Query vars.
* @param WP_REST_Request $request The REST request.
*
* @return array Filtered query values.
*/
public static function tick_query_filter( array $args, WP_REST_Request $request ): array {
// Extract arguments.
$ticker_slug = $request->get_param( 'ticker' );
$limit = intval( $request->get_param( 'limit' ) );
$last_poll = $request->get_param( 'last' );
if ( ! empty( $ticker_slug ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'scliveticker_ticker',
'field' => 'slug',
'terms' => $ticker_slug,
);
}
if ( $limit > 0 ) {
$args['posts_per_page'] = $limit;
$args['paged'] = 1;
} elseif ( $limit < 0 ) {
$args['nopaging'] = true;
$args['paged'] = 0;
}
if ( ! empty( $last_poll ) ) {
$last_poll = explode(
',',
gmdate(
'Y,m,d,H,i,s',
rest_parse_date( $last_poll )
)
);
$args['date_query'] = array(
'column' => 'post_date_gmt',
'after' => array(
'year' => intval( $last_poll[0] ),
'month' => intval( $last_poll[1] ),
'day' => intval( $last_poll[2] ),
'hour' => intval( $last_poll[3] ),
'minute' => intval( $last_poll[4] ),
'second' => intval( $last_poll[5] ),
),
);
}
return $args;
}
}

View File

@ -26,7 +26,7 @@ class SCLiveticker {
*
* @var string OPTIONS
*/
const VERSION = '1.1.0';
const VERSION = '1.3.0';
/**
* Options tag.
@ -62,7 +62,7 @@ class SCLiveticker {
*
* @return void
*/
public static function init() {
public static function init(): void {
// Skip on autosave.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
@ -71,13 +71,17 @@ class SCLiveticker {
// Load plugin options.
self::update_options();
// Skip on AJAX if not enabled disabled.
// Add filter for REST API queries.
add_filter( 'rest_api_init', array( 'SCLiveticker\\Api', 'init' ) );
add_filter( 'rest_scliveticker_tick_query', array( 'SCLiveticker\\Api', 'tick_query_filter' ), 10, 2 );
// Skip on AJAX if not enabled.
if ( ( ! isset( self::$options['enable_ajax'] ) || 1 !== self::$options['enable_ajax'] ) && ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
return;
}
// Load Textdomain.
load_plugin_textdomain( 'stklcode-liveticker', false );
load_plugin_textdomain( 'stklcode-liveticker' );
// Allow shortcodes in widgets.
add_filter( 'widget_text', 'do_shortcode' );
@ -100,7 +104,7 @@ class SCLiveticker {
add_action( 'right_now_content_table_end', array( 'SCLiveticker\\Admin', 'dashboard_right_now' ) );
// Settings.
add_action( 'admin_init', array( 'SCLiveticker\\Admin', 'register_settings' ) );
add_action( 'admin_init', array( 'SCLiveticker\\Settings', 'register_settings' ) );
add_action( 'admin_menu', array( 'SCLiveticker\\Admin', 'register_settings_page' ) );
}
}
@ -110,11 +114,11 @@ class SCLiveticker {
*
* @return void
*/
public static function register_types() {
public static function register_types(): void {
// Add new taxonomy, make it hierarchical (like categories).
$labels = array(
'name' => _x( 'Ticker', 'taxonomy general name' ),
'singular_name' => _x( 'Ticker', 'taxonomy singular name' ),
'name' => _x( 'Ticker', 'taxonomy general name', 'stklcode-liveticker' ),
'singular_name' => _x( 'Ticker', 'taxonomy singular name', 'stklcode-liveticker' ),
'search_items' => __( 'Search Tickers', 'stklcode-liveticker' ),
'all_items' => __( 'All Tickers', 'stklcode-liveticker' ),
'parent_item' => __( 'Parent Ticker', 'stklcode-liveticker' ),
@ -178,7 +182,7 @@ class SCLiveticker {
*
* @return string
*/
public static function shortcode_ticker_show( $atts ) {
public static function shortcode_ticker_show( array $atts ): string {
// Indicate presence of shortcode (to enqueue styles/scripts later).
self::$shortcode_present = true;
@ -201,16 +205,6 @@ class SCLiveticker {
} else {
$show_feed = 1 === self::$options['show_feed'];
}
$output = '<div class="wp-block-scliveticker-ticker';
if ( 1 === self::$options['enable_ajax'] ) {
$output .= ' sclt-ajax" '
. 'data-sclt-ticker="' . $ticker . '" '
. 'data-sclt-limit="' . $limit . '" '
. 'data-sclt-last="' . time();
}
$output .= '"><ul>';
$args = array(
'post_type' => 'scliveticker_tick',
'posts_per_page' => $limit,
@ -225,20 +219,32 @@ class SCLiveticker {
$wp_query = new WP_Query( $args );
$ticks = '';
$last = null;
while ( $wp_query->have_posts() ) {
$wp_query->the_post();
$output .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content() );
$ticks .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content(), get_the_ID() );
if ( is_null( $last ) ) {
$last = get_post_modified_time( 'Y-m-d\TH:i:s', true );
}
}
$output .= '</ul></div>';
$output = '<div class="wp-block-scliveticker-ticker';
if ( 1 === self::$options['enable_ajax'] ) {
$output .= ' sclt-ajax" '
. 'data-sclt-ticker="' . esc_attr( $ticker ) . '" '
. 'data-sclt-limit="' . $limit . '" '
. 'data-sclt-last="' . $last;
}
$output .= '"><ul>' . $ticks . '</ul></div>';
// Show RSS feed link, if configured.
if ( $show_feed ) {
$feed_link = get_post_type_archive_feed_link( 'scliveticker_tick' ) . '';
if ( false === strpos( $feed_link, '&' ) ) {
$feed_link .= '?scliveticker_ticker=' . $ticker;
$feed_link .= '?scliveticker_ticker=' . rawurlencode( $ticker );
} else {
$feed_link .= '&scliveticker_ticker=' . $ticker;
$feed_link .= '&scliveticker_ticker=' . rawurlencode( $ticker );
}
$output .= '<a href="' . esc_attr( $feed_link ) . '">Feed</a>';
}
@ -253,9 +259,9 @@ class SCLiveticker {
* @return void
* @since 1.1 Combined former methods "enqueue_styles" and "enqueue_scripts".
*/
public static function enqueue_resources() {
public static function enqueue_resources(): void {
// Only add if shortcode is present.
if ( self::$shortcode_present || self::$widget_present || self::block_present() ) {
if ( self::$shortcode_present || self::$widget_present || has_block( 'scliveticker/ticker' ) ) {
wp_enqueue_script(
'scliveticker-js',
SCLIVETICKER_BASE . 'scripts/liveticker.min.js',
@ -267,11 +273,13 @@ class SCLiveticker {
// Add endpoint to script.
wp_localize_script(
'scliveticker-js',
'sclivetickerAjax',
'scliveticker',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'scliveticker_update-ticks' ),
'poll_interval' => self::$options['poll_interval'] * 1000,
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'scliveticker_update-ticks' ),
'api' => rest_url(),
'embedded_script' => boolval( self::$options['embedded_script'] ),
'poll_interval' => self::$options['poll_interval'] * 1000,
)
);
@ -281,8 +289,7 @@ class SCLiveticker {
'sclt-css',
SCLIVETICKER_BASE . 'styles/liveticker.min.css',
'',
self::VERSION,
'all'
self::VERSION
);
}
}
@ -293,14 +300,14 @@ class SCLiveticker {
*
* @return void
*/
public static function ajax_update() {
public static function ajax_update(): void {
// Verify AJAX nonce.
check_ajax_referer( 'scliveticker_update-ticks' );
// Extract update requests.
if ( isset( $_POST['update'] ) && is_array( $_POST['update'] ) ) { // Input var okay.
$res = array();
// @codingStandardsIgnoreLine Sanitization of arrayhandled on field level.
// @codingStandardsIgnoreLine Sanitization of array handled on field level.
foreach ( wp_unslash( $_POST['update'] ) as $update_req ) {
if ( is_array( $update_req ) && ( isset( $update_req['s'] ) || isset( $update_req['w'] ) ) ) {
if ( isset( $update_req['s'] ) ) {
@ -310,7 +317,7 @@ class SCLiveticker {
$is_widget = true;
$slug = sanitize_text_field( $update_req['w'] );
} else {
// Should never occur, but for completenes' sake...
// Should never occur, but for completeness' sake...
break;
}
@ -353,9 +360,9 @@ class SCLiveticker {
while ( $query->have_posts() ) {
$query->the_post();
if ( $is_widget ) {
$out .= self::tick_html_widget( get_the_time( 'd.m.Y H:i' ), get_the_title(), false );
$out .= self::tick_html_widget( get_the_time( 'd.m.Y H:i' ), get_the_title(), false, get_the_ID() );
} else {
$out .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content(), $is_widget );
$out .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content(), get_the_ID() );
}
}
@ -386,18 +393,19 @@ class SCLiveticker {
*
* @return void
*/
public static function mark_widget_present() {
public static function mark_widget_present(): void {
self::$widget_present = true;
}
/**
* Update options.
*
* @param array $options Optional. New options to save.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 removed unused parameter
*/
protected static function update_options( $options = null ) {
protected static function update_options(): void {
self::$options = wp_parse_args(
get_option( self::OPTION ),
self::default_options()
@ -409,31 +417,37 @@ class SCLiveticker {
*
* @return array The options array.
*/
protected static function default_options() {
protected static function default_options(): array {
return array(
'enable_ajax' => 1,
'poll_interval' => 60,
'enable_css' => 1,
'show_feed' => 0,
'reset_settings' => 0,
'enable_ajax' => 1,
'poll_interval' => 60,
'enable_css' => 1,
'show_feed' => 0,
'enable_shortcode' => 0,
'embedded_script' => 0,
'reset_settings' => 0,
);
}
/**
* Generate HTML code for a tick element.
*
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param string $content Tick content.
* @param boolean $is_widget Is the code for Widget.
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param string $content Tick content.
* @param integer $id Tick ID.
*
* @return string HTML code of tick.
*/
private static function tick_html( $time, $title, $content, $is_widget = false ) {
return '<li class="sclt-tick">'
. '<p><span class="sclt-tick_time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-tick-title">' . esc_html( $title ) . '</span></p>'
. '<p class="sclt-tick-content">' . $content . '</p></li>';
private static function tick_html( string $time, string $title, string $content, int $id ): string {
if ( self::$options['enable_shortcode'] ) {
$content = do_shortcode( $content );
}
return '<li class="sclt-tick" data-sclt-tick-id="' . esc_attr( $id ) . '">'
. '<span class="sclt-tick-time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-tick-title">' . esc_html( $title ) . '</span>'
. '<div class="sclt-tick-content">' . $content . '</div></li>';
}
/**
@ -442,28 +456,21 @@ class SCLiveticker {
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param boolean $highlight Highlight element.
* @param integer $id Tick ID.
*
* @return string HTML code of widget tick.
*/
public static function tick_html_widget( $time, $title, $highlight ) {
public static function tick_html_widget( string $time, string $title, bool $highlight, int $id = 0 ): string {
$out = '<li';
if ( $highlight ) {
$out .= ' class="sclt-widget-new"';
}
if ( $id > 0 ) {
$out .= ' data-sclt-tick-id="' . esc_attr( $id ) . '"';
}
return $out . '>'
. '<span class="sclt-widget-time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-widget-title">' . $title . '</span>'
. '</li>';
}
/**
* Check if the Gutenberg block is present in current post.
*
* @return boolean True, if Gutenberg block is present.
* @since 1.1
*/
private static function block_present() {
return function_exists( 'has_block' ) && // We are in WP 5.x environment.
has_block( 'scliveticker/ticker' ); // Specific block is present.
}
}

305
includes/class-settings.php Normal file
View File

@ -0,0 +1,305 @@
<?php
/**
* Liveticker: Plugin settings class.
*
* This file contains the derived class for the plugin's settings.
*
* @package SCLiveticker
*/
namespace SCLiveticker;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Liveticker admin configuration.
*
* @since 1.3.0 extracted from {@link Admin} class
*/
class Settings extends SCLiveticker {
/**
* Register settings API
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function register_settings(): void {
register_setting(
'scliveticker_settings',
self::OPTION,
array( __CLASS__, 'validate_settings' )
);
// Form sections.
add_settings_section(
'scliveticker_settings_general',
__( 'General', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_general_section' ),
'scliveticker-settings-page'
);
// Form fields.
add_settings_field(
'enable_ajax',
__( 'Use AJAX', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_ajax_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-ajax' )
);
add_settings_field(
'poll_interval',
__( 'AJAX poll interval', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_poll_interval_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-poll-interval' )
);
add_settings_field(
'enable_css',
__( 'Default CSS Styles', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_css_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-css' )
);
add_settings_field(
'show_feed',
__( 'Show RSS feed', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_show_feed_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-show-feed' )
);
add_settings_field(
'enable_shortcode',
__( 'Shortcode support', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_shortcode_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-shortcode' )
);
add_settings_field(
'embedded_script',
__( 'Embedded JavaScript', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_embedded_script_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-embedded-script' )
);
}
/**
* Render the settings page.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function render_settings_page(): void {
?>
<div class="wrap">
<div id="icon-options-general" class="icon32"><br></div>
<h2>Liveticker <?php esc_html_e( 'Settings', 'stklcode-liveticker' ); ?></h2>
<?php
if ( isset( $_GET['settings-updated'] ) ) { // phpcs:ignore
echo '<div class="updated"><p>' . esc_html__( 'Settings updated successfully.', 'stklcode-liveticker' ) . '</p></div>';
}
?>
<form action="options.php" method="post">
<?php
settings_fields( 'scliveticker_settings' );
do_settings_sections( 'scliveticker-settings-page' );
submit_button();
?>
</form>
</div>
<?php
}
/**
* Render general section.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_general_section(): void {
}
/**
* Render enable AJAX field.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_enable_ajax_field(): void {
self::render_checkbox(
'enable-ajax',
'[enable_ajax]',
self::$options['enable_ajax'],
__( 'Disable this option to not use AJAX update. This means all liveticker widgets and shortcodes are only updated once on site load.', 'stklcode-liveticker' ),
__( 'Enable AJAX updates', 'stklcode-liveticker' )
);
}
/**
* Render AJAX poll interval field.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_poll_interval_field(): void {
$poll_interval = self::$options['poll_interval'];
echo '<input id="' . esc_attr( self::OPTION ) . '-poll-interval" type="number" name="' . esc_attr( self::OPTION ) . '[poll_interval]" value="' . esc_attr( $poll_interval ) . '"> ';
esc_html_e( 'seconds', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Interval (in seconds) to update ticker if AJAX is enabled.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render enable css field.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_enable_css_field(): void {
self::render_checkbox(
'enable-css',
'[enable_css]',
self::$options['enable_css'],
__( 'Disable this option to remove the default styling CSS file.', 'stklcode-liveticker' ),
__( 'Enable default stylesheet', 'stklcode-liveticker' )
);
}
/**
* Render enable css field.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_show_feed_field(): void {
self::render_checkbox(
'show-feed',
'[show_feed]',
self::$options['show_feed'],
__( 'Can be overwritten in shortcode.', 'stklcode-liveticker' ),
__( 'Show RSS feed in shortcode', 'stklcode-liveticker' )
);
}
/**
* Render enable shortcode field.
*
* @return void
*
* @since 1.2.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_enable_shortcode_field(): void {
self::render_checkbox(
'enable-shortcode',
'[enable_shortcode]',
self::$options['enable_shortcode'],
__( 'Enable shortcode processing in tick content.', 'stklcode-liveticker' ),
__( 'Allow shortcodes in tick content', 'stklcode-liveticker' )
);
}
/**
* Render embedded script field.
*
* @return void
*
* @since 1.2.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function settings_embedded_script_field(): void {
self::render_checkbox(
'embedded-script',
'[embedded_script]',
self::$options['embedded_script'],
__( 'Allow embedded script evaluation in tick contents. This might be useful for embedded content, e.g. social media integrations.', 'stklcode-liveticker' ) .
' ' .
__( 'Be aware that this feature potentially enables cross-site scripting, so make sure content is created by trusted people and only enable this if required.', 'stklcode-liveticker' ),
__( 'Allow JavaScript in tick content', 'stklcode-liveticker' )
);
}
/**
* Validate settings callback.
*
* @param array $input Input arguments.
*
* @return array Parsed arguments.
*
* @since 1.0.0
* @since 1.3.0 moved from Admin to Settings class
*/
public static function validate_settings( array $input ): array {
$defaults = self::default_options();
$result['enable_ajax'] = isset( $input['enable_ajax'] ) ? intval( $input['enable_ajax'] ) : 0;
$result['poll_interval'] = isset( $input['poll_interval'] ) ? intval( $input['poll_interval'] ) : $defaults['poll_interval'];
$result['enable_css'] = isset( $input['enable_css'] ) ? intval( $input['enable_css'] ) : 0;
$result['show_feed'] = isset( $input['show_feed'] ) ? intval( $input['show_feed'] ) : 0;
$result['enable_shortcode'] = isset( $input['enable_shortcode'] ) ? intval( $input['enable_shortcode'] ) : 0;
$result['embedded_script'] = isset( $input['embedded_script'] ) ? intval( $input['embedded_script'] ) : 0;
return $result;
}
/**
* Render a checkbox field.
*
* @param string $id Field ID.
* @param string $name Option name.
* @param mixed $value Current value.
* @param string $description Description text.
* @param string $screen_reader_text Screen reader text.
*
* @return void
*/
private static function render_checkbox(
string $id,
string $name,
$value,
string $description,
string $screen_reader_text
) {
?>
<fieldset>
<legend class="screen-reader-text"><?php echo esc_html( $screen_reader_text ); ?></legend>
<label for="<?php echo esc_attr( self::OPTION . '-' . $id ); ?>">
<input id="<?php echo esc_attr( self::OPTION . '-' . $id ); ?>" name="<?php echo esc_attr( self::OPTION . $name ); ?>" type="checkbox" value="1" <?php checked( $value, 1 ); ?>>
<?php esc_html_e( 'Enable', 'stklcode-liveticker' ); ?>
</label>
<p class="description"><?php echo esc_html( $description ); ?></p>
</fieldset>
<?php
}
}

View File

@ -28,7 +28,7 @@ class System extends SCLiveticker {
*
* @return void
*/
public static function activate() {
public static function activate(): void {
// Load current options.
self::update_options();
@ -49,7 +49,7 @@ class System extends SCLiveticker {
*
* @return void
*/
public static function uninstall() {
public static function uninstall(): void {
// Delete all ticks.
$ticks = new WP_Query( array( 'post_type' => 'scliveticker_tick' ) );
foreach ( $ticks->get_posts() as $tick ) {

View File

@ -31,7 +31,7 @@ class Widget extends WP_Widget {
/**
* Register the widget.
*/
public static function register() {
public static function register(): void {
register_widget( __CLASS__ );
}
@ -49,10 +49,10 @@ class Widget extends WP_Widget {
SCLiveticker::mark_widget_present();
$instance = self::fill_options_with_defaults( $instance );
$before_widget = isset( $args['before_widget'] ) ? $args['before_widget'] : '';
$after_widget = isset( $args['after_widget'] ) ? $args['after_widget'] : '';
$before_title = isset( $args['before_title'] ) ? $args['before_title'] : '';
$after_title = isset( $args['after_title'] ) ? $args['after_title'] : '';
$before_widget = $args['before_widget'] ?? '';
$after_widget = $args['after_widget'] ?? '';
$before_title = $args['before_title'] ?? '';
$after_title = $args['after_title'] ?? '';
$title = apply_filters( 'scliveticker_catlit', $instance['title'] );
$category = apply_filters( 'scliveticker_catlit', $instance['category'] );
$count = apply_filters( 'scliveticker_catlit', $instance['count'] );
@ -95,14 +95,16 @@ class Widget extends WP_Widget {
$wp_query = new WP_Query( $args );
$cnt = 0;
while ( $wp_query->have_posts() && ( $count <= 0 || ++ $cnt < $count ) ) {
while ( $wp_query->have_posts() && ( $count <= 0 || ++$cnt < $count ) ) {
$wp_query->the_post();
// @codingStandardsIgnoreLine
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
echo SCLiveticker::tick_html_widget(
esc_html( get_the_time( 'd.m.Y - H:i' ) ),
get_the_title(),
( '1' === $highlight && get_the_time( 'U' ) > ( time() - $highlight_time ) )
( '1' === $highlight && get_the_time( 'U' ) > ( time() - $highlight_time ) ),
get_the_ID()
);
// phpcs:enable
}
echo '</ul>';
@ -144,7 +146,13 @@ class Widget extends WP_Widget {
$highlight = isset( $instance['highlight'] ) ? esc_attr( $instance['highlight'] ) : '0';
$highlight_time = isset( $instance['highlight_time'] ) ? esc_attr( $instance['highlight_time'] ) : '0';
$ajax = isset( $instance['ajax'] ) ? esc_attr( $instance['ajax'] ) : '0';
$categories = get_terms( 'scliveticker_ticker', 'orderby=name&order=ASC' );
$categories = get_terms(
array(
'taxonomy' => 'scliveticker_ticker',
'orderby' => 'name',
'order' => 'ASC',
)
);
include SCLIVETICKER_DIR . 'views/widget-form.php';
}
@ -156,7 +164,7 @@ class Widget extends WP_Widget {
*
* @return array Complete instance configuration.
*/
private static function fill_options_with_defaults( $instance ) {
private static function fill_options_with_defaults( array $instance ): array {
$default = array(
'title' => '',
'category' => '',

View File

@ -1,13 +1,13 @@
{
"name": "stklcode-liveticker",
"version": "1.1.0",
"version": "1.3.0",
"description": "A simple Liveticker for Wordpress.",
"author": "Stefan Kalscheuer",
"license": "GPL-2.0+",
"devDependencies": {
"@wordpress/eslint-plugin": "^3",
"eslint": "^6",
"stylelint": "^13",
"stylelint-config-wordpress": "^16"
"@wordpress/eslint-plugin": "^22",
"@wordpress/stylelint-config": "^23",
"eslint": "^8",
"stylelint": "^16"
}
}

View File

@ -12,12 +12,19 @@
<file>views</file>
<!-- Compliance with WordPress Coding Standard -->
<config name="minimum_supported_wp_version" value="4.0"/>
<config name="minimum_supported_wp_version" value="5.0"/>
<rule ref="WordPress">
<exclude name="WordPress.DB.SlowDBQuery.slow_db_query_tax_query"/>
</rule>
<!-- Verify usage of the correct textdomain for WP translation -->
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array" value="stklcode-liveticker"/>
</properties>
</rule>
<!-- PHP compatibility level -->
<config name="testVersion" value="5.6-"/>
<rule ref="PHPCompatibility"/>
<config name="testVersion" value="7.2-"/>
<rule ref="PHPCompatibilityWP"/>
</ruleset>

View File

@ -1,8 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="./vendor/autoload.php">
<phpunit bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true">
<testsuites>
<testsuite name="WP Liveticker 2 TestSuite">
<directory suffix="-test.php">./test/</directory>
<directory prefix="test-" suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">includes</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -86,6 +86,10 @@
type: 'boolean',
default: false,
},
sort: {
type: 'string',
// implicit default: 'desc', left empty here for backwards compatibility of the block
},
},
edit: withSelect( function( select ) {
return {
@ -152,7 +156,7 @@
disabled: props.attributes.unlimited,
value: props.attributes.limit,
onChange: function( val ) {
props.setAttributes( { limit: val } );
props.setAttributes( { limit: Number( val ) } );
},
}
),
@ -166,6 +170,26 @@
},
}
),
el(
wp.components.SelectControl,
{
label: __( 'Output direction', 'stklcode-liveticker' ),
value: props.attributes.sort,
options: [
{
value: 'desc',
label: __( 'newest first', 'stklcode-liveticker' ),
},
{
value: 'asc',
label: __( 'oldest first', 'stklcode-liveticker' ),
},
],
onChange: function( val ) {
props.setAttributes( { sort: val } );
},
}
),
];
}
@ -183,6 +207,7 @@
'data-sclt-ticker': props.attributes.ticker,
'data-sclt-limit': props.attributes.unlimited ? 0 : props.attributes.limit,
'data-sclt-last': 0,
'data-sclt-sort': props.attributes.sort,
}
);
},

View File

@ -1,14 +1,12 @@
/**
* Contructor of the scLiveticker object.
* Constructor of the scLiveticker object.
*
* @class
*/
( function() {
var ajaxURL = sclivetickerAjax.ajax_url;
var nonce = sclivetickerAjax.nonce;
var pollInterval = sclivetickerAjax.poll_interval;
var apiURL;
var pollInterval;
var ticker;
var widgets;
/**
* Initialize iveticker JS component.
@ -17,69 +15,45 @@
*/
var init = function() {
var updateNow = false;
var c = 0;
// Opt out if AJAX pobject not present.
if ( 'undefined' === typeof sclivetickerAjax ) {
if ( 'undefined' === typeof scliveticker ) {
return;
}
// Extract AJAX settings.
ajaxURL = sclivetickerAjax.ajax_url;
nonce = sclivetickerAjax.nonce;
pollInterval = sclivetickerAjax.poll_interval;
// Extract settings.
apiURL = scliveticker.api + 'wp/v2/scliveticker_tick';
pollInterval = scliveticker.poll_interval;
// Get ticker elements.
ticker = [].map.call(
document.querySelectorAll( 'div.wp-block-scliveticker-ticker.sclt-ajax' ),
function( elem ) {
var list = elem.querySelector( 'ul' );
var last = Number( elem.getAttribute( 'data-sclt-last' ) );
if ( ! list ) {
list = document.createElement( 'ul' );
elem.appendChild( list );
}
if ( 0 === last ) {
var o = parseElement( elem, false, ++c );
if ( '0' === o.lastPoll ) {
updateNow = true;
}
return {
s: elem.getAttribute( 'data-sclt-ticker' ),
l: elem.getAttribute( 'data-sclt-limit' ),
t: last,
e: list,
};
return o;
}
);
// Get widget elements.
widgets = [].map.call(
document.querySelectorAll( 'div.wp-widget-scliveticker-ticker.sclt-ajax' ),
function( elem ) {
var list = elem.querySelector( 'ul' );
var last = Number( elem.getAttribute( 'data-sclt-last' ) );
if ( ! list ) {
list = document.createElement( 'ul' );
elem.appendChild( list );
ticker.concat(
[].map.call(
document.querySelectorAll( 'div.wp-widget-scliveticker-ticker.sclt-ajax' ),
function( elem ) {
var o = parseElement( elem, true, ++c );
if ( '0' === o.lastPoll ) {
updateNow = true;
}
return o;
}
if ( 0 === last ) {
updateNow = true;
}
return {
w: elem.getAttribute( 'data-sclt-ticker' ),
l: elem.getAttribute( 'data-sclt-limit' ),
t: last,
e: list,
};
}
)
);
// Trigger update, if necessary.
if ( ( 0 < ticker.length || widgets.length ) && 0 < pollInterval ) {
if ( ( 0 < ticker.length ) && 0 < pollInterval ) {
if ( updateNow ) {
update();
} else {
@ -89,100 +63,197 @@
};
/**
* Update liveticker on current page via AJAX call.
* Parse an HTML element containing a liveticker.
*
* @param {HTMLElement} elem The element.
* @param {boolean} widget Is the element a widget?
* @param {number} n Number of the container.
* @return {{ticker: string, lastPoll: string, ticks: any, limit: string, isWidget: *}} Ticker descriptor object.
*/
var parseElement = function( elem, widget, n ) {
var list = elem.querySelector( 'ul' );
var last = elem.getAttribute( 'data-sclt-last' );
var sort = elem.getAttribute( 'data-sclt-sort' );
elem.id = 'sclt-' + n;
if ( ! list ) {
list = document.createElement( 'ul' );
elem.appendChild( list );
} else {
[].forEach.call(
elem.querySelectorAll( 'li.sclt-tick' ),
function( li ) {
var id = li.getAttribute( 'data-sclt-tick-id' );
if ( id ) {
li.id = 'sclt-' + n + '-' + id;
li.removeAttribute( 'data-sclt-tick-id' );
}
}
);
}
if ( 'asc' !== sort && 'desc' !== 'sort' ) {
sort = 'desc';
}
return {
id: n,
ticker: elem.getAttribute( 'data-sclt-ticker' ),
limit: elem.getAttribute( 'data-sclt-limit' ),
lastPoll: last,
sort: sort,
ticks: list,
isWidget: widget,
updating: false,
};
};
/**
* Update liveticker on current page via REST API call.
*
* @return {void}
*/
var update = function() {
// Extract ticker-slug, limit and timestamp of last poll.
var updateReq = 'action=sclt_update-ticks&_ajax_nonce=' + nonce;
var i, j;
var xhr = new XMLHttpRequest();
// Iterate over available tickers.
ticker.forEach(
function( t ) {
var xhr, query;
for ( i = 0; i < ticker.length; i++ ) {
updateReq = updateReq +
'&update[' + i + '][s]=' + ticker[ i ].s +
'&update[' + i + '][l]=' + ticker[ i ].l +
'&update[' + i + '][t]=' + ticker[ i ].t;
}
for ( j = 0; j < widgets.length; j++ ) {
updateReq = updateReq +
'&update[' + ( i + j ) + '][w]=' + widgets[ j ].w +
'&update[' + ( i + j ) + '][l]=' + widgets[ j ].l +
'&update[' + ( i + j ) + '][t]=' + widgets[ j ].t;
}
if ( t.updating ) {
// Do not update twice.
return;
}
// Issue AJAX request.
xhr.open( 'POST', ajaxURL, true );
xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded;' );
xhr.onreadystatechange = function() {
var updateResp;
if ( XMLHttpRequest.DONE === this.readyState && 200 === this.status ) {
try {
updateResp = JSON.parse( this.responseText );
if ( updateResp ) {
updateResp.forEach(
function( u ) {
ticker.forEach(
function( t ) {
if ( t.s === u.s ) {
t.t = u.t; // Update last poll timestamp.
updateHTML( t, u ); // Update HTML markup.
}
}
);
widgets.forEach(
function( t ) {
if ( t.w === u.w ) {
t.t = u.t;
updateHTML( t, u );
}
t.updating = true;
xhr = new XMLHttpRequest();
query = '?ticker=' + encodeURI( t.ticker ) +
'&limit=' + encodeURI( t.limit ) +
'&last=' + encodeURI( t.lastPoll );
xhr.open( 'GET', apiURL + query, true );
xhr.addEventListener(
'load',
function() {
var updateResp;
try {
updateResp = JSON.parse( this.responseText );
if ( updateResp ) {
updateResp.reverse();
updateResp.forEach(
function( u ) {
addTick( t, u );
}
);
}
);
t.updating = false;
} catch ( e ) {
// eslint-disable-next-line no-console
console.warn( 'Liveticker AJAX update failed, stopping automatic updates.' );
}
}
setTimeout( update, pollInterval ); // Re-trigger update.
} catch ( e ) {
// eslint-disable-next-line no-console
console.warn( 'Liveticker AJAX update failed, stopping automatic updates.' );
}
);
xhr.send();
}
};
xhr.send( updateReq );
);
// Re-trigger update.
setTimeout( update, pollInterval );
};
/**
* Do actual update of HTML code.
*
* @param {Object} t Ticker or Widget reference.
* @param {number} t.l Limit of entries to display.
* @param {HTMLElement} t.e HTML element of the ticker/widget.
* @param {Object} u Update entity.
* @param {string} u.h HTML code to append.
* @param {number} u.t Timetsamp of last update.
* @param {Object} t Ticker or Widget reference.
* @param {Object} u Update entity.
* @return {void}
*/
var updateHTML = function( t, u ) {
var addTick = function( t, u ) {
// Parse new DOM-part.
var n = document.createElement( 'ul' );
n.innerHTML = u.h;
var li = document.createElement( 'li' );
var time = document.createElement( 'span' );
var title = document.createElement( 'span' );
var content = document.createElement( 'div' );
var cls = t.isWidget ? 'sclt-widget' : 'sclt-tick';
var old;
var scripts = [];
// Prepend new ticks to container.
while ( n.hasChildNodes() ) {
t.e.prepend( n.lastChild );
}
time.classList.add( cls + '-time' );
time.innerText = u.modified_rendered;
title.classList.add( cls + '-title' );
title.innerText = u.title.rendered;
content.classList.add( cls + '-content' );
content.innerHTML = u.content.rendered;
t.e.parentNode.setAttribute( 'data-sclt-last', u.t );
// Remove tail, if limit is set.
if ( 0 < t.l ) {
[].slice.call( t.e.getElementsByTagName( 'li' ), t.l ).forEach(
function( li ) {
li.remove();
// Process embedded scripts, if any.
if ( scliveticker.embedded_script ) {
Array.prototype.forEach.call(
content.getElementsByTagName( 'script' ),
function( script ) {
var script2;
if ( script.src ) {
// Move referenced scripts to page head.
script.parentNode.removeChild( script );
script2 = document.createElement( 'script' );
Array.prototype.forEach.call( script.attributes, function( a ) {
script2.setAttribute( a.nodeName, a.nodeValue );
} );
document.head.appendChild( script2 );
} else {
scripts.push( script );
}
}
);
}
// Create the actual tick element.
li.id = 'sclt-' + t.id + '-' + u.id;
li.classList.add( cls );
li.appendChild( time );
li.appendChild( title );
li.appendChild( content );
old = document.getElementById( 'sclt-' + t.id + '-' + u.id );
if ( old ) {
// Replace entry, if it already exists (i.e. has been updated).
t.ticks.replaceChild( li, old );
} else if ( 'asc' === t.sort ) {
// Append new tick as last element to container.
t.ticks.appendChild( li );
} else {
// Prepend new tick as fist element to container.
t.ticks.insertBefore( li, t.ticks.firstChild );
}
// Update last poll time.
t.lastPoll = u.date_gmt;
t.ticks.parentNode.setAttribute( 'data-sclt-last', u.date_gmt );
// Remove tail, if limit is set.
if ( 0 < t.limit && t.limit < t.ticks.children.length ) {
if ( 'asc' === t.sort ) {
old = [].slice.call( t.ticks.children, 0, -t.limit );
} else {
old = [].slice.call( t.ticks.children, t.limit );
}
old.forEach(
function( l ) {
l.remove();
}
);
}
// Evaluate embedded inline scripts.
// Directly evaluate script otherwise.
scripts.forEach( function( script ) {
try {
// eslint-disable-next-line no-eval
eval( script.innerHTML );
} catch ( e ) {
// eslint-disable-next-line no-console
console.warn( 'Failed to evaluate embedded script.' );
}
} );
};
document.addEventListener(

View File

@ -9,7 +9,7 @@
* @wordpress-plugin
* Plugin Name: Liveticker (by stklcode)
* Description: A simple Liveticker for WordPress.
* Version: 1.1.0
* Version: 1.3.0
* Author: Stefan Kalscheuer
* Author URI: https://www.stklcode.de
* Text Domain: stklcode-liveticker
@ -26,7 +26,7 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Liveticker (by stklcode). If not, see http://www.gnu.org/licenses/gpl-2.0.html.
* along with Liveticker (by stklcode). If not, see https://www.gnu.org/licenses/gpl-2.0.html.
*/
// Exit if accessed directly.
@ -64,22 +64,24 @@ spl_autoload_register( 'scliveticker_autoload' );
/**
* Autoloader for Liveticker classes.
*
* @param string $class Name of the class to load.
* @param string $class_name Name of the class to load.
*
* @return void
*/
function scliveticker_autoload( $class ) {
function scliveticker_autoload( string $class_name ): void {
$plugin_classes = array(
'SCLiveticker\\SCLiveticker',
'SCLiveticker\\Admin',
'SCLiveticker\\Api',
'SCLiveticker\\Settings',
'SCLiveticker\\System',
'SCLiveticker\\Widget',
);
if ( in_array( $class, $plugin_classes, true ) ) {
if ( in_array( $class_name, $plugin_classes, true ) ) {
require_once sprintf(
'%s/includes/class-%s.php',
SCLIVETICKER_DIR,
strtolower( str_replace( '_', '-', substr( $class, 13 ) ) )
strtolower( str_replace( '_', '-', substr( $class_name, 13 ) ) )
);
}
}

29
tests/bootstrap.php Normal file
View File

@ -0,0 +1,29 @@
<?php
/**
* PHPUnit bootstrap file.
*
* @package SCLiveticker
*/
use Yoast\WPTestUtils\WPIntegration;
require_once dirname( __DIR__ ) . '/vendor/yoast/wp-test-utils/src/WPIntegration/bootstrap-functions.php';
$_tests_dir = WPIntegration\get_path_to_wp_test_dir();
// Get access to tests_add_filter() function.
require_once $_tests_dir . 'includes/functions.php';
// Add plugin to active mu-plugins to make sure it gets loaded.
tests_add_filter(
'muplugins_loaded',
function() {
require dirname( __DIR__ ) . '/stklcode-liveticker.php';
}
);
/*
* Bootstrap WordPress. This will also load the Composer autoload file, the PHPUnit Polyfills
* and the custom autoloader for the TestCase and the mock object classes.
*/
WPIntegration\bootstrap_it();

157
tests/test-api.php Normal file
View File

@ -0,0 +1,157 @@
<?php
/**
* Liveticker: Plugin API tests.
*
* This file contains unit tests for the plugin's REST API extensions.
*
* @package SCLiveticker
*/
namespace SCLiveticker;
use DateInterval;
use DateTime;
use WP_REST_Request;
use WP_REST_Server;
use Yoast\WPTestUtils\WPIntegration\TestCase;
/**
* Class Test_API.
*/
class Test_API extends TestCase {
/**
* Initialize WP REST API for tests.
*
* @return void
*/
public function set_up(): void {
parent::set_up();
global $wp_rest_server;
$wp_rest_server = new WP_REST_Server();
do_action( 'rest_api_init' );
}
/**
* Test presence of registered routes for ticks and tickers.
*
* @return void
*/
public function test_register_route(): void {
global $wp_rest_server;
$routes = $wp_rest_server->get_routes();
self::assertArrayHasKey( '/wp/v2/scliveticker_tick', $routes, 'Ticks not exposed in API' );
self::assertArrayHasKey( '/wp/v2/scliveticker_tick/(?P<id>[\d]+)', $routes, 'Specific ticks not exposed in API' );
self::assertArrayHasKey( '/wp/v2/scliveticker_ticker', $routes, 'Tickers not exposed in API' );
self::assertArrayHasKey( '/wp/v2/scliveticker_ticker/(?P<id>[\d]+)', $routes, 'Specific tickers not exposed in API' );
}
/**
* Test fetching ticks and tickers via the REST API.
*
* @return void
*/
public function test_get_ticks(): void {
global $wp_rest_server;
$request = new WP_REST_Request( 'GET', '/wp/v2/scliveticker_tick' );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code' );
self::assertEmpty( $response->get_data(), 'No data expected on empty database' );
// Create two tickers with 10 ticks each.
wp_set_current_user( 1 );
$ticker_id = array(
1 => self::factory()->term->create(
array(
'name' => 'Ticker 1',
'description' => 'Test Liveticker 1',
'slug' => 'ticker1',
'taxonomy' => 'scliveticker_ticker',
)
),
2 >= self::factory()->term->create(
array(
'name' => 'Ticker 2',
'description' => 'Test Liveticker 2',
'slug' => 'ticker2',
'taxonomy' => 'scliveticker_ticker',
)
),
);
$dt = new DateTime( '2021-05-22 16:17:18' );
foreach ( range( 1, 20 ) as $n ) {
$t = 0 === $n % 2 ? '1' : '2';
$i = ceil( $n / 2 );
$p = self::factory()->post->create(
array(
'post_type' => 'scliveticker_tick',
'post_date_gmt' => $dt->format( 'Y-m-d H_i_s' ),
'post_title' => 'Tick ' . $t . '.' . $i,
'post_status' => 'publish',
'post_content' => 'Content of Tick ' . $t . '.' . $i,
)
);
wp_set_object_terms( $p, $ticker_id[ $t ], 'scliveticker_ticker' );
$dt->add( new DateInterval( 'PT1M' ) );
}
wp_set_current_user( 0 );
// Verify ticker presence via API.
$response = $wp_rest_server->dispatch( new WP_REST_Request( 'GET', '/wp/v2/scliveticker_ticker' ) );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code' );
self::assertEquals( 2, count( $response->get_data() ), 'Unexpected number of tickers' );
// Query all entries. The "limit" parameter should overrule "per_page".
$request->set_param( 'limit', - 1 );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code' );
self::assertEquals( 20, count( $response->get_data() ), 'Unexpected number of ticks without filter' );
// Limit number of entries.
$request->set_param( 'limit', 12 );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code with limit' );
self::assertEquals( 12, count( $response->get_data() ), 'Unexpected number of ticks with limit' );
// Use built-in pagination.
$request->set_param( 'limit', null );
$request->set_param( 'per_page', 13 );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code for first page' );
self::assertEquals( 13, count( $response->get_data() ), 'Unexpected number of ticks for first page' );
self::assertEquals( 20, $response->get_headers()['X-WP-Total'], 'Unexpected total header' );
self::assertEquals( 2, $response->get_headers()['X-WP-TotalPages'], 'Unexpected pages header' );
$request->set_param( 'page', 2 );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code for second page' );
self::assertEquals( 7, count( $response->get_data() ), 'Unexpected number of ticks for second page' );
// Filter by time.
$request->set_param( 'limit', null );
$request->set_param( 'per_page', 10 );
$request->set_param( 'page', 1 );
$request->set_param( 'last', $dt->sub( new DateInterval( 'PT4M' ) )->format( 'Y-m-d H:i:s' ) );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code with time filter' );
self::assertEquals( 3, count( $response->get_data() ), 'Unexpected number of ticks with time filter' );
// Filter by ticker.
$request->set_param( 'last', null );
$request->set_param( 'ticker', 'ticker1' );
$response = $wp_rest_server->dispatch( $request );
self::assertEquals( 200, $response->get_status(), 'Unexpected status code with ticker filter' );
self::assertEquals( 10, count( $response->get_data() ), 'Unexpected number of ticks with ticker filter' );
self::assertEmpty(
array_filter(
$response->get_data(),
function ( $t ) use ( $ticker_id ) {
return 1 !== count( $t['scliveticker_ticker'] ) || ! in_array( $ticker_id[1], $t['scliveticker_ticker'], true );
}
),
'No tick from ticker 2 should be present filtering for ticker1'
);
}
}

View File

@ -1,29 +0,0 @@
<?php
/**
* Liveticker: Settings page.
*
* This file contains the view model for the Plugin settings oage.
*
* @package Liveticker
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="wrap">
<div id="icon-options-general" class="icon32"><br></div>
<h2>Liveticker <?php esc_html_e( 'Settings', 'stklcode-liveticker' ); ?></h2>
<?php
if ( isset( $_GET['settings-updated'] ) ) { // phpcs:ignore
echo '<div class="updated"><p>' . esc_html__( 'Settings updated successfully.', 'stklcode-liveticker' ) . '</p></div>';
}
?>
<form action="options.php" method="post">
<?php
settings_fields( 'scliveticker_settings' );
do_settings_sections( 'scliveticker-settings-page' );
submit_button();
?>
</form>
</div>

View File

@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<table>
<tr>
<td>
<label for="<?php echo esc_html( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:' ); ?></label>
<label for="<?php echo esc_html( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'stklcode-liveticker' ); ?></label>
</td>
<td>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
@ -54,10 +54,10 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php esc_html_e( 'all', 'stklcode-liveticker' ); ?>
</option>
<?php
for ( $i = 1; $i <= 10; $i ++ ) {
for ( $i = 1; $i <= 10; $i++ ) {
printf(
'<option value="%d"%s>%d</option>',
$i,
intval( $i ),
( $i === $count ) ? ' selected' : '',
intval( $i )
);