Compare commits

...

137 Commits

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-19 11:55:08 +01:00
b907d2dc89 add PHP 8.0 to CI roster + minor dev dependency updates
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-06 12:29:30 +01:00
7817f460d2 declare compatibility with WP 5.5
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-27 18:33:54 +01:00
0b53709849 drop PHP 7.2 from CI builds and add nightly 2020-09-09 09:52:18 +02:00
7b57b59861 use Node v12 for CI builds 2020-09-09 09:45:14 +02:00
790ef303bb update devenv 2020-09-09 09:40:35 +02:00
3511146c98 docs: update Travis CI badge 2020-09-09 09:39:27 +02:00
c7d7d27f1c add missing textdomain to taxonomy name 2020-05-22 11:28:54 +02:00
dc366abcc4 Merge branch 'master' into develop 2020-05-02 19:32:01 +02:00
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
43 changed files with 1936 additions and 1206 deletions

23
.distignore Normal file
View File

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

View File

@ -1,38 +1,28 @@
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:22
commands:
- ./vendor/bin/robo test:cs
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 ]
- npm install
- name: lint-assets
image: node:22
commands:
- npx eslint scripts/block.js
- npx eslint scripts/liveticker.js
- npx stylelint styles/block.css
- npx stylelint styles/liveticker.css
depends_on:
- node-install

View File

@ -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"
}
}
]
}

20
.gitattributes vendored Normal file
View File

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

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

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

View File

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

View File

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

View File

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

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,13 +1,19 @@
[![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
* Contributors: stklcode
* 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: 5.0
* Tested up to: 6.7
* Requires PHP: 7.2
* Stable tag: 1.3.0
* License: GPLv2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
A simple ajaxified liveticker plugin for WordPress.
@ -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 7.2 or above
* WordPress 5.0 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,52 @@ 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.3.0 - 2025-03-10
* Requires at least PHP 7.2 and WordPress 5.0
* Sorting direction can now be changed for liveticker blocks
### 1.2.3 - 2025-02-04
* Escape ticker ID in shortcode output
* Tested with WP 6.7
### 1.2.2 - 2024-03-02
* Update use of deprecated WP core functions
* Extend output sanitization
* Tested with WP 6.4
### 1.2.1 - 2022-02-01
* Fix issue with limit in Gutenberg block.
### 1.2.0 - 2022-01-23
* Requires WordPress 4.7 or above
* Migrated AJAX to REST API
* Resolved Javascript compatibility issues with IE11
* Added optional shortcode support for tick content
* Support embedded JavaScript execution in tick content (e.g. for social media integrations)
### 1.1.1 - 2021-03-20
* "Ticker" taxonomy name is now translatable
### 1.1.0 - 2020-05-02
* Requires PHP 5.6 or above
* 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: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 58 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: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/screenshot-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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

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

View File

@ -1,6 +1,6 @@
{
"name": "stklcode/wp-liveticker",
"version": "1.0.0",
"name": "stklcode/stklcode-liveticker",
"version": "1.3.0",
"description": "A simple Liveticker for Wordpress.",
"keywords": [
"wordpress",
@ -17,23 +17,17 @@
],
"type": "wordpress-plugin",
"require": {
"php": ">=5.2",
"composer/installers": "~1.0"
"php": ">=7.2",
"composer/installers": "~v2.3.0"
},
"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",
"slowprog/composer-copy-file": "~0.3",
"squizlabs/php_codesniffer": "^3.11",
"wp-coding-standards/wpcs": "^3.1",
"yoast/wp-test-utils": "^1.2"
},
"scripts": {
"post-install-cmd": [
@ -43,39 +37,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
}
]
}
}

81
includes/class-admin.php Normal file
View File

@ -0,0 +1,81 @@
<?php
/**
* Liveticker: Plugin admin class.
*
* This file contains the derived class for the plugin's administration features.
*
* @package SCLiveticker
*/
namespace SCLiveticker;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Liveticker admin configuration.
*/
class Admin extends SCLiveticker {
/**
* Add to Right Now Widget
*
* @return void
*/
public static function dashboard_right_now(): void {
$total_files = wp_count_posts( 'scliveticker_tick' );
echo '<tr>';
echo '<td class="first b b-tags"><a href="edit.php?post_type=scliveticker_tick">' . esc_html( $total_files->publish ) . '</a></td>';
echo '<td class="t tags"><a href="edit.php?post_type=scliveticker_tick">' . esc_html__( 'Ticks', 'stklcode-liveticker' ) . '</a></td>';
echo '</tr>';
}
/**
* Register settings page.
*
* @return void
*/
public static function register_settings_page(): void {
add_submenu_page(
'edit.php?post_type=scliveticker_tick',
'Liveticker ' . __( 'Settings', 'stklcode-liveticker' ),
__( 'Settings', 'stklcode-liveticker' ),
'manage_options',
'scliveticker_settings',
array( Settings::class, 'render_settings_page' )
);
}
/**
* Register custom Gutenberg block type.
*
* @return void
* @since 1.1
*/
public static function register_block(): void {
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',
)
);
}
}

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

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

View File

@ -1,205 +0,0 @@
<?php
/**
* Liveticker: Plugin admin class.
*
* This file contains the derived class for the plugin's administration features.
*
* @package Liveticker
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Liveticker admin configuration.
*/
class SCLiveticker_Admin extends SCLiveticker {
/**
* Add to Right Now Widget
*
* @return void
*/
public static function dashboard_right_now() {
$total_files = wp_count_posts( 'scliveticker_tick' );
echo '<tr>';
echo '<td class="first b b-tags"><a href="edit.php?post_type=scliveticker_tick">' . esc_html( $total_files->publish ) . '</a></td>';
echo '<td class="t tags"><a href="edit.php?post_type=scliveticker_tick">' . esc_html__( 'Ticks', 'stklcode-liveticker' ) . '</a></td>';
echo '</tr>';
}
/**
* Register settings page.
*
* @return void
*/
public static function register_settings_page() {
add_submenu_page(
'edit.php?post_type=scliveticker_tick',
'Liveticker ' . __( 'Settings', 'stklcode-liveticker' ),
__( 'Settings', 'stklcode-liveticker' ),
'manage_options',
'scliveticker_settings',
array(
__CLASS__,
'settings_page',
)
);
}
/**
* Register settings API
*
* @return void
*/
public static function register_settings() {
register_setting(
'scliveticker_settings',
self::OPTION,
array( __CLASS__, 'validate_settings' )
);
// Form sections.
add_settings_section(
'scliveticker_settings_general',
__( 'General', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_general_section' ),
'scliveticker-settings-page'
);
// Form fields.
add_settings_field(
'enable_ajax',
__( 'Use AJAX', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_ajax_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-ajax' )
);
add_settings_field(
'poll_interval',
__( 'AJAX poll interval', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_poll_interval_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-poll-interval' )
);
add_settings_field(
'enable_css',
__( 'Default CSS Styles', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_enable_css_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-enable-css' )
);
add_settings_field(
'show_feed',
__( 'Show RSS feed', 'stklcode-liveticker' ),
array( __CLASS__, 'settings_show_feed_field' ),
'scliveticker-settings-page',
'scliveticker_settings_general',
array( 'label_for' => esc_attr( self::OPTION ) . '-show-feed' )
);
}
/**
* Render general section.
*
* @return void
*/
public static function settings_general_section() {
}
/**
* Render uninstall section.
*
* @return void
*/
public static function settings_uninstall_section() {
}
/**
* Render enable AJAX field.
*
* @return void
*/
public static function settings_enable_ajax_field() {
$checked = self::$_options['enable_ajax'];
echo '<input id="' . esc_attr( self::OPTION ) . '-enable-ajax" type="checkbox" name="' . esc_attr( self::OPTION ) . '[enable_ajax]" value="1" ' . checked( $checked, 1, false ) . '> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Disable this option to not use AJAX update. This means all liveticker widgets and shortcodes are only updated once on site load.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render AJAX poll interval field.
*
* @return void
*/
public static function settings_poll_interval_field() {
$poll_interval = self::$_options['poll_interval'];
echo '<input id="' . esc_attr( self::OPTION ) . '-poll-interval" type="number" name="' . esc_attr( self::OPTION ) . '[poll_interval]" value="' . esc_attr( $poll_interval ) . '"> ';
esc_html_e( 'seconds', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Interval (in seconds) to update ticker if AJAX is enabled.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render enable css field.
*
* @return void
*/
public static function settings_enable_css_field() {
$checked = self::$_options['enable_css'];
echo '<input id="' . esc_attr( self::OPTION ) . '-enable-css" type="checkbox" name="' . esc_attr( self::OPTION ) . '[enable_css]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Disable this option to remove the default styling CSS file.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render enable css field.
*
* @return void
*/
public static function settings_show_feed_field() {
$checked = self::$_options['show_feed'];
echo '<input id="' . esc_attr( self::OPTION ) . '-show-feed" type="checkbox" name="' . esc_attr( self::OPTION ) . '[show_feed]" value="1" ' . checked( $checked, 1, false ) . ' /> ';
esc_html_e( 'Enable', 'stklcode-liveticker' );
echo '<p class="description">' . esc_html__( 'Can be overwritten in shortcode.', 'stklcode-liveticker' ) . '</p>';
}
/**
* Render the settings page.
*
* @return void
*/
public static function settings_page() {
include SCLIVETICKER_DIR . 'views/settings-page.php';
}
/**
* Validate settings callback.
*
* @param array $input Input arguments.
*
* @return array Parsed arguments.
*/
public static function validate_settings( $input ) {
$defaults = self::default_options();
$result['enable_ajax'] = isset( $input['enable_ajax'] ) ? intval( $input['enable_ajax'] ) : 0;
$result['poll_interval'] = isset( $input['poll_interval'] ) ? intval( $input['poll_interval'] ) : $defaults['poll_interval'];
$result['enable_css'] = isset( $input['enable_css'] ) ? intval( $input['enable_css'] ) : 0;
$result['show_feed'] = isset( $input['show_feed'] ) ? intval( $input['show_feed'] ) : 0;
return $result;
}
}

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.3.0';
/**
* 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.
@ -57,7 +62,7 @@ class SCLiveticker {
*
* @return void
*/
public static function init() {
public static function init(): void {
// Skip on autosave.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
@ -66,13 +71,17 @@ 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;
}
// Load Textdomain.
load_plugin_textdomain( 'stklcode-liveticker', false );
load_plugin_textdomain( 'stklcode-liveticker' );
// Allow shortcodes in widgets.
add_filter( 'widget_text', 'do_shortcode' );
@ -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\\Settings', 'register_settings' ) );
add_action( 'admin_menu', array( 'SCLiveticker\\Admin', 'register_settings_page' ) );
}
}
@ -108,11 +114,11 @@ class SCLiveticker {
*
* @return void
*/
public static function register_types() {
public static function register_types(): void {
// Add new taxonomy, make it hierarchical (like categories).
$labels = array(
'name' => _x( 'Ticker', 'taxonomy general name' ),
'singular_name' => _x( 'Ticker', 'taxonomy singular name' ),
'name' => _x( 'Ticker', 'taxonomy general name', 'stklcode-liveticker' ),
'singular_name' => _x( 'Ticker', 'taxonomy singular name', 'stklcode-liveticker' ),
'search_items' => __( 'Search Tickers', 'stklcode-liveticker' ),
'all_items' => __( 'All Tickers', 'stklcode-liveticker' ),
'parent_item' => __( 'Parent Ticker', 'stklcode-liveticker' ),
@ -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 );
@ -174,7 +182,7 @@ class SCLiveticker {
*
* @return string
*/
public static function shortcode_ticker_show( $atts ) {
public static function shortcode_ticker_show( array $atts ): string {
// Indicate presence of shortcode (to enqueue styles/scripts later).
self::$shortcode_present = true;
@ -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,20 +219,32 @@ class SCLiveticker {
$wp_query = new WP_Query( $args );
$ticks = '';
$last = null;
while ( $wp_query->have_posts() ) {
$wp_query->the_post();
$output .= self::tick_html( get_the_time( 'd.m.Y H.i' ), get_the_title(), get_the_content() );
$ticks .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content(), get_the_ID() );
if ( is_null( $last ) ) {
$last = get_post_modified_time( 'Y-m-d\TH:i:s', true );
}
}
$output .= '</ul>';
$output = '<div class="wp-block-scliveticker-ticker';
if ( 1 === self::$options['enable_ajax'] ) {
$output .= ' sclt-ajax" '
. 'data-sclt-ticker="' . esc_attr( $ticker ) . '" '
. 'data-sclt-limit="' . $limit . '" '
. 'data-sclt-last="' . $last;
}
$output .= '"><ul>' . $ticks . '</ul></div>';
// Show RSS feed link, if configured.
if ( $show_feed ) {
$feed_link = get_post_type_archive_feed_link( 'scliveticker_tick' ) . '';
if ( false === strpos( $feed_link, '&' ) ) {
$feed_link .= '?scliveticker_ticker=' . $ticker;
$feed_link .= '?scliveticker_ticker=' . rawurlencode( $ticker );
} else {
$feed_link .= '&scliveticker_ticker=' . $ticker;
$feed_link .= '&scliveticker_ticker=' . rawurlencode( $ticker );
}
$output .= '<a href="' . esc_attr( $feed_link ) . '">Feed</a>';
}
@ -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(): void {
// Only add if shortcode is present.
if ( self::$shortcode_present || self::$widget_present ) {
if ( self::$shortcode_present || self::$widget_present || has_block( 'scliveticker/ticker' ) ) {
wp_enqueue_script(
'scliveticker-js',
SCLIVETICKER_BASE . 'scripts/liveticker.min.js',
@ -279,13 +273,25 @@ 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
);
}
}
}
@ -294,14 +300,14 @@ class SCLiveticker {
*
* @return void
*/
public static function ajax_update() {
public static function ajax_update(): void {
// Verify AJAX nonce.
check_ajax_referer( 'scliveticker_update-ticks' );
// Extract update requests.
if ( isset( $_POST['update'] ) && is_array( $_POST['update'] ) ) { // Input var okay.
$res = array();
// @codingStandardsIgnoreLine Sanitization of arrayhandled on field level.
// @codingStandardsIgnoreLine Sanitization of array handled on field level.
foreach ( wp_unslash( $_POST['update'] ) as $update_req ) {
if ( is_array( $update_req ) && ( isset( $update_req['s'] ) || isset( $update_req['w'] ) ) ) {
if ( isset( $update_req['s'] ) ) {
@ -311,12 +317,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 +342,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 +360,9 @@ class SCLiveticker {
while ( $query->have_posts() ) {
$query->the_post();
if ( $is_widget ) {
$out .= self::tick_html_widget( get_the_time( 'd.m.Y H.i' ), get_the_title(), false );
$out .= self::tick_html_widget( get_the_time( 'd.m.Y H:i' ), get_the_title(), false, get_the_ID() );
} else {
$out .= self::tick_html( get_the_time( 'd.m.Y H.i' ), get_the_title(), get_the_content(), $is_widget );
$out .= self::tick_html( get_the_time( 'd.m.Y H:i' ), get_the_title(), get_the_content(), get_the_ID() );
}
}
@ -350,13 +370,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(),
);
}
}
@ -373,19 +393,20 @@ class SCLiveticker {
*
* @return void
*/
public static function mark_widget_present() {
public static function mark_widget_present(): void {
self::$widget_present = true;
}
/**
* Update options.
*
* @param array $options Optional. New options to save.
*
* @return void
*
* @since 1.0.0
* @since 1.3.0 removed unused parameter
*/
protected static function update_options( $options = null ) {
self::$_options = wp_parse_args(
protected static function update_options(): void {
self::$options = wp_parse_args(
get_option( self::OPTION ),
self::default_options()
);
@ -396,31 +417,37 @@ class SCLiveticker {
*
* @return array The options array.
*/
protected static function default_options() {
protected static function default_options(): array {
return array(
'enable_ajax' => 1,
'poll_interval' => 60,
'enable_css' => 1,
'show_feed' => 0,
'reset_settings' => 0,
'enable_ajax' => 1,
'poll_interval' => 60,
'enable_css' => 1,
'show_feed' => 0,
'enable_shortcode' => 0,
'embedded_script' => 0,
'reset_settings' => 0,
);
}
/**
* Generate HTML code for a tick element.
*
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param string $content Tick content.
* @param boolean $is_widget Is the code for Widget.
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param string $content Tick content.
* @param integer $id Tick ID.
*
* @return string HTML code of tick.
*/
private static function tick_html( $time, $title, $content, $is_widget = false ) {
return '<li class="sclt-tick">'
. '<p><span class="sclt-tick_time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-tick-title">' . esc_html( $title ) . '</span></p>'
. '<p class="sclt-tick-content">' . $content . '</p></li>';
private static function tick_html( string $time, string $title, string $content, int $id ): string {
if ( self::$options['enable_shortcode'] ) {
$content = do_shortcode( $content );
}
return '<li class="sclt-tick" data-sclt-tick-id="' . esc_attr( $id ) . '">'
. '<span class="sclt-tick-time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-tick-title">' . esc_html( $title ) . '</span>'
. '<div class="sclt-tick-content">' . $content . '</div></li>';
}
/**
@ -429,14 +456,18 @@ class SCLiveticker {
* @param string $time Tick time (readable).
* @param string $title Tick title.
* @param boolean $highlight Highlight element.
* @param integer $id Tick ID.
*
* @return string HTML code of widget tick.
*/
public static function tick_html_widget( $time, $title, $highlight ) {
public static function tick_html_widget( string $time, string $title, bool $highlight, int $id = 0 ): string {
$out = '<li';
if ( $highlight ) {
$out .= ' class="sclt-widget-new"';
}
if ( $id > 0 ) {
$out .= ' data-sclt-tick-id="' . esc_attr( $id ) . '"';
}
return $out . '>'
. '<span class="sclt-widget-time">' . esc_html( $time ) . '</span>'
. '<span class="sclt-widget-title">' . $title . '</span>'

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

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

View File

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

View File

@ -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.
@ -26,7 +31,7 @@ class SCLiveticker_Widget extends WP_Widget {
/**
* Register the widget.
*/
public static function register() {
public static function register(): void {
register_widget( __CLASS__ );
}
@ -44,10 +49,10 @@ class SCLiveticker_Widget extends WP_Widget {
SCLiveticker::mark_widget_present();
$instance = self::fill_options_with_defaults( $instance );
$before_widget = isset( $args['before_widget'] ) ? $args['before_widget'] : '';
$after_widget = isset( $args['after_widget'] ) ? $args['after_widget'] : '';
$before_title = isset( $args['before_title'] ) ? $args['before_title'] : '';
$after_title = isset( $args['after_title'] ) ? $args['after_title'] : '';
$before_widget = $args['before_widget'] ?? '';
$after_widget = $args['after_widget'] ?? '';
$before_title = $args['before_title'] ?? '';
$after_title = $args['after_title'] ?? '';
$title = apply_filters( 'scliveticker_catlit', $instance['title'] );
$category = apply_filters( 'scliveticker_catlit', $instance['category'] );
$count = apply_filters( 'scliveticker_catlit', $instance['count'] );
@ -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';
}
@ -151,7 +164,7 @@ class SCLiveticker_Widget extends WP_Widget {
*
* @return array Complete instance configuration.
*/
private static function fill_options_with_defaults( $instance ) {
private static function fill_options_with_defaults( array $instance ): array {
$default = array(
'title' => '',
'category' => '',

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.3.0",
"description": "A simple Liveticker for Wordpress.",
"author": "Stefan Kalscheuer",
"license": "GPL-2.0+",
"dependencies": {
"stylelint-config-wordpress": "^13.1.0"
"devDependencies": {
"@wordpress/eslint-plugin": "^22",
"@wordpress/stylelint-config": "^23",
"eslint": "^8",
"stylelint": "^16"
}
}

View File

@ -12,12 +12,19 @@
<file>views</file>
<!-- Compliance with WordPress Coding Standard -->
<config name="minimum_supported_wp_version" value="4.0"/>
<config name="minimum_supported_wp_version" value="5.0"/>
<rule ref="WordPress">
<exclude name="WordPress.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="7.2-"/>
<rule ref="PHPCompatibilityWP"/>
</ruleset>

View File

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

215
scripts/block.js Normal file
View File

@ -0,0 +1,215 @@
/**
* 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,
},
sort: {
type: 'string',
// implicit default: 'desc', left empty here for backwards compatibility of the block
},
},
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 } );
},
}
),
el(
wp.components.SelectControl,
{
label: __( 'Output direction', 'stklcode-liveticker' ),
value: props.attributes.sort,
options: [
{
value: 'desc',
label: __( 'newest first', 'stklcode-liveticker' ),
},
{
value: 'asc',
label: __( 'oldest first', 'stklcode-liveticker' ),
},
],
onChange: function( val ) {
props.setAttributes( { sort: val } );
},
}
),
];
}
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,
'data-sclt-sort': props.attributes.sort,
}
);
},
} );
}() );

View File

@ -1,154 +1,265 @@
/**
* Contructor of the scLiveticker object.
* Constructor 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' );
var sort = elem.getAttribute( 'data-sclt-sort' );
// 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' );
}
}
);
}
if ( 'asc' !== sort && 'desc' !== 'sort' ) {
sort = 'desc';
}
return {
id: n,
ticker: elem.getAttribute( 'data-sclt-ticker' ),
limit: elem.getAttribute( 'data-sclt-limit' ),
lastPoll: last,
sort: sort,
ticks: list,
isWidget: widget,
updating: false,
};
};
/**
* Update liveticker on current page via REST API call.
*
* @return {void}
*/
var update = function() {
// 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 if ( 'asc' === t.sort ) {
// Append new tick as last element to container.
t.ticks.appendChild( li );
} else {
// Prepend new tick as fist element to container.
t.ticks.insertBefore( li, t.ticks.firstChild );
}
// Update last poll time.
t.lastPoll = u.date_gmt;
t.ticks.parentNode.setAttribute( 'data-sclt-last', u.date_gmt );
// Remove tail, if limit is set.
if ( 0 < t.limit && t.limit < t.ticks.children.length ) {
if ( 'asc' === t.sort ) {
old = [].slice.call( t.ticks.children, 0, -t.limit );
} else {
old = [].slice.call( t.ticks.children, t.limit );
}
old.forEach(
function( l ) {
l.remove();
}
);
}
// Evaluate embedded inline scripts.
// Directly evaluate script otherwise.
scripts.forEach( function( script ) {
try {
// eslint-disable-next-line no-eval
eval( script.innerHTML );
} catch ( e ) {
// eslint-disable-next-line no-console
console.warn( 'Failed to evaluate embedded script.' );
}
} );
};
document.addEventListener(
'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.3.0
* Author: Stefan Kalscheuer
* Author URI: https://www.stklcode.de
* Text Domain: stklcode-liveticker
@ -26,7 +26,7 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Liveticker (by stklcode). If not, see http://www.gnu.org/licenses/gpl-2.0.html.
* along with Liveticker (by stklcode). If not, see https://www.gnu.org/licenses/gpl-2.0.html.
*/
// Exit if accessed directly.
@ -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,24 @@ spl_autoload_register( 'scliveticker_autoload' );
/**
* Autoloader for Liveticker classes.
*
* @param string $class Name of the class to load.
* @param string $class_name Name of the class to load.
*
* @return void
*/
function scliveticker_autoload( $class ) {
function scliveticker_autoload( string $class_name ): void {
$plugin_classes = array(
'SCLiveticker',
'SCLiveticker_Admin',
'SCLiveticker_System',
'SCLiveticker_Widget',
'SCLiveticker\\SCLiveticker',
'SCLiveticker\\Admin',
'SCLiveticker\\Api',
'SCLiveticker\\Settings',
'SCLiveticker\\System',
'SCLiveticker\\Widget',
);
if ( in_array( $class, $plugin_classes, true ) ) {
if ( in_array( $class_name, $plugin_classes, true ) ) {
require_once sprintf(
'%s/includes/class-%s.php',
SCLIVETICKER_DIR,
strtolower( str_replace( '_', '-', $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;

29
tests/bootstrap.php Normal file
View File

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

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

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

View File

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

View File

@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<table>
<tr>
<td>
<label for="<?php echo esc_html( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:' ); ?></label>
<label for="<?php echo esc_html( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'stklcode-liveticker' ); ?></label>
</td>
<td>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
@ -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 )
);