106 Commits

Author SHA1 Message Date
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
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
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
ac8062b262 README++
Some checks failed
continuous-integration/drone/push Build is failing
2020-05-02 19:16:31 +02:00
10830babcc restore UL element for widget output 2020-05-02 19:16:31 +02:00
0515debed0 prepare release 1.1.0 2020-05-02 19:16:31 +02:00
6700d76528 use colon instead of dot for hour/minute separation 2020-05-02 19:16:31 +02:00
d75b790ccf 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-02 19:16:31 +02:00
04131a1d99 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-05-02 19:16:31 +02:00
4a1197af28 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-05-02 19:16:31 +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
cf65d6d7c3 Merge branch 'develop' 2020-04-10 17:25:54 +02:00
8d0e033ae9 set PHPUnit to 8.x to use same version across all builds
Actually there are no unit tets implemented, however CI builds fail...
2020-04-10 17:17:57 +02:00
4463fa7f1f raise required PHP version to 5.6 2020-04-09 16:38:44 +02:00
ca5c81356b respect "enable_css" option and do not enqueue stylesheet if disabled 2020-04-09 12:27:02 +02:00
f0bf1c3542 enqueue styles and scripts if only Gutenberg block is present
Resources have only been added if shortcode or widget hooks have been
triggered before. If we switch to native Gutenberg block, none of the
checks is true and no scripts are available.
2020-04-09 12:27:02 +02:00
0cc35e9dd5 update devenv and readme 2020-04-09 12:26:59 +02:00
5c4b1eaf60 select first available ticker for new blocks 2020-01-13 17:28:19 +01:00
dba37eb64b remove undefined className attribute from block rendering 2020-01-13 17:18:52 +01:00
42dce1eb65 update stylelint 2020-01-13 17:18:30 +01:00
e41027d246 update Drone CI configuration 2020-01-13 17:16:22 +01:00
3df29ff76f add PHP 7.4 to build roster and remove unsupported 7.x versions 2020-01-07 19:33:37 +01:00
7f35444642 fix Robo file and pass node option through all targets [skip ci] 2019-11-25 20:10:33 +01:00
e1ba63fd8e gutenberg block screenshot, updated WP 5.3 screenshots, readme++ 2019-11-24 18:09:21 +01:00
61a3e4a104 adjustments to block output when no tickers are available
Always show the first label and display spinner or message below.
2019-11-24 18:09:21 +01:00
dd074293a7 make block script ES5 compatible
There are polyfills for methods like apiFetch, so migrate the syntax to
ES5 (no generators, method shorthands, arrow functions, ...) and we are
done here without adding transpilers to the project.
2019-11-24 18:09:21 +01:00
df1de841e8 expose ticker taxonomy to API and add select element to Gutenberg block
This is more handy than entering the ticker slug manually.
2019-11-24 18:09:21 +01:00
acf3b010f1 wrap frontend JS into IIFE to
Exposing the liveticker functionality to a public namespace is not
necessary, so it is now wrapped into an anonymous function.
2019-11-24 18:09:21 +01:00
b88e1c2903 update ticker immediately, if not prefilled by backend
When a ticker is added by a Gutenberg block, it is initially empty. This
can be detected by checking the "last" flag for 0 value. If found, the
ticker is now updated immediately by the AJAX function and not waiting
for the poll interval to trigger.
2019-11-24 18:09:21 +01:00
fc4783d07a bump version to 1.1.0-alpha and update dev-dependencies 2019-11-24 18:09:21 +01:00
9ddcc41c6b implement Gutenberg block to add liveticker without legacy shortcode
* implement react-based JS block
* refactor shortcode and widget to use the same syntax and classes
2019-11-24 18:09:21 +01:00
e201d7c02f use GMT timestamp for dynamic update
Use real unix timestamps and do not rely on the system timezone. We now
query the "post_date_gmt" field and use timestamps without zone bias.
2019-11-24 17:53:08 +01:00
8103e78652 Declare compatibility with WP 5.3 [skip ci] 2019-11-24 17:47:42 +01:00
c195388d64 Declare compatibility with WP 5.3 [skip ci] 2019-11-13 17:39:27 +01:00
0cab1a3580 update JS code style to currently recommended conventions
ES5 support is still present to not introduce breaking changes (dropped
IE10 support) in minor updates.
2019-08-28 11:15:02 +02:00
2906d435d9 fix .eslintrc 2019-08-27 20:19:28 +02:00
8acd840fc5 update Drone CI configuration 2019-08-27 20:13:22 +02:00
b923d3494b remove PHP 5.5 from CI matrix
Dev dependencies start dropping support for PHP 5.5 now. Because the
5.2 to 5.4 are already missing in the CI matrix, we drop explicit 5.5
builds for now and rely on static compatibility checks.
2019-08-27 20:09:56 +02:00
7ca687a85c include ESLint and stylelint checks in build scripts using Node 2019-08-27 20:09:44 +02:00
d4edbb6423 update dev-dependencies 2019-08-27 19:12:39 +02:00
18c55f6c4c remove underscore prefix from $_options field 2019-08-27 19:12:00 +02:00
1a0b763290 rename local variable $cat to not mess up with WP global 2019-08-27 19:11:34 +02:00
29232a05eb Update Packagist link [skip ci] 2019-05-08 16:48:53 +02:00
2d33465181 Declare compatibility with WP 5.2 [skip ci] 2019-05-08 16:38:25 +02:00
053f1dda52 Declare compatibility with WP 5.1 [skip CI] 2019-03-02 17:19:15 +01:00
faf5f0fc5a Change package name to actual plugin name
Installing the plugin via Composer leads to inconsistent directory
layout and potentially overwrites a different plugin.
2019-03-02 17:17:34 +01:00
faebd1f705 Add .gitattributes
Set development files and assets to export ignore list in order to clean
up the package distributed via Composer/Packagist.
2019-02-19 20:53:58 +01:00
89f33429a2 Update Drone CI config 2018-11-11 20:33:03 +01:00
de5a043a39 Add Slack notification to Travis 2018-11-02 16:07:23 +01:00
4fb675336d Add badges to ReadMe and corrected CI links in Contributing [skip ci] 2018-11-02 10:49:47 +01:00
38 changed files with 1543 additions and 957 deletions

22
.distignore Normal file
View File

@ -0,0 +1,22 @@
/.git
/.github
/assets
/bin
/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

@ -1,38 +1,29 @@
clone:
git:
image: plugins/git
depth: 1
skip_verify: true
kind: pipeline
name: default
type: docker
pipeline:
restore-cache:
image: drillster/drone-volume-cache
restore: true
mount:
- vendor
volumes:
- /var/lib/drone/cache:/cache
cache_key: [ DRONE_REPO_OWNER, DRONE_REPO_NAME, DRONE_BRANCH ]
pre-build:
image: composer
steps:
- name: composer-install
image: composer:2
commands:
- composer install
test:
image: composer
- composer install --ignore-platform-req=php
- name: lint-php
image: php:8.2
commands:
- ./vendor/bin/robo test
test-style:
image: composer
- ./vendor/bin/phpcs
depends_on:
- composer-install
- name: node-install
image: node:18
commands:
- ./vendor/bin/robo test:cs
- npm install
- name: lint-assets
image: node:20
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
rebuild-cache:
image: drillster/drone-volume-cache
rebuild: true
mount:
- vendor
volumes:
- /var/lib/drone/cache:/cache
cache_key: [ DRONE_REPO_OWNER, DRONE_REPO_NAME, DRONE_BRANCH ]

View File

@ -1,3 +1,34 @@
{
"extends": "./vendor/npm-asset/eslint-config-wordpress/index.js"
"env": {
"es6": false,
"browser": true
},
"globals": {
"scliveticker": "readonly",
"wp": "readonly"
},
"extends": [
"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": [
"*"
],
"rules": {
"no-var": "off",
"object-shorthand": "off"
}
}
]
}

19
.gitattributes vendored Normal file
View File

@ -0,0 +1,19 @@
/assets 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
/package.json export-ignore
/package-lock.json export-ignore
/phpcs.xml export-ignore
/phpunit.xml export-ignore
/tests export-ignore

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

@ -0,0 +1,77 @@
name: CI
on: [push, pull_request]
jobs:
integration:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- php: '8.3'
wordpress: '6.4'
- php: '8.2'
wordpress: '6.3'
- php: '8.1'
wordpress: '6.1'
- php: '8.0'
wordpress: '5.9'
- php: '5.6'
wordpress: '4.7'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
- name: Setup DB
run: sudo /etc/init.d/mysql start
- 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@v3
with:
node-version: '20'
- 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
uses: sonarsource/sonarcloud-github-action@master
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.0'
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,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: '7.4'
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 }}

5
.gitignore vendored
View File

@ -1,7 +1,8 @@
composer.lock
/vendor/
/node_modules/
/dist/
.idea
/.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,13 +0,0 @@
language: php
dist: trusty
php:
- '5.5'
- '5.6'
- '7.0'
- '7.1'
- '7.2'
- '7.3'
before_script:
- composer install
script:
- composer test-all

View File

@ -111,10 +111,10 @@ We probably find a solution for that.
## Continuous Integration
Automated tests are run using [Travis CI](https://travis-ci.org/stklcode/wp-liveticker2) for every commit including pull requests.
Automated tests are run using [Travis CI](https://travis-ci.org/stklcode/wp-liveticker) for every commit including pull requests.
They ensure compatibility with the supported PHP versions and the WP Coding Standards.
There is also a semi-automated code quality analysis pushing results to [SonarCloud](https://sonarcloud.io/dashboard?id=de.stklcode.web.wordpress.plugins%3Awp-liveticker2).
There is also a semi-automated code quality analysis pushing results to [SonarCloud](https://sonarcloud.io/dashboard?id=de.stklcode.web.wordpress.plugins%3Awp-liveticker).
Keep in mind that the ruleset is not yet perfect, so not every minor issue has to be fixed immediately.
## Still Open Questions?

View File

@ -1,11 +1,17 @@
[![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/stable/LICENSE.md)
# Liveticker (by stklcode)
* Contributors: Stefan Kalscheuer
* Tags: liveticker, feed, rss
* Requires at least: 4.0
* Tested up to: 5.0
* Requires PHP: 5.2
* Stable tag: 1.0.0
* Requires at least: 4.7
* Tested up to: 6.4
* Requires PHP: 5.6
* Stable tag: 1.2.2
* License: GPLv2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -21,28 +27,31 @@ Easily add multiple livetickers, add them to posts with shortcode or use them as
* Handle multiple Tickers
* Automatic update via AJAX
* RSS feed capability
* Shortcode to display liveticker
* Gutenberg block and shortcode to display liveticker
* Add ticker to sidebar widgets
* Ability to customise through CSS
* Ability to customize through CSS
* Localization support
## Installation
1. Upload `stklcode-liveticker` to the `/wp-content/plugins/` directory.
2. Activate the plugin through the 'Plugins' menu in WordPress.
3. Go to Liveticker menu to start.
If you dont know how to install a plugin for WordPress, [heres how](https://wordpress.org/support/article/managing-plugins/).
You can obtain the plugin through fhe official WordPress plugin repository.
Alternatively you can also use _Copmposer_.
### Requirements ###
* PHP 5.2 or above
* WordPress 4.0 or above
* PHP 5.6 or above
* WordPress 4.7 or above
## Frequently asked questions
### How do I display a liveticker on my post/page?
Use the shortcode `[liveticker ticker="my-ticker"]`.
On WordPress 5 sites there is a Gutenberg Block available to embed a liveticker in your post.
You can also use the shortcode `[liveticker ticker="my-ticker"]` on WordPress 4 or classic-mode sites.
If you want to define a custom tick limit, you might also add a limit with `[liveticker ticker="my-ticker" limit="10"]`.
### Can I use my own styles?
@ -52,9 +61,11 @@ You can deactivate the default stylesheet on the settings page and include your
### Does the liveticker work with caching?
It strongly depends on the use case.
If you update your ticker every 5 minutes, a caching time of 12 hours obviously makes no sense.
However the AJAX update will fetch the latest ticks and update cached tickers depending on the configured interval.
If you activate AJAX updates (enabled by default), the JavaScript will automatically update the content, even when the
page is loaded from cached.
If AJAX is disabled, it depends on your update and caching intervals. If you update your ticker every 5 minutes, a
caching time of 12 hours obviously makes no sense.
## Screenshots
@ -63,11 +74,42 @@ However the AJAX update will fetch the latest ticks and update cached tickers d
2. Tick management
3. Ticker configuration.
4. Settings page
5. Example shortcode
6. Example widget
5. Gutenberg block
6. Example shortcode
7. Example widget
## Changelog
### 1.2.2 - 2023-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
* Use GMT for automatic updates
* Gutenberg Block available
* Ticks exposed through REST API
* Changed AJAX update logic for embedded media compatibility
### 1.0.0 - 2018-11-02
* Initial release

View File

@ -1,490 +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';
/**
* 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
*
* @return void
*/
public function testCS() {
$this->say( 'Executing PHPCS tests...' );
$this->_exec( __DIR__ . '/vendor/bin/phpcs --standard=phpcs.xml -s' );
}
/**
* 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,
)
) {
$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();
}
$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' );
}
/**
* 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,
)
) {
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,
)
) {
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,
)
) {
$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,
)
) {
// 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,
)
) {
// 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,
)
) {
// 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,
)
) {
// 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: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/screenshot-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

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

@ -0,0 +1,181 @@
#!/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"
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/*
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.github.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}
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/wp-liveticker",
"version": "1.0.0",
"name": "stklcode/stklcode-liveticker",
"version": "1.2.2",
"description": "A simple Liveticker for Wordpress.",
"keywords": [
"wordpress",
@ -17,23 +17,18 @@
],
"type": "wordpress-plugin",
"require": {
"php": ">=5.2",
"composer/installers": "~1.0"
"php": ">=5.6",
"composer/installers": "~1.12"
},
"require-dev": {
"php": ">=5.2",
"consolidation/robo": "^1.0.0",
"phpunit/phpunit": "*",
"phpunit/php-code-coverage": "*",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4",
"slowprog/composer-copy-file": "~0.2",
"squizlabs/php_codesniffer": "^3.1",
"wimg/php-compatibility": "^8.0",
"wp-coding-standards/wpcs": "~0.14",
"patchwork/jsqueeze": "^2.0.5",
"natxet/CssMin": "^3.0.5",
"dealerdirect/phpcodesniffer-composer-installer": "^v1.0",
"matthiasmullie/minify": "^1.3",
"npm-asset/eslint-config-wordpress": "^2.0"
"phpcompatibility/phpcompatibility-wp": "^2.1",
"phpunit/phpunit": "^5|^6|^7|^8|^9",
"slowprog/composer-copy-file": "~0.3",
"squizlabs/php_codesniffer": "^3.9",
"wp-coding-standards/wpcs": "^3.0",
"yoast/phpunit-polyfills": "^2.0"
},
"scripts": {
"post-install-cmd": [
@ -43,39 +38,38 @@
"@minify"
],
"build": [
"@minify",
"robo build"
],
"package": [
"@minify",
"robo package"
],
"deploy": [
"@minify",
"robo deploy:all"
],
"test-all": [
"@test",
"@test-cs"
"@minify"
],
"test": [
"phpunit"
],
"test-cs": [
"lint-all": [
"@lint-php",
"@lint-css",
"@lint-js"
],
"lint-php": [
"phpcs --standard=phpcs.xml -s"
],
"fix-cs": [
"phpcbf --standard=phpcs.xml"
"lint-css": [
"npx stylelint styles/block.css",
"npx stylelint styles/liveticker.css"
],
"lint-js": [
"npx eslint scripts/block.js",
"npx eslint scripts/liveticker.js"
],
"minify": [
"minifycss styles/block.css > styles/block.min.css",
"minifycss styles/liveticker.css > styles/liveticker.min.css",
"minifyjs scripts/block.js > scripts/block.min.js",
"minifyjs scripts/liveticker.js > scripts/liveticker.min.js"
]
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
"config": {
"allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
]
}
}

View File

@ -4,9 +4,11 @@
*
* This file contains the derived class for the plugin's administration features.
*
* @package Liveticker
* @package SCLiveticker
*/
namespace SCLiveticker;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
@ -15,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Liveticker admin configuration.
*/
class SCLiveticker_Admin extends SCLiveticker {
class Admin extends SCLiveticker {
/**
* Add to Right Now Widget
*
@ -105,6 +107,24 @@ class SCLiveticker_Admin extends SCLiveticker {
'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' )
);
}
/**
@ -129,7 +149,7 @@ class SCLiveticker_Admin extends SCLiveticker {
* @return void
*/
public static function settings_enable_ajax_field() {
$checked = self::$_options['enable_ajax'];
$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' );
@ -142,7 +162,7 @@ class SCLiveticker_Admin extends SCLiveticker {
* @return void
*/
public static function settings_poll_interval_field() {
$poll_interval = self::$_options['poll_interval'];
$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' );
@ -156,7 +176,7 @@ class SCLiveticker_Admin extends SCLiveticker {
* @return void
*/
public static function settings_enable_css_field() {
$checked = self::$_options['enable_css'];
$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' );
@ -169,13 +189,43 @@ class SCLiveticker_Admin extends SCLiveticker {
* @return void
*/
public static function settings_show_feed_field() {
$checked = self::$_options['show_feed'];
$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 enable shortcode field.
*
* @return void
*
* @since 1.2
*/
public static function settings_enable_shortcode_field() {
$checked = self::$options['enable_shortcode'];
echo '<input id="' . esc_attr( self::OPTION ) . '-enable-shortcode" type="checkbox" name="' . esc_attr( self::OPTION ) . '[enable_shortcode]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Enable shortcode processing in tick content.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render embedded script field.
*
* @return void
*
* @since 1.2
*/
public static function settings_embedded_script_field() {
$checked = self::$options['embedded_script'];
echo '<input id="' . esc_attr( self::OPTION ) . '-embedded-script" type="checkbox" name="' . esc_attr( self::OPTION ) . '[embedded_script]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Allow embedded script evaluation in tick contents. This might be useful for embedded content, e.g. social media integrations.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render the settings page.
*
@ -195,11 +245,44 @@ class SCLiveticker_Admin extends SCLiveticker {
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;
$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;
}
/**
* Register custom Gutenberg block type.
*
* @return void
* @since 1.1
*/
public static function register_block() {
wp_register_script(
'scliveticker-editor',
SCLIVETICKER_BASE . 'scripts/block.min.js',
array( 'wp-blocks', 'wp-element' ),
self::VERSION,
true
);
wp_register_style(
'scliveticker-editor',
SCLIVETICKER_BASE . 'styles/block.min.css',
array(),
self::VERSION
);
register_block_type(
'scliveticker-block/liveticker',
array(
'editor_script' => 'scliveticker-editor',
'editor_style' => 'scliveticker-editor',
)
);
}
}

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

@ -0,0 +1,101 @@
<?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;
/**
* Liveticker.
*
* @since 1.2
*/
class Api {
/**
* Initialize custom fields for REST API responses.
*
* @return void
*/
public static function init() {
// 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( $args, $request ) {
// 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

@ -4,14 +4,19 @@
*
* This file contains the plugin's base class.
*
* @package Liveticker
* @package SCLiveticker
*/
namespace SCLiveticker;
use WP_Query;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Liveticker.
*/
@ -21,7 +26,7 @@ class SCLiveticker {
*
* @var string OPTIONS
*/
const VERSION = '1.0.0';
const VERSION = '1.2.2';
/**
* Options tag.
@ -33,9 +38,9 @@ class SCLiveticker {
/**
* Plugin options.
*
* @var array $_options
* @var array $options
*/
protected static $_options;
protected static $options;
/**
* Marker if shortcode is present.
@ -66,8 +71,12 @@ class SCLiveticker {
// Load plugin options.
self::update_options();
// Skip on AJAX if not enabled disabled.
if ( ( ! isset( self::$_options['enable_ajax'] ) || 1 !== self::$_options['enable_ajax'] ) && ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
// 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;
}
@ -80,14 +89,11 @@ class SCLiveticker {
// Add shortcode.
add_shortcode( 'liveticker', array( __CLASS__, 'shortcode_ticker_show' ) );
// Enqueue styles.
add_action( 'wp_footer', array( __CLASS__, 'enqueue_styles' ) );
// Enqueue JavaScript.
add_action( 'wp_footer', array( __CLASS__, 'enqueue_scripts' ) );
// Enqueue styles and JavaScript.
add_action( 'wp_footer', array( __CLASS__, 'enqueue_resources' ) );
// Add AJAX hook if configured.
if ( 1 === self::$_options['enable_ajax'] ) {
if ( 1 === self::$options['enable_ajax'] ) {
add_action( 'wp_ajax_sclt_update-ticks', array( __CLASS__, 'ajax_update' ) );
add_action( 'wp_ajax_nopriv_sclt_update-ticks', array( __CLASS__, 'ajax_update' ) );
}
@ -95,11 +101,11 @@ class SCLiveticker {
// Admin only actions.
if ( is_admin() ) {
// Add dashboard "right now" functionality.
add_action( 'right_now_content_table_end', array( 'SCLiveticker_Admin', 'dashboard_right_now' ) );
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_menu', array( 'SCLiveticker_Admin', 'register_settings_page' ) );
add_action( 'admin_init', array( 'SCLiveticker\\Admin', 'register_settings' ) );
add_action( 'admin_menu', array( 'SCLiveticker\\Admin', 'register_settings_page' ) );
}
}
@ -111,8 +117,8 @@ class SCLiveticker {
public static function register_types() {
// 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' ),
@ -133,6 +139,7 @@ class SCLiveticker {
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'show_in_rest' => true,
)
);
@ -162,6 +169,7 @@ class SCLiveticker {
'supports' => array( 'title', 'editor', 'author' ),
'taxonomies' => array( 'scliveticker_ticker' ),
'has_archive' => true,
'show_in_rest' => true,
);
register_post_type( 'scliveticker_tick', $args );
@ -195,18 +203,8 @@ class SCLiveticker {
if ( isset( $atts['feed'] ) ) {
$show_feed = 'true' === strtolower( $atts['feed'] ) || '1' === $atts['feed'];
} else {
$show_feed = 1 === self::$_options['show_feed'];
$show_feed = 1 === self::$options['show_feed'];
}
$output = '<ul class="sclt-ticker';
if ( 1 === self::$_options['enable_ajax'] ) {
$output .= ' sclt-ticker-ajax" '
. 'data-sclt-ticker="' . $ticker . '" '
. 'data-sclt-limit="' . $limit . '" '
. 'data-sclt-last="' . current_time( 'timestamp' );
}
$output .= '">';
$args = array(
'post_type' => 'scliveticker_tick',
'posts_per_page' => $limit,
@ -221,12 +219,24 @@ 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>';
$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="' . $last;
}
$output .= '"><ul>' . $ticks . '</ul></div>';
// Show RSS feed link, if configured.
if ( $show_feed ) {
@ -243,31 +253,15 @@ class SCLiveticker {
return $output;
}
/**
* Register frontend CSS.
*
* @return void
*/
public static function enqueue_styles() {
// Only add if shortcode is present.
if ( self::$shortcode_present || self::$widget_present ) {
wp_enqueue_style(
'wplt-css',
SCLIVETICKER_BASE . 'styles/liveticker.min.css',
'',
self::VERSION, 'all'
);
}
}
/**
* Register frontend JS.
*
* @return void
* @since 1.1 Combined former methods "enqueue_styles" and "enqueue_scripts".
*/
public static function enqueue_scripts() {
public static function enqueue_resources() {
// Only add if shortcode is present.
if ( self::$shortcode_present || self::$widget_present ) {
if ( self::$shortcode_present || self::$widget_present || self::block_present() ) {
wp_enqueue_script(
'scliveticker-js',
SCLIVETICKER_BASE . 'scripts/liveticker.min.js',
@ -279,13 +273,26 @@ 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,
)
);
// Enqueue CSS if enabled.
if ( 1 === self::$options['enable_css'] ) {
wp_enqueue_style(
'sclt-css',
SCLIVETICKER_BASE . 'styles/liveticker.min.css',
'',
self::VERSION,
'all'
);
}
}
}
@ -301,7 +308,7 @@ class SCLiveticker {
// 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'] ) ) {
@ -311,12 +318,18 @@ 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;
}
$limit = ( isset( $update_req['l'] ) ) ? intval( $update_req['l'] ) : - 1;
$last_poll = ( isset( $update_req['t'] ) ) ? intval( $update_req['t'] ) : 0;
$last_poll = explode(
',',
gmdate(
'Y,m,d,H,i,s',
( isset( $update_req['t'] ) ) ? intval( $update_req['t'] ) : 0
)
);
// Query new ticks from DB.
$query_args = array(
@ -330,7 +343,15 @@ class SCLiveticker {
),
),
'date_query' => array(
'after' => date( 'c', $last_poll ),
'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] ),
),
),
);
@ -340,9 +361,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() );
}
}
@ -350,13 +371,13 @@ class SCLiveticker {
$res[] = array(
'w' => $slug,
'h' => $out,
't' => current_time( 'timestamp' ),
't' => time(),
);
} else {
$res[] = array(
's' => $slug,
'h' => $out,
't' => current_time( 'timestamp' ),
't' => time(),
);
}
}
@ -385,7 +406,7 @@ class SCLiveticker {
* @return void
*/
protected static function update_options( $options = null ) {
self::$_options = wp_parse_args(
self::$options = wp_parse_args(
get_option( self::OPTION ),
self::default_options()
);
@ -398,29 +419,35 @@ class SCLiveticker {
*/
protected static function default_options() {
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( $time, $title, $content, $id ) {
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>';
}
/**
@ -429,17 +456,32 @@ 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( $time, $title, $highlight, $id = 0 ) {
$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.
}
}

View File

@ -4,9 +4,13 @@
*
* This file contains the derived class for the plugin's system operations.
*
* @package Liveticker
* @package SCLiveticker
*/
namespace SCLiveticker;
use WP_Query;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
@ -15,7 +19,7 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Liveticker system configuration.
*/
class SCLiveticker_System extends SCLiveticker {
class System extends SCLiveticker {
/**
* Activation hook.
@ -31,7 +35,7 @@ class SCLiveticker_System extends SCLiveticker {
// Add default settings to database.
$defaults = self::default_options();
if ( self::$_options['reset_settings'] ) {
if ( self::$options['reset_settings'] ) {
// Reset requested, overwrite existing options with default.
update_option( self::OPTION, $defaults );
} else {

View File

@ -4,17 +4,22 @@
*
* This file contains the liveticker widget.
*
* @package Liveticker
* @package SCLiveticker
*/
namespace SCLiveticker;
use WP_Query;
use WP_Widget;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class SCLiveticker_Widget.
* Class Widget.
*/
class SCLiveticker_Widget extends WP_Widget {
class Widget extends WP_Widget {
/**
* SCLiveticker_Widget constructor.
@ -68,14 +73,14 @@ class SCLiveticker_Widget extends WP_Widget {
echo $before_title . esc_html( $title ) . $after_title;
}
echo '<ul class="sclt-widget';
echo '<div class="wp-widget-scliveticker-ticker';
if ( '1' === $ajax ) {
echo ' sclt-widget-ajax" '
echo ' sclt-ajax" '
. 'data-sclt-ticker="' . esc_attr( $category ) . '" '
. 'data-sclt-limit="' . esc_attr( $count ) . '" '
. 'data-sclt-last="' . esc_attr( current_time( 'timestamp' ) );
. 'data-sclt-last="' . esc_attr( current_datetime()->getTimestamp() );
}
echo '">';
echo '"><ul class="sclt-widget">';
$args = array(
'post_type' => 'scliveticker_tick',
@ -90,14 +95,16 @@ class SCLiveticker_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' ) ),
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>';
@ -139,7 +146,13 @@ class SCLiveticker_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';
}

88
package-lock.json generated
View File

@ -1,88 +0,0 @@
{
"name": "wp-liveticker2",
"version": "1.0.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"cssesc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-1.0.1.tgz",
"integrity": "sha512-S2hzrpWvE6G/rW7i7IxJfWBYn27QWfOIncUW++8Rbo1VB5zsJDSVPcnI+Q8z7rhxT6/yZeLOCja4cZnghJrNGA=="
},
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"postcss-media-query-parser": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
"integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ="
},
"postcss-resolve-nested-selector": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
"integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4="
},
"postcss-selector-parser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-4.0.0.tgz",
"integrity": "sha512-5h+MvEjnzu1qy6MabjuoPatsGAjjDV9B24e7Cktjl+ClNtjVjmvAXjOFQr1u7RlWULKNGYaYVE4s+DIIQ4bOGA==",
"requires": {
"cssesc": "^1.0.1",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
"stylelint-config-recommended": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-2.1.0.tgz",
"integrity": "sha512-ajMbivOD7JxdsnlS5945KYhvt7L/HwN6YeYF2BH6kE4UCLJR0YvXMf+2j7nQpJyYLZx9uZzU5G1ZOSBiWAc6yA=="
},
"stylelint-config-recommended-scss": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-3.2.0.tgz",
"integrity": "sha512-M8BFHMRf8KNz5EQPKJd8nMCGmBd2o5coDEObfHVbEkyLDgjIf1V+U5dHjaGgvhm0zToUxshxN+Gc5wpbOOew4g==",
"requires": {
"stylelint-config-recommended": "^2.0.0"
}
},
"stylelint-config-wordpress": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/stylelint-config-wordpress/-/stylelint-config-wordpress-13.1.0.tgz",
"integrity": "sha512-dpKj2/d3/XjDVoOvQzd54GoM8Rj5zldluOZKkVhBCc4JYMc6r1VYL5hpcgIjqy/i2Hyqg4Rh7zTafE/2AWq//w==",
"requires": {
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.3.0"
}
},
"stylelint-scss": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.3.2.tgz",
"integrity": "sha512-0x+nD1heoMJYOfi3FfGcz3Hrwhcm+Qyq+BuvoBv5v9xrZZ1aziRXQauuhjwb87gWAa9MBzxhfUqBnvTUrHlLjA==",
"requires": {
"lodash": "^4.17.10",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-selector-parser": "^4.0.0",
"postcss-value-parser": "^3.3.0"
}
},
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
}
}
}

View File

@ -1,10 +1,13 @@
{
"name": "wp-liveticker",
"version": "1.0.0",
"name": "stklcode-liveticker",
"version": "1.2.2",
"description": "A simple Liveticker for Wordpress.",
"author": "Stefan Kalscheuer",
"license": "GPL-2.0+",
"dependencies": {
"stylelint-config-wordpress": "^13.1.0"
"devDependencies": {
"@wordpress/eslint-plugin": "^17",
"@wordpress/stylelint-config": "^21",
"eslint": "^8",
"stylelint": "^14"
}
}

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="4.7"/>
<rule ref="WordPress">
<exclude name="WordPress.VIP.SlowDBQuery.slow_db_query_tax_query"/>
<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.2-"/>
<rule ref="PHPCompatibility"/>
<config name="testVersion" value="5.6-"/>
<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>

190
scripts/block.js Normal file
View File

@ -0,0 +1,190 @@
/**
* stklcode-liveticker Gutenberg Block
*
* Gutenberg Block to integrate the liveticker widget without shortcode.
*/
( function() {
var __ = wp.i18n.__;
var registerBlockType = wp.blocks.registerBlockType;
var registerStore = wp.data.registerStore;
var withSelect = wp.data.withSelect;
var el = wp.element.createElement;
/**
* Datastore actions.
*/
var actions = {
setTickers: function( tickers ) {
return {
type: 'SET_TICKERS',
tickers: tickers,
};
},
getTickers: function( path ) {
return {
type: 'RECEIVE_TICKERS',
path: path,
};
},
};
registerStore( 'scliveticker/ticker', {
reducer: function( state, action ) {
if ( undefined === state ) {
state = { tickers: null };
}
switch ( action.type ) {
case 'SET_TICKERS':
state.tickers = action.tickers;
return state;
case 'RECEIVE_TICKERS':
return action.tickers;
}
return state;
},
actions: actions,
selectors: {
receiveTickers: function( state ) {
return state.tickers;
},
},
resolvers: {
receiveTickers: function() {
return wp.apiFetch( { path: '/wp/v2/scliveticker_ticker' } ).then( function( tickers ) {
return actions.setTickers( tickers.map( function( t ) {
return {
name: t.name,
slug: t.slug,
};
} ) );
} );
},
},
} );
registerBlockType( 'scliveticker/ticker', {
title: __( 'Liveticker', 'stklcode-liveticker' ),
icon: 'rss',
category: 'widgets',
keywords: [
__( 'Liveticker', 'stklcode-liveticker' ),
],
attributes: {
ticker: {
type: 'string',
default: '',
},
limit: {
type: 'number',
default: 5,
},
unlimited: {
type: 'boolean',
default: false,
},
},
edit: withSelect( function( select ) {
return {
tickers: select( 'scliveticker/ticker' ).receiveTickers(),
};
} )( function( props ) {
var label = [
el(
wp.components.Dashicon,
{ icon: 'rss' }
),
__( 'Liveticker', 'stklcode-liveticker' ),
];
var content;
if ( null === props.tickers ) {
// Tickers not yet loaded.
content = [
el(
'span',
{ className: 'components-base-control label' },
label
),
el( wp.components.Spinner ),
];
} else if ( 0 === props.tickers.length ) {
// No tickers available.
content = [
el(
'span',
{ className: 'components-base-control label' },
label
),
el( 'span', null, __( 'No tickers available', 'stklcode-liveticker' ) ),
];
} else {
// Tickers loaded and available.
if ( 0 === props.attributes.ticker.length && props.tickers.length > 0 ) {
props.attributes.ticker = props.tickers[ 0 ].slug;
}
content = [
el(
wp.components.SelectControl,
{
label: label,
value: props.attributes.ticker,
options: props.tickers.map( function( t ) {
return {
value: t.slug,
label: t.name,
};
} ),
onChange: function( val ) {
props.setAttributes( { ticker: val } );
},
}
),
el(
wp.components.TextControl,
{
label: __( 'Number of Ticks', 'stklcode-liveticker' ),
type: 'number',
min: 1,
step: 1,
disabled: props.attributes.unlimited,
value: props.attributes.limit,
onChange: function( val ) {
props.setAttributes( { limit: Number( val ) } );
},
}
),
el(
wp.components.CheckboxControl,
{
label: __( 'unlimited', 'stklcode-liveticker' ),
checked: props.attributes.unlimited,
onChange: function( val ) {
props.setAttributes( { unlimited: val } );
},
}
),
];
}
return el(
'div',
{ className: props.className + ' components-placeholder' },
content
);
} ),
save: function( props ) {
return el(
'div',
{
className: 'sclt-ajax',
'data-sclt-ticker': props.attributes.ticker,
'data-sclt-limit': props.attributes.unlimited ? 0 : props.attributes.limit,
'data-sclt-last': 0,
}
);
},
} );
}() );

View File

@ -1,154 +1,251 @@
/**
* Contructor of the scLiveticker object.
*
* @constructor
* @class
*/
function scLiveticker() {
}
( function() {
var apiURL;
var pollInterval;
var ticker;
/**
* Initialize iveticker JS component.
*
* @return {void}
*/
scLiveticker.init = function() {
/**
* Initialize iveticker JS component.
*
* @return {void}
*/
var init = function() {
var updateNow = false;
var c = 0;
// Opt out if AJAX pobject not present.
if ( 'undefined' === typeof sclivetickerAjax ) {
return;
}
// Extract AJAX settings.
scLiveticker.ajaxURL = sclivetickerAjax.ajax_url;
scLiveticker.nonce = sclivetickerAjax.nonce;
scLiveticker.pollInterval = sclivetickerAjax.poll_interval;
// Get ticker elements.
scLiveticker.ticker = [].map.call(
document.querySelectorAll( 'ul.sclt-ticker-ajax' ),
function( elem ) {
return {
s: elem.getAttribute( 'data-sclt-ticker' ),
l: elem.getAttribute( 'data-sclt-limit' ),
t: elem.getAttribute( 'data-sclt-last' ),
e: elem
};
// Opt out if AJAX pobject not present.
if ( 'undefined' === typeof scliveticker ) {
return;
}
);
// Get widget elements.
scLiveticker.widgets = [].map.call(
document.querySelectorAll( 'ul.sclt-widget-ajax' ),
function( elem ) {
return {
w: elem.getAttribute( 'data-sclt-ticker' ),
l: elem.getAttribute( 'data-sclt-limit' ),
t: elem.getAttribute( 'data-sclt-last' ),
e: elem
};
}
);
// Extract settings.
apiURL = scliveticker.api + 'wp/v2/scliveticker_tick';
pollInterval = scliveticker.poll_interval;
// Trigger update, if necessary.
if ( ( 0 < scLiveticker.ticker.length || scLiveticker.widgets.length ) && 0 < scLiveticker.pollInterval ) {
setTimeout( scLiveticker.update, scLiveticker.pollInterval );
}
};
/**
* Update liveticker on current page via AJAX call.
*
* @return {void}
*/
scLiveticker.update = function() {
// Extract ticker-slug, limit and timestamp of last poll.
var updateReq = 'action=sclt_update-ticks&_ajax_nonce=' + scLiveticker.nonce;
var i, j;
var xhr = new XMLHttpRequest();
for ( i = 0; i < scLiveticker.ticker.length; i++ ) {
updateReq = updateReq +
'&update[' + i + '][s]=' + scLiveticker.ticker[ i ].s +
'&update[' + i + '][l]=' + scLiveticker.ticker[ i ].l +
'&update[' + i + '][t]=' + scLiveticker.ticker[ i ].t;
}
for ( j = 0; j < scLiveticker.widgets.length; j++ ) {
updateReq = updateReq +
'&update[' + ( i + j ) + '][w]=' + scLiveticker.widgets[ j ].w +
'&update[' + ( i + j ) + '][l]=' + scLiveticker.widgets[ j ].l +
'&update[' + ( i + j ) + '][t]=' + scLiveticker.widgets[ j ].t;
}
// Issue AJAX request.
xhr.open( 'POST', scLiveticker.ajaxURL, true );
xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded;' );
xhr.onreadystatechange = function() {
var update;
if ( XMLHttpRequest.DONE === this.readyState && 200 === this.status ) {
try {
update = JSON.parse( this.responseText );
if ( update ) {
update.forEach(
function( u ) {
scLiveticker.ticker.forEach(
function( t ) {
if ( t.s === u.s ) {
t.t = u.t; // Update last poll timestamp.
scLiveticker.updateHTML( t, u ); // Update HTML markup.
}
}
);
scLiveticker.widgets.forEach(
function( t ) {
if ( t.w === u.w ) {
t.t = u.t;
scLiveticker.updateHTML( t, u );
}
}
);
}
);
// Get ticker elements.
ticker = [].map.call(
document.querySelectorAll( 'div.wp-block-scliveticker-ticker.sclt-ajax' ),
function( elem ) {
var o = parseElement( elem, false, ++c );
if ( '0' === o.lastPoll ) {
updateNow = true;
}
setTimeout( scLiveticker.update, scLiveticker.pollInterval ); // Re-trigger update.
} catch ( e ) {
console.warn( 'Liveticker AJAX update failed, stopping automatic updates.' );
return o;
}
);
// Get widget elements.
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;
}
)
);
// Trigger update, if necessary.
if ( ( 0 < ticker.length ) && 0 < pollInterval ) {
if ( updateNow ) {
update();
} else {
setTimeout( update, pollInterval );
}
}
};
xhr.send( updateReq );
};
/**
* 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.
* @return {void}
*/
scLiveticker.updateHTML = function( t, u ) {
/**
* 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' );
// Prepend HTML of new ticks.
t.e.innerHTML = u.h + t.e.innerHTML;
t.e.setAttribute( 'data-sclt-last', u.t );
elem.id = 'sclt-' + n;
// Remove tail, if limit is set.
if ( 0 < t.l ) {
[].slice.call( t.e.getElementsByTagName( 'li' ), t.l ).forEach(
function( li ) {
li.remove();
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' );
}
}
);
}
return {
id: n,
ticker: elem.getAttribute( 'data-sclt-ticker' ),
limit: elem.getAttribute( 'data-sclt-limit' ),
lastPoll: last,
ticks: list,
isWidget: widget,
updating: false,
};
};
/**
* Update liveticker on current page via REST API call.
*
* @return {void}
*/
var update = function() {
// Iterate over available tickers.
ticker.forEach(
function( t ) {
var xhr, query;
if ( t.updating ) {
// Do not update twice.
return;
}
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.' );
}
}
);
xhr.send();
}
);
}
};
document.addEventListener(
'DOMContentLoaded',
function() {
scLiveticker.init(); // Trigger periodic update of livetickers.
}
);
// Re-trigger update.
setTimeout( update, pollInterval );
};
/**
* Do actual update of HTML code.
*
* @param {Object} t Ticker or Widget reference.
* @param {Object} u Update entity.
* @return {void}
*/
var addTick = function( t, u ) {
// Parse new DOM-part.
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 = [];
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;
// 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 {
// Prepend new tick 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 ) {
[].slice.call( t.ticks.getElementsByTagName( 'li' ), t.limit ).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(
'DOMContentLoaded',
function() {
init(); // Trigger periodic update of livetickers.
}
);
}() );

View File

@ -9,7 +9,7 @@
* @wordpress-plugin
* Plugin Name: Liveticker (by stklcode)
* Description: A simple Liveticker for WordPress.
* Version: 1.0.0
* Version: 1.2.2
* Author: Stefan Kalscheuer
* Author URI: https://www.stklcode.de
* Text Domain: stklcode-liveticker
@ -41,19 +41,22 @@ define( 'SCLIVETICKER_BASE', plugin_dir_url( __FILE__ ) );
define( 'SCLIVETICKER_BASENAME', plugin_basename( __FILE__ ) );
// System Hooks.
add_action( 'init', array( 'SCLiveticker', 'register_types' ) );
add_action( 'plugins_loaded', array( 'SCLiveticker', 'init' ) );
register_activation_hook( SCLIVETICKER_FILE, array( 'SCLiveticker_System', 'activate' ) );
register_uninstall_hook( SCLIVETICKER_FILE, array( 'SCLiveticker_System', 'uninstall' ) );
add_action( 'init', array( 'SCLiveticker\\SCLiveticker', 'register_types' ) );
add_action( 'plugins_loaded', array( 'SCLiveticker\\SCLiveticker', 'init' ) );
register_activation_hook( SCLIVETICKER_FILE, array( 'SCLiveticker\\System', 'activate' ) );
register_uninstall_hook( SCLIVETICKER_FILE, array( 'SCLiveticker\\System', 'uninstall' ) );
// Allow shortcodes in widgets.
add_filter( 'widget_text', 'do_shortcode' );
// Add shortcode.
add_shortcode( 'liveticker', array( 'SCLiveticker', 'shortcode_ticker_show' ) );
add_shortcode( 'liveticker', array( 'SCLiveticker\\SCLiveticker', 'shortcode_ticker_show' ) );
// Add Widget.
add_action( 'widgets_init', array( 'SCLiveticker_Widget', 'register' ) );
add_action( 'widgets_init', array( 'SCLiveticker\\Widget', 'register' ) );
// Add Gutenberg block.
add_action( 'enqueue_block_editor_assets', array( 'SCLiveticker\\Admin', 'register_block' ) );
// Autoload.
spl_autoload_register( 'scliveticker_autoload' );
@ -61,22 +64,23 @@ 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( $class_name ) {
$plugin_classes = array(
'SCLiveticker',
'SCLiveticker_Admin',
'SCLiveticker_System',
'SCLiveticker_Widget',
'SCLiveticker\\SCLiveticker',
'SCLiveticker\\Admin',
'SCLiveticker\\Api',
'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( '_', '-', $class ) )
strtolower( str_replace( '_', '-', substr( $class_name, 13 ) ) )
);
}
}

9
styles/block.css Normal file
View File

@ -0,0 +1,9 @@
div.wp-block-scliveticker-ticker div.components-base-control:first-child label,
div.wp-block-scliveticker-ticker .label {
font-weight: 600;
}
div.wp-block-scliveticker-ticker .label > .dashicon,
div.wp-block-scliveticker-ticker div.components-base-control:first-child label > .dashicon {
margin-right: 8px;
}

View File

@ -1,8 +1,10 @@
ul.sclt-ticker {
div.wp-block-scliveticker-ticker ul,
div.wp-block-scliveticker-ajax-ticker ul {
list-style-type: none;
}
ul.sclt-ticker > li.sclt-tick {
div.wp-block-scliveticker-ticker ul > li.sclt-tick,
div.wp-block-scliveticker-ajax-ticker ul > li.sclt-tick {
background-color: #f5f5f5;
list-style-type: none;
margin: 0.1em;

38
tests/bootstrap.php Normal file
View File

@ -0,0 +1,38 @@
<?php
/**
* PHPUnit bootstrap file.
*
* @package SCLiveticker
*/
$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}
// Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file.
$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' );
if ( false !== $_phpunit_polyfills_path ) {
define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path );
}
if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) {
echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit( 1 );
}
// Give access to tests_add_filter() function.
require_once "{$_tests_dir}/includes/functions.php";
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( dirname( __FILE__ ) ) . '/stklcode-liveticker.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

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 WP_UnitTestCase;
/**
* Class Test_API.
*/
class Test_API extends WP_UnitTestCase {
/**
* Initialize WP REST API for tests.
*
* @return void
*/
public function set_up() {
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() {
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() {
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

@ -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 ); ?>" />
@ -27,12 +27,12 @@ if ( ! defined( 'ABSPATH' ) ) {
<td>
<select id="<?php echo esc_attr( $this->get_field_id( 'category' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'category' ) ); ?>">
<?php
foreach ( $categories as $cat ) {
echo '<option value="' . esc_attr( $cat->slug ) . '"';
if ( $category === $cat->slug ) {
foreach ( $categories as $c ) {
echo '<option value="' . esc_attr( $c->slug ) . '"';
if ( $category === $c->slug ) {
echo ' selected="selected"';
}
echo '>' . esc_html( $cat->name ) . '</option>';
echo '>' . esc_html( $c->name ) . '</option>';
}
?>
</select>
@ -40,7 +40,7 @@ if ( ! defined( 'ABSPATH' ) ) {
</tr>
<tr>
<td>
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of Ticks:', 'stklcode-liveticker' ); ?></label>
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of Ticks', 'stklcode-liveticker' ); ?>:</label>
</td>
<td>
<select id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>">
@ -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 )
);