Release/v3.0.0 (#200)

* Bump WP version

* ♻️ Initial Refactor commit [APP-687] (#109)

* Initial refactor commit

*  Added build and tests CI/CD

* PR Rejects

* Rejects leftover

* Setup base (#110)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: settings variable

* update: removed duplicate css import

* Update modules/settings/assets/js/api/index.js

Co-authored-by: VasylD <vasyld@elementor.red>

---------

Co-authored-by: Ohad <ohad@elementor.com>
Co-authored-by: VasylD <vasyld@elementor.red>

* [Infra]  updated Github actions (#114)

* updated github actions

* removed composer github auth

* PHPCS

* removed package-lock.json from ignore to allow `npm ci`

* added missing husky

* ignore legacy

* removed unused non existing import

* Add connect modal (#111)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: add rule to move jsx props to multiline imporving readability

* add: connect modal

* update: hooks import for better readability

* update: replace functions with hooks

* fix: alignment and style

* update: imports

* update: removed conflicting imports

* fix: add compatibility for mobile devices

---------

Co-authored-by: Ohad <ohad@elementor.com>

* [APP 705] add connect module, settings and notification component (#112)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: add rule to move jsx props to multiline imporving readability

* add: connect modal

* update: hooks import for better readability

* update: replace functions with hooks

* add: connect module

* add: settings and get settings route

* add: hooks and contexts to get settings

* add: hooks

* add: notification component

* add: data api

* add: settings provider and connect settings

* add: husky

* fix: formatting and text-domain

* update: filter names

* fix: hook import

* add: set function for settings

* add: prop-types package

* update: refactor notification component and context

* update: remove filter for authorize url

* update: imports and exports of hooks

* update: plugin settings context filename and relevant imports

---------

Co-authored-by: Ohad <ohad@elementor.com>

* [APP 707] general setting components (#113)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: add rule to move jsx props to multiline imporving readability

* add: connect modal

* update: hooks import for better readability

* update: replace functions with hooks

* add: connect module

* add: settings and get settings route

* add: hooks and contexts to get settings

* add: hooks

* add: notification component

* add: data api

* add: settings provider and connect settings

* add: husky

* add: icon size control

* fix: icon size control labels

* add: icon select component

* add: color picker component

* add: accessibility icons

* add: icon export

* update: add icons to the component

* fix: styling for the icon select control

* update: color picker with react-colorful component

* update: icon size component with live icon design

* fix: styling of radio boxes

* add: icon design settings layout

* add: position settings layout

* add: layout exports

* add: alignment matrix and position control components

* add: position settings  & position settings for mobile layout

* fix: formatting and text-domain

* update: filter names

* fix: hook import

* add: set function for settings

* add: prop-types package

* update: refactor notification component and context

* update: remove filter for authorize url

* Update modules/settings/assets/js/components/color-picker/style.css

Co-authored-by: Raz Ohad <admin@bainternet.info>

* update: color picker class name

---------

Co-authored-by: Ohad <ohad@elementor.com>
Co-authored-by: Raz Ohad <admin@bainternet.info>

* [App 780] Navigation Sidebar (#115)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: add rule to move jsx props to multiline imporving readability

* add: connect modal

* update: hooks import for better readability

* update: replace functions with hooks

* add: connect module

* add: settings and get settings route

* add: hooks and contexts to get settings

* add: hooks

* add: notification component

* add: data api

* add: settings provider and connect settings

* add: husky

* fix: formatting and text-domain

* update: filter names

* fix: hook import

* add: set function for settings

* add: prop-types package

* update: refactor notification component and context

* update: remove filter for authorize url

* update: imports and exports of hooks

* update: plugin settings context filename and relevant imports

* update: icons and icon imports

* add: sidebar(wip)

* update: fix width of connect screen on mobile

* update: sidebar layout

* add: credit card and user arrow icons

* update: hidden wpfooter and fixed sidebar height

* update: sidebar layout

* add: basic page layouts

* update: sidebar layout

* add: sidebar menu, sidebar app bar and my account menu components

* update: add sidebar and menu settings

* update: add page layouts

* update: admin top bar

* add: bottom bar

* add: bottom bar and top bar

* add: bottom bar and top bar

* update: page content styling

* fix: styling

* fix: styling

* update: text domain

* update: added translations

* fix: admin top bar layout

* update: exports of icons

* update: exports of components

* add: aliases for imports and fix exports

* fix: height and styling of the layout

* fix: unhide wp footer

* update: keep widget menu open on page load (default)

* update: linter rules to move first prop to new line

* update: linter rules to move first prop to new line

---------

Co-authored-by: Ohad <ohad@elementor.com>

* Fix error on install plugin, add prettier (#116)

* Feature/app 810 assemble icon settings page (#117)

* Initial refactor commit

*  Added build and tests CI/CD

* update: add src for admin settings

* update: incorrect constant names

* update: namespace

* add: accessibility settings

* update: webpack to output files inside a folder

* update: build output folders

* update: removed commented code

* update: npm scripts

* add: webpack config

* add: hooks

* update: move admin setting to the module folder

* update: assets loading logic

* update: add rule to move jsx props to multiline imporving readability

* add: connect modal

* update: hooks import for better readability

* update: replace functions with hooks

* add: connect module

* add: settings and get settings route

* add: hooks and contexts to get settings

* add: hooks

* add: notification component

* add: data api

* add: settings provider and connect settings

* add: husky

* add: icon size control

* fix: icon size control labels

* add: icon select component

* add: color picker component

* add: accessibility icons

* add: icon export

* update: add icons to the component

* fix: styling for the icon select control

* update: color picker with react-colorful component

* update: icon size component with live icon design

* fix: styling of radio boxes

* add: icon design settings layout

* add: position settings layout

* add: layout exports

* add: alignment matrix and position control components

* add: position settings  & position settings for mobile layout

* fix: formatting and text-domain

* update: filter names

* fix: hook import

* add: set function for settings

* add: prop-types package

* update: refactor notification component and context

* update: remove filter for authorize url

* update: imports and exports of hooks

* update: plugin settings context filename and relevant imports

* update: icons and icon imports

* add: sidebar(wip)

* update: fix width of connect screen on mobile

* update: sidebar layout

* Update modules/settings/assets/js/components/color-picker/style.css

Co-authored-by: Raz Ohad <admin@bainternet.info>

* update: color picker class name

* add: credit card and user arrow icons

* update: hidden wpfooter and fixed sidebar height

* update: sidebar layout

* add: basic page layouts

* update: sidebar layout

* add: sidebar menu, sidebar app bar and my account menu components

* update: add sidebar and menu settings

* update: add page layouts

* update: admin top bar

* add: bottom bar

* add: bottom bar and top bar

* add: bottom bar and top bar

* update: page content styling

* fix: styling

* fix: styling

* update: text domain

* add: props to wrapper

* add: icon design and position setting layouts

* add: in page scroll behaviour to the settings

* add: widget icons and getter function

* update: icon design settings getter and setter functions

* update: imports

* add: mobile layout for position settings

* add: icon position settings

* add: icon position settings hooks and handlers

* fix: alignment of controls in AlignmentMatrixControl

* update: useSettings and usePositionSetting hooks and relevant functions

* fix: colors of AlignmentMatrixControl

* fix: styling of components and layouts

* add: aliases

* add: container wrapper to page

* update: accessibility options rendering logic

* fix: order of the icons

* add: aliases for components and hooks imports

* fix: styling of settings panel

* fix: container height for settings page

* update: toggle control states

* add: widget icon settings

* add: load saved widget icon settings

* update: move layout to page for different designs per page

* update: add changes tracking and disable button logic

* add: async/await to save settings

* update: convert options to array of objects

* Update modules/settings/assets/js/components/bottom-bar/index.js

Co-authored-by: VasylD <vasyld@elementor.red>

* Update modules/settings/assets/js/app.js

Co-authored-by: VasylD <vasyld@elementor.red>

* fix: remove duplicate entries

---------

Co-authored-by: Ohad <ohad@elementor.com>
Co-authored-by: Raz Ohad <admin@bainternet.info>
Co-authored-by: VasylD <vasyld@elementor.red>

* Feature/app 708 widget menu settings (#118)

* add: icons for menu settings

* add: placeholder layout for widget preview in menu settings

* update: load saved settings and updated imports

* add: logics for handling and saving menu settings

* add: useSavedSettings hook

* update: set export as default for Sidebar layout

* add: widget menu settings layout and settings

* update: add widget menu settings and widget preview layouts

* add: hide/show minimum option alert notification

* update: styling of the save button

* update: save settings logic to use async/await

* fix: accessibility text icon

* update: app type (#119)

* [APP-834] Update account menu buttons (#121)

* update: account menu buttons

* update: billing link

* add: error handling for switch account

* [APP-835] add service data (#122)

* add: client functions

* add: site register and site info endpoints

* update: add plan data settings

* update: add support for 201 response code

* update: add plan data key

* update: store the plan data on the once the site is registered

* update: add filter for client url

* add: retry registering in there is any error after connect

* update: setting prefix

* add: plan data

* update: add account details to menu

* fix: lint issues

* update: add data checkbox support (#123)

* [APP-928] Settings pointer (#125)

* add: settings pointer

* update: add alias for the settings

* [APP-837] Add post connect modal (#120)

* add: post connect modal

* update: settings prefix

* fix: connect modal design

* update: connect modal text

* add: connect modal graphics

* update: connect modal icon

* update: post connect modal

* update: sidebar menu text

* update: text of icon settings

* update: text

* update: php compatibility with return types

* add: accessibility statement page structure (#126)

* [APP-721] Render widget and global settings (#124)

* add: webhook endpoint

* add: widget module

* add: default widget settings on successful registration

* update: name of global object to ea11yWidget

* update: remove json encoding to make objects available on the frontend

* update: widget url, filter and enqueuing method

* update: removed obsolete code

* update: enqueue script only when connected

* update: add check for valid plan data and key

* update: conditional check

* update: conditional check

* fix: widget loading error (#128)

* [Legacy] Upgrade To New [APP-949] (#127)

* Added `Notice_Base` and `Notices` component to core module

* Always load core module and load all other modules based on legacy status

* added filter in customizer settings

* added bubble / pimple in admin menu to indicate upgrade

* added `Dismissible_Deprecated_Nag` notice to none legacy pages

* added `Dismissible_Deprecated_Nag` notice to legacy pages

* Added `Upgrade` component to legacy module

includes:
* loading of notices
* introduction modal
* admin menu pimple
* customizer notice
* pointer
* confirmation modal
* upgrade logic and handler

*  Fixed legacy module test

* added `local:quick-run` command to run in browser mode

* update phpunit workflow

* ensure wp.ajax is loaded

* wrong translations

* Updated strings

* added "Equally"

* remove unused test

*  use custom version of wp test library (#129)

*  use custom version of wp test library

* cleanup

* update WP versions for testing

* [APP-711] Widget preview (#130)

* add: dynamic script loader for widget

* update: settings name

* update: settings save function and comments

* update: tools settings object structure

* add: widget preview section

* update: added setting page slug as a constant

* update: enqueue widget for preview in the settings

* add: widget icon assets link

* add: widget icon svgs

* update: store widget url in a constant

* update: store widget url in a constant

* update: trigger widget preview update on menu item changes

* update: remove the icon option from the frontend.

* update: add widget URL

* update: plan data setting type

* update: widget plan url

* update: widget plan url and parse plan data

* fix: phpcs error ext-json missing

* fix: widget url

* fix: save and use plan data as a serialized option

* fix: use template string for widget url

* [APP-908] Accessibility generator (#131)

* add: accessibility statement radio icons

* update: add form group on radio buttons

* add: statement generator

* add: statement generator

* add: accessibility statement data option

* update: create page in WordPress and save it to the option

* update: exclude zip file from the git

* update: render statement page conditionally

* add: statement link layout and settings

* add: preload statement data

* update: publish the created page and add link for it

* update: changed Dynamic Script Loader to WidgetLoader

* add: accessibility statement url

* update: text and styling

* update: styling of the preview text

* update: restructure statement generator

* add: support for dynamic update in statement links

* update: remove index.css file for widget loader

* add: widget styling for settings page

* add: empty link when hide link is enabled

* update: statement page structure and logic

* fix: typo

* update: convert component into a styled component

* fix: styling and layout

* update: icons

* update: convert radio buttons to styled component

* fix: typo and style

* add: fading for the link preview

* update: import

* update: styling and spacing

* fix: sidebar layout

* update: wpcs to latest version

* fix: spacing

* fix: wpcs version

* add: check for valid statement page

* fix: jitters on rendering

* fix: use escape attribute

* update: settings menu slug and plugin name

* fix: menu item rendering

* update: definition of the styled text field

* add: addPage function to the API

* fix: add notification on page creation

* add: copy link icon

* update: optimize SVGs

* [APP-908] Additional fixes (#133)

* fix: text domains

* updated: styled component syntax

* update: use await instead of then

* fix: prevent application crash in case widget fails to load

* add: generated info tip card

* update: refactor function

* Fix: Fix the QA bugs [n/a] (#135)

* [APP-830] Add mixpanel events (#134)

* [APP-830] Add mixpanel events

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* [APP-830] Add user to init Mixpanel (#136)

* [APP-830] Add mixpanel events

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* Merge branch 'develop' into feature/APP-830-add-mixpanel

# Conflicts:
#	.gitignore
#	modules/settings/assets/js/components/sidebar-menu/index.js
#	modules/settings/assets/js/pages/accessibility-statement.js

* [APP-830] Add user to init Mixpanel

* [APP-830] Add user to init Mixpanel

* [APP-830] Add user to init Mixpanel

* [APP-830] rename events (#137)

* Connect and Preview Fixes (#139)

* updated connect admin page

* Use unified widget URL instead of hardcoded Js to support envs

* Removed enqueue of fictional widget.js and reuse settings `admin` handle

* Fix: Fix the QA bugs [n/a] (#138)

* New: Finish the BE integration [n/a]

* Fix: Fix some bugs [n/a]

* upgrade flow UI and design tweaks [app-949] (#141)

* Updated Learn More links with UTM's

* Tweaked Pointer strings Icon and CTA

* dismissible notice strings

* sticky notice strings

* updated upgrade flow design for pointer, notices, introduction modal, and confirmation modal

* added build script

* [APP-979] Update links and plugin name (#140)

* Ensure loading of legacy widget based on any saved data and fixed legacy JS

* Bug/app 1002 (#143)

* Bug: Update the logo in the "Hide Widget" modal [APP-1001]

* Fix: Update the side menu spaces [APP-1002]

* [APP-991] Add translation for statement (#142)

* [APP-979] Update links and plugin name

* [APP-991] Add translation for statement

* Set Prod Widget URL

* [APP-1004][APP-1005][APP-1006] Fix generator UI and logic, fix statement UI, fix copy link (#144)

* [APP-1004] Fix generator UI and logic

* [APP-1005] Fix statement UI

* [APP-1005] Fix statement UI

* Mixpanel record session

* Fix: Enhance position values validation [APP-1009] (#146)

* Bug/app 1003 (#147)

* Fix: Add a border to the preview [n/a]

* Fix: Fix Capabilities screen UI [APP-1003]

* [APP-1020] add missed events (#148)

* [APP-1015] fix switch account (#149)

* [APP-1015] fix switch account (#150)

* [APP-1021] Fix switch modal ui (#151)

* fix: ui issues

* fix: translation strings

* [APP-912] add default settings for RTL (#152)

* [APP-912] add default settings for RTL

* [APP-912] add default settings for RTL

* [APP-912] add default settings for RTL

* [APP-1026] Remove HTML breaking <style> tag & update pointer logic (#153)

* fix: remove extra closing tag

* update: hide settings pointer when plugin settings is opened

* update: remove unused functions

* update: add help text to mobile position settings

* Bug/app 1003 (#157)

* Fix: Add missed translations [n/a]

* Fix: Refresh the plan data on page load [n/a]

* New: Add loader to the settings [n/a]

* update: delete lock key after each check (#160)

* New: Update dashboard icon size [n/a] (#158)

* New: Update dashboard icon size [n/a]

* Fix: Fix widget previews [n/a]

* [APP-1018] Help menu change (#155)

* update: remove accessibility word from menu items

* update: remove top bar

* update: add help button to the sidebar

* update: re-add spacing in styled css code

* update: create styled components

* [APP-973] Add an UTM for users upgrading from one click to the new widget (#165)

* update: ui of the statement preview (#166)

* [APP-1011][APP-1013] focus outline and sitemap settings (#161)

* [APP-1011][APP-1013] focus outline and sitemap settings

* [APP-1011][APP-1013] focus outline and sitemap settings

* [APP-1011][APP-1013] focus outline and sitemap settings

* [APP-1011][APP-1013] focus outline and sitemap settings

* update: video link (#167)

* [APP-1051] Fix layout on the small and medium devices (#156)

* fix: layout on the small and medium devices

* update: change components to styled components

* refactor: position settings wrapper into a separate component

* [APP-1012][APP-1085] Add skip to content settings and event for Mixpanel (#169)

* [APP-1012][APP-1085] Add skip to content settings and event for Mixpanel

* [APP-1012][APP-1085] Add skip to content settings and event for Mixpanel

* [APP-1012][APP-1085] Add skip to content settings and event for Mixpanel

* [APP-1012][APP-1085] Add skip to content settings and event for Mixpanel

* [APP-1048] Add tooltip to accessibility statement (#159)

* add: tooltip to accessibility statement

* update: hide infotip when statement link is set

* update: infotip's text

* fix: infotip naming and logic

* [APP-1049] Add back button accessibility statement (#164)

* add: back button to the statement link section

* add: Edit link button to statement page section

* update: add admin_url and generate query args properly

* update: learn more link for accessibility statement page (#168)

* Fix: Update overlay height [n/a] (#171)

* Fix settings and connect issues (#170)

* fix: token fails to refresh after expiry

* update: add 12 hour time for plan data refresh and fix missing subscription id

* update: add check to refresh plan data

* fix: decoding errors and alignment and add logging for errors

* update: refresh logic and formatting

* Update/app 1029 app name (#173)

* update: plugin name

* update: default menu option

* update: menu structure

* update: add inline checks

* update: reduce padding on app icon in menu

* fix: height of the modal

* update: app menu icon color

* update: menu icon colors

* fix: menu icon size

* update: plugin names

* update: HELP_LINK

* update: app name

* update: icon background color

* update: icon size

* fix: admin icon size

* fix: width of the sidebar

* Fix/toggle not working properly (#174)

* fix: toggles not working properly in some cases

* fix: saving of the settings was not working

* add: missing adminUrl in settings data

* fix: switch was not working properly in some cases (#175)

* Fix/app 1093 incorrect position on default (#176)

* fix: switch was not working properly in some cases

* fix: default setting structure for the icon position

* [APP-1096] Text changes (#177)

* fix: switch was not working properly in some cases

* update: plugin name and action buttons title

* fix: revert change to is_active function

* fix: button color

* New: Add the skip link [APP-1012] (#179)

* [APP-1097] Fix preview load (#178)

* [APP-1097] Fix preview load

* [APP-1097] Fix preview load

* [APP-1097] Fix preview load

* Update modules/settings/assets/js/components/widget-loader/index.js

---------

Co-authored-by: Raz Ohad <ohad@elementor.com>

* [APP-1123] Accessibility statement text (#181)

* fix: switch was not working properly in some cases

* update: accessibility statement content

* [APP-1121] Add support for react-jsx-runtime for older WP versions (#180)

* fix: switch was not working properly in some cases

* update: revert wp-scripts version to 28.0.0 to add support for older WordPress versions

* update: revert wp-scripts to 27.9.0

* add: support for react-jsx-runtime in older versions of WordPress

* update: version to the latest wp-scripts 30.3.0

* update: add lib/ to gitignore

* update: plugin name

* Fix: Fix admin widget previews [n/a] (#183)

* [APP-1061] change mixpanel user id (#184)

* [APP-1129] change toggle_event for mixpanel (#186)

* Remove skip to content btn if anchor does not exist (#187)

* fix: hide wp notices to keep the layout from shifting (#189)

* [APP-1143] fix bug with capability display (#195)

* [APP-1143] fix bug with capability display

* [APP-1143] fix bug with capability display

* [APP-1143] fix bug with capability display

* [APP-1143] fix bug with capability display

* add: loading text to widget preview (#196)

* [APP-1142] add 'appType' super props, change identify key (#191)

* [APP-1108][APP-1109][APP-1110] Add analytics backend logic

* [APP-1142] Add 'appType' super props

* [APP-1144] Accessibility statement tooltip text update (#198)

* update: text

* update: switch design

* Tweak: Update widget loaders [n/a] (#197)

* added images to readme (#172)

* added images to readme

* updated readme

* V3.0.0

* updated readme

* updated tested up to

* added deploy workflows

* Remove duplicate settings menu

* update: switch redirect url

---------

Co-authored-by: Yakir Sitbon <kingyes1@gmail.com>
Co-authored-by: Nirbhay Singh <121793120+nirbhayel@users.noreply.github.com>
Co-authored-by: VasylD <vasyld@elementor.red>
Co-authored-by: Pavlo Kniazevych <139438463+pkniazevych@users.noreply.github.com>
This commit is contained in:
Raz Ohad 2025-02-19 12:39:50 +02:00 committed by GitHub
parent fc328354c5
commit ded7beb67b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
210 changed files with 37834 additions and 1599 deletions

33
.build-rsync-exclude Normal file
View file

@ -0,0 +1,33 @@
.bpg.json
.babelrc
ruleset.xml
webpack.config.js
phpunit.xml.dist
.DS_Store
node_modules/
.git
.gitignore
.gitmodules
.github/
.editorconfig
.gitattributes
.eslintrc
.eslintignore
.phpcs.xml.dist
.phpunit.xml.d
coverage/
assets/dev/
.phpunit.result*
bin/
tests/
package.json
composer.json
composer.lock
package-lock.json
jest.config.js
.build-rsync-exclude
one-click-accessibility/
one-click-accessibility.zip
.svn-assets/
.husky/

11
.eslintignore Normal file
View file

@ -0,0 +1,11 @@
assets/lib/**/*.js
assets/js/*.js
**/*.min.js
**/node_modules/**
**/vendor/**
build/**
libraries/**
coverage/**
assets/build/**
tests/**
modules/legacy/assets/**

66
.eslintrc Normal file
View file

@ -0,0 +1,66 @@
{
"extends": [
"plugin:react/recommended",
"plugin:@wordpress/eslint-plugin/recommended-with-formatting",
"plugin:import/recommended",
"plugin:jsx-a11y/strict",
"plugin:prettier/recommended"
],
"plugins": [
"babel",
"react"
],
"env": {
"browser": true,
"node": true,
"es6": true,
"jquery": true
},
"globals": {
"jQuery": "writable"
},
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"requireConfigFile": false
},
"rules": {
"no-console": "off",
"react-hooks/exhaustive-deps": "off",
"strict": [ "error", "global" ],
"curly": "warn",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type"
],
"alphabetize": {
"order": "asc",
"caseInsensitive": false
}
}
],
"import/newline-after-import": ["error", {
"count": 1
}]
},
"settings": {
"import/resolver": {
"node": {
"paths": ["node_modules"]
},
"webpack": {
"config": "webpack.config.js"
}
}
},
"parser": "@babel/eslint-parser"
}

23
.github/checkstyle-problem-matcher.json vendored Normal file
View file

@ -0,0 +1,23 @@
{
"problemMatcher": [
{
"owner": "phpcs",
"severity": "error",
"pattern": [
{
"regexp": "^<file name=\"(?:\\/github\\/workspace\\/)?(.*)\">$",
"file": 1
},
{
"regexp": "<error line=\"(\\d*)\" column=\"(\\d*)\" severity=\"(error|warning)\" message=\"(.*)\" source=\"(.*)(\"\\/>+)$",
"line": 1,
"column": 2,
"severity": 3,
"message": 4,
"code": 5,
"loop": true
}
]
}
]
}

View file

@ -0,0 +1,65 @@
#!/bin/bash
set -eo pipefail
if [[ -z "$SVN_USERNAME" ]]; then
echo "Set the SVN_USERNAME secret"
exit 1
fi
if [[ -z "$SVN_PASSWORD" ]]; then
echo "Set the SVN_PASSWORD secret"
exit 1
fi
if [[ -z "$PLUGIN_VERSION" ]]; then
echo "Set the PLUGIN_VERSION env var"
exit 1
fi
if [[ -z "$PLUGIN_SLUG" ]]; then
echo "Set the PLUGIN_SLUG env var"
exit 1
fi
echo "Publish version: ${PLUGIN_VERSION}"
PLUGIN_PATH="$GITHUB_WORKSPACE/${PLUGIN_SLUG}"
SVN_PATH="$GITHUB_WORKSPACE/svn"
cd $PLUGIN_PATH
mkdir -p $SVN_PATH
cd $SVN_PATH
echo "Checkout from SVN"
svn co https://plugins.svn.wordpress.org/${PLUGIN_SLUG}/trunk
echo "Clean trunk folder"
cd $SVN_PATH/trunk
find . -maxdepth 1 -not -name ".svn" -not -name "." -not -name ".." -exec rm -rf {} +
echo "Copy files"
rsync -ah --progress $PLUGIN_PATH/* $SVN_PATH/trunk
echo "Preparing files"
cd $SVN_PATH/trunk
echo "svn delete"
svn status | grep -v '^.[ \t]*\\..*' | { grep '^!' || true; } | awk '{print $2}' | xargs -r svn delete;
echo "svn add"
svn status | grep -v '^.[ \t]*\\..*' | { grep '^?' || true; } | awk '{print $2}' | xargs -r svn add;
svn status
echo "Commit files to trunk"
svn ci -m "Upload v${PLUGIN_VERSION}" --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD"
echo "Copy files from trunk to tag ${PLUGIN_VERSION}"
svn cp https://plugins.svn.wordpress.org/${PLUGIN_SLUG}/trunk https://plugins.svn.wordpress.org/${PLUGIN_SLUG}/tags/${PLUGIN_VERSION} --message "Tagged ${PLUGIN_VERSION}" --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD"
svn update
echo "Remove the SVN folder from the workspace (for multiple releases in the same Action)"
#rm -rf $SVN_PATH
echo "Back to the workspace root"
cd $GITHUB_WORKSPACE

View file

@ -0,0 +1,65 @@
'use strict';
const fs = require( "fs" );
const MAIN_FILE_NAME = 'pojo-accessibility.php';
const VERSION_CONSTANT = 'EA11Y_VERSION';
const { VERSION } = process.env;
if ( ! VERSION ) {
throw new Error( 'VERSION is not defined in the environment variables.' );
}
async function replaceInFileWithArray( filePath, replacementArray ) {
try {
let content = fs.readFileSync( filePath, { encoding: 'utf8' } );
for ( const replacement of replacementArray ) {
console.log( `Replacing ${ replacement.from } with ${ replacement.to } in ${ filePath }` );
content = content.replace( replacement.from, replacement.to );
}
console.log( content );
//fs.writeFileSync( filePath, content, { encoding: 'utf8' } );
console.log( `All replacements in ${ filePath } were successful.`);
} catch (error) {
console.error( `An error occurred in ${ filePath }:`, error );
}
}
const run = async () => {
try {
// update stable tag in readme.txt
await replaceInFileWithArray( './readme.txt',
[
{
from: /Stable tag: (.*)/m,
to: `Stable tag: ${ VERSION }`,
},
{
from: /= NEXT_VERSION =/m,
to: `= ${ VERSION } =`,
},
]
);
// update version in MAIN_FILE_NAME
await replaceInFileWithArray( `./${MAIN_FILE_NAME}`,
[
{
from: /\* Version: (.*)/m,
to: `* Version: ${ VERSION }`,
},
{
from: new RegExp(`define\\( '${VERSION_CONSTANT}', '(.*)' \\);`, 'm' ),
to: `define( '${VERSION_CONSTANT}', '${VERSION}' );`,
}
]
);
} catch ( err ) {
// eslint-disable-next-line no-console
console.error( 'Error occurred:', err );
process.exit( 1 );
}
};
run();

49
.github/scripts/validate-build-files.sh vendored Normal file
View file

@ -0,0 +1,49 @@
#!/bin/bash
set -eo pipefail
if [[ -z "$PLUGIN_SLUG" ]]; then
echo "Set the PLUGIN_SLUG env var"
exit 1
fi
if [[ -z "$PLUGIN_VERSION" ]]; then
echo "Set the PLUGIN_VERSION env var"
exit 1
fi
PLUGIN_PATH="$GITHUB_WORKSPACE/${PLUGIN_SLUG}"
cd $PLUGIN_PATH
PLUGIN_MAIN_FILE="${PLUGIN_SLUG}.php"
if [ ! -f "${PLUGIN_MAIN_FILE}" ]; then
echo "${PLUGIN_MAIN_FILE} file does not exist"
exit 1
fi
if [ ! -f "readme.txt" ]; then
echo "readme.txt file does not exist"
exit 1
fi
if [[ $(grep -c "Version: $PLUGIN_VERSION" "$PLUGIN_MAIN_FILE") -eq 0 ]]; then
echo "${PLUGIN_MAIN_FILE} file does not contain the correct build version : $PLUGIN_VERSION"
EXISTING_VERSION=$(sed -n 's/.*Version: \(.*\)/\1/p' "$PLUGIN_MAIN_FILE")
echo "Existing version: $EXISTING_VERSION"
exit 1
fi
if [[ $(grep -c "Stable tag: $PLUGIN_VERSION" "readme.txt") -eq 0 ]]; then
echo "readme.txt file does not contain the correct stable tag version : $PLUGIN_VERSION"
EXISTING_VERSION=$(sed -n 's/.*Stable tag: \(.*\)/\1/p' "readme.txt")
echo "Existing version: $EXISTING_VERSION"
exit 1
fi
echo "validate-build-files Details:"
echo "---"
echo "SVN Tag name: $PLUGIN_VERSION"
echo "Package Version: $PACKAGE_VERSION"
echo "Trunk Files:"
ls -la

88
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,88 @@
name: Build
on:
pull_request:
workflow_call:
workflow_dispatch:
inputs:
push_to_slack:
description: 'Push to Slack'
type: boolean
required: true
default: 'false'
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
env:
fail-fast: 'true'
- name: Install Composer dependencies cache
uses: ramsey/composer-install@v3
- name: Install Composer dependencies
run:
composer install --no-dev
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: "Debug info: show tooling versions"
continue-on-error: true
run: |
set +e
echo "Start debug Info"
echo "Date: ${{ steps.date.outputs.date }}"
echo "PHP Version: $(php -v)"
echo "Composer Version: $(composer --version)"
echo "Node Version: $(node --version)"
echo "NPM Version: $(npm --version)"
echo "GIT Version: $(git --version)"
echo "The github context is:"
echo "${{ toJson(github) }}"
echo "End debug Info"
echo "exitcode=$?" >> $GITHUB_OUTPUT
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Build Plugin
run: |
rsync -av --exclude-from=.build-rsync-exclude . one-click-accessibility
zip -r one-click-accessibility.zip one-click-accessibility
- name: Archive
uses: actions/upload-artifact@v4
with:
name: one-click-accessibility
path: one-click-accessibility.zip
retention-days: 14
- name: Push to Slack on PR merge
if: ${{ github.event.pull_request.merged == true }}
run: |
curl -F file=@one-click-accessibility.zip -F "initial_comment=One Click Accessibility - A new PR has been pushed to the master branch by ${{ github.actor }}." title="${{ github.event.pull_request.title }}" -F channels=C07LFCFNGDB -H "Authorization: Bearer ${{ secrets.CLOUD_SLACK_BOT_TOKEN }}" https://slack.com/api/files.upload
- name: Push to Slack on Manual Trigger
if: ${{ github.event.inputs.push_to_slack == 'true' }}
run: |
curl -F file=@one-click-accessibility.zip -F "initial_comment=One Click Accessibility - A new build was triggered by ${{ github.actor }}." title="Manuall ${{ steps.date.outputs.date }}" -F channels=C07LFCFNGDB -H "Authorization: Bearer ${{ secrets.CLOUD_SLACK_BOT_TOKEN }}" https://slack.com/api/files.upload

30
.github/workflows/deploy-wp-assets.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: Deploy WordPress Assets
on:
workflow_dispatch:
jobs:
deploy-wp-assets:
if: ( github.actor == 'KingYes' || github.actor == 'arielk' || github.actor == 'bainternet' ) && startsWith( github.repository, 'elementor/' )
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install SVN
run: |
sudo apt-get update -y
sudo apt-get install -y subversion
which svn
svn --version
- name: Preparing envs
run: |
svn co https://plugins.svn.wordpress.org/pojo-accessibility/assets remote-wp-assets
rm -rf remote-wp-assets/*
rsync -ah --progress .wporg-assets/* remote-wp-assets/
- name: Publish to WordPress.org SVN
run: |
cd remote-wp-assets
svn status | grep -v '^.[ \t]*\\..*' | { grep '^!' || true; } | awk '{print $2}' | xargs -r svn delete;
svn status | grep -v '^.[ \t]*\\..*' | { grep '^?' || true; } | awk '{print $2}' | xargs -r svn add;
svn status
svn ci -m "Assets updated" --no-auth-cache --non-interactive --username "${{ secrets.SVN_USERNAME }}" --password "${{ secrets.SVN_PASSWORD }}"

54
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Deploy
on:
workflow_dispatch:
inputs:
VERSION:
type: choice
description: 'Version Type'
options:
- NONE
- PATCH
- MINOR
- MAJOR
required: true
default: 'NONE'
jobs:
build:
if: ( github.actor == 'KingYes' || github.actor == 'bainternet' || github.actor == 'arielk' ) && startsWith( github.repository, 'elementor/' )
uses: ./.github/workflows/build.yml
secrets: inherit
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install SVN
run: |
sudo apt-get update -y
sudo apt-get install -y subversion
which svn
svn --version
- name: Download Artifact
uses: actions/download-artifact@v4
with:
name: site-mailer
- run: ls -lah
- name: unzip
run: unzip site-mailer.zip
- name: Preparing envs
run: |
echo "PACKAGE_VERSION=$(cat package.json | jq -r '.version')" >> $GITHUB_ENV
echo "PLUGIN_SLUG=$(cat package.json | jq -r '.name')" >> $GITHUB_ENV
- name: Publish to WordPress.org SVN
env:
PLUGIN_SLUG: ${{ env.PLUGIN_SLUG }}
PLUGIN_VERSION: ${{ env.PACKAGE_VERSION }}
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
run: |
bash "${GITHUB_WORKSPACE}/.github/scripts/validate-build-files.sh"
bash "${GITHUB_WORKSPACE}/.github/scripts/publish-to-wordpress-org.sh"

45
.github/workflows/js-ci.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: NPM CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env:
token: ${{ secrets.CLOUD_DEVOPS_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: npm run lint:js
# - name: Test
# run: npm run test

View file

@ -0,0 +1,52 @@
name: PHP Lint
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
jobs:
PHP-Code-Standards:
name: Lint PHP files
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip PHPCS]') || !contains(github.event.head_commit.message, '[skip CI]')"
steps:
- name: Check out source code
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: none
tools: composer, cs2pr, phpcs
env:
fail-fast: 'true'
- name: Log debug information
run: |
export PATH=$HOME/.composer/vendor/bin:$PATH
php --version
phpcs -i
composer --version
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Add error matcher
run: echo "::add-matcher::$(pwd)/.github/checkstyle-problem-matcher.json"
- name: Run style check
run: |
export PATH=$HOME/.composer/vendor/bin:$PATH
composer run lint -- --report=checkstyle

135
.github/workflows/phpunit.yml vendored Normal file
View file

@ -0,0 +1,135 @@
name: PHPUnit
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
jobs:
unit-tests:
name: "WP ${{ matrix.wp }}, multisite: ${{ matrix.ms }}, PHP: ${{ matrix.php }}"
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [7.4, 8.0, 8.1, 8.2]
wp: [6.5.x, 6.6.x, latest, nightly]
ms: [no, yes]
coverage: [no]
phpunit: [9]
include:
# PHP 7.4 is not supported by PHPUnit 9
- phpunit: 7
php: 7.4
# add coverage job
- coverage: yes
php: 8.0
wp: latest
ms: no
phpunit: 9
services:
mysql:
image: mariadb:latest
ports:
- "3306:3306"
env:
MYSQL_ROOT_PASSWORD: wordpress
MARIADB_INITDB_SKIP_TZINFO: 1
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_DATABASE: wordpress_test
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Matrix variables
id: matrix
run: |
echo "::group::Matrix variables"
echo "PHP version = ${{ matrix.php }}"
echo "WordPress = ${{ matrix.wp }}"
echo "Multisite = ${{ matrix.ms }}"
echo "Coverage = ${{ matrix.coverage }}"
echo "PHPUnit = ${{ matrix.phpunit }}"
echo "::endgroup::"
- name: Decide whether to enable coverage
id: coverage
run: |
if [ "${{ matrix.coverage }}" = "yes" ]; then
echo "coverage=pcov" >> $GITHUB_OUTPUT
echo 'ini=pcov.directory=inc, pcov.exclude="~/(vendor|tests|node_modules)/~"'
else
echo "coverage=none" >> $GITHUB_OUTPUT
echo "ini=opcache.enable_cli=1, opcache.jit=tracing, opcache.jit_buffer_size=64M" >> $GITHUB_OUTPUT
fi
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
coverage: ${{ steps.coverage.outputs.coverage }}
ini-values: ${{ steps.coverage.outputs.ini }}
php-version: ${{ matrix.php }}
env:
fail-fast: 'true'
- name: Install PHPUnit
run: |
wget -q -O /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${{ matrix.phpunit }}.phar"
chmod +x /usr/local/bin/phpunit
- name: Install dependencies
uses: ramsey/composer-install@v3
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Set up WordPress and WordPress Test Library
uses: bainternet/setup-wordpress-test-library@master
with:
version: ${{ matrix.wp }}
- name: Set up multisite mode
run: echo "WP_MULTISITE=1" >> $GITHUB_ENV
if: matrix.ms == 'yes'
- name: Update wp-test-config.php
run: |
if php -r 'exit(PHP_VERSION_ID < 80100);'; then
echo "Disabling WP_DEBUG in wp-test-config.php"
sed -i "s@define( 'WP_DEBUG', true );@// define( 'WP_DEBUG', true );@" /tmp/wordpress-tests-lib/wp-tests-config.php
fi
- name: Verify MariaDB connection
run: |
while ! mysqladmin ping -h 127.0.0.1 -P ${{ job.services.mysql.ports[3306] }} --silent; do
sleep 1
done
timeout-minutes: 1
- name: Run tests
run: |
OPTIONS=
if [ "${{ steps.coverage.outputs.coverage }}" != 'none' ]; then
OPTIONS="$OPTIONS --coverage-clover=clover.xml"
fi
phpunit --order-by=random ${OPTIONS}

12
.gitignore vendored
View file

@ -2,9 +2,19 @@
node_modules/
.sass-cache/
build/
lib/
log/
vendor/
composer.lock
npm-debug.log
.DS_Store
package-lock.json
bin/.env
*/node_modules
!assets/dev/*
assets/js/
coverage/
.phpunit.result.cache
.editorconfig
.vscode
.env
*.zip

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
npm run lint:js

View file

@ -1,16 +0,0 @@
{
"bitwise": true,
"browser": true,
"curly": true,
"eqeqeq": true,
"eqnull": true,
"esnext": true,
"immed": true,
"jquery": true,
"latedef": true,
"newcap": true,
"noarg": true,
"node": true,
"strict": false,
"trailing": true
}

10
.prettierrc Normal file
View file

@ -0,0 +1,10 @@
{
"useTabs": true,
"printWidth": 80,
"semi": true,
"singleQuote": true,
"endOfLine": "auto",
"arrowParens": "always",
"proseWrap": "always",
"bracketSpacing": true
}

View file

@ -1,35 +0,0 @@
# Travis CI Configuration File
sudo: false
# Tell Travis CI we're using PHP
language: php
# PHP version used in first build configuration.
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7.0
# WordPress version used in first build configuration.
env:
- WP_VERSION=latest WP_MULTISITE=0
- WP_VERSION=latest WP_MULTISITE=1
- WP_VERSION=4.0 WP_MULTISITE=0
- WP_VERSION=4.0 WP_MULTISITE=1
- WP_VERSION=4.1 WP_MULTISITE=0
- WP_VERSION=4.1 WP_MULTISITE=1
- WP_VERSION=4.2 WP_MULTISITE=0
- WP_VERSION=4.2 WP_MULTISITE=1
- WP_VERSION=4.3 WP_MULTISITE=0
- WP_VERSION=4.3 WP_MULTISITE=1
before_script:
- bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
after_success:
- bash <(curl -s https://codecov.io/bash)
script: phpunit --coverage-clover=coverage.xml

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,297 +0,0 @@
/**
* Pojo Makefile
*/
'use strict';
module.exports = function( grunt ) {
require( 'matchdep' ).filterDev( 'grunt-*' ).forEach( grunt.loadNpmTasks );
// Project configuration.
grunt.initConfig( {
pkg: grunt.file.readJSON( 'package.json' ),
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
'<%= grunt.template.today("dd-mm-yyyy") %> */',
checktextdomain: {
standard: {
options:{
text_domain: 'pojo-accessibility',
correct_domain: true,
keywords: [
// WordPress keywords
'__:1,2d',
'_e:1,2d',
'_x:1,2c,3d',
'esc_html__:1,2d',
'esc_html_e:1,2d',
'esc_html_x:1,2c,3d',
'esc_attr__:1,2d',
'esc_attr_e:1,2d',
'esc_attr_x:1,2c,3d',
'_ex:1,2c,3d',
'_n:1,2,4d',
'_nx:1,2,4c,5d',
'_n_noop:1,2,3d',
'_nx_noop:1,2,3c,4d'
]
},
files: [ {
src: [
'**/*.php',
'!node_modules/**',
'!build/**',
'!tests/**',
'!vendor/**',
'!*~'
],
expand: true
} ]
}
},
usebanner: {
dist: {
options: {
banner: '<%= banner %>'
},
files: {
src: [
'assets/js/app.min.js',
'assets/css/style.css',
'assets/css/style.min.css'
]
}
}
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: [
'Gruntfile.js',
'assets/js/skip-link-focus-fix.js',
'assets/js/app.dev.js'
]
},
uglify: {
options: {},
dist: {
files: {
'assets/js/app.min.js': [
'assets/js/skip-link-focus-fix.js',
'assets/js/app.dev.js'
]
}
}
},
less: {
dist: {
options: {
//cleancss: true
},
files: {
'assets/css/style.css': 'assets/less/style.less'
}
},
mincss: {
options: {
compress: true
},
files: {
'assets/css/style.min.css': 'assets/less/style.less'
}
}
},
watch: {
js: {
files: [
'**/*.js',
'!**/*.min.js'
],
tasks: [
'scripts'
],
options: {}
},
less: {
files: [
'**/*.less'
],
tasks: [
'styles'
],
options: {}
}
},
bumpup: {
options: {
updateProps: {
pkg: 'package.json'
}
},
file: 'package.json'
},
replace: {
plugin_main: {
src: [ 'pojo-accessibility.php' ],
overwrite: true,
replacements: [
{
from: /Version: \d{1,1}\.\d{1,2}\.\d{1,2}/g,
to: 'Version: <%= pkg.version %>'
}
]
},
readme: {
src: [ 'readme.txt' ],
overwrite: true,
replacements: [
{
from: /Stable tag: \d{1,1}\.\d{1,2}\.\d{1,2}/g,
to: 'Stable tag: <%= pkg.version %>'
}
]
}
},
shell: {
git_add_all : {
command: [
'git add --all',
'git commit -m "Bump to <%= pkg.version %>"'
].join( '&&' )
}
},
release: {
options: {
bump: false,
npm: false,
commit: false,
tagName: 'v<%= version %>',
commitMessage: 'released v<%= version %>',
tagMessage: 'Tagged as v<%= version %>'
}
},
wp_readme_to_markdown: {
github: {
options: {
screenshot_url: 'assets/{screenshot}.png',
wordpressPluginSlug: 'pojo-accessibility',
travisUrlRepo: 'https://travis-ci.org/pojome/one-click-accessibility',
gruntDependencyStatusUrl: 'https://david-dm.org/pojome/pojo-accessibility'
},
files: {
'README.md': 'readme.txt'
}
}
},
copy: {
main: {
src: [
'**',
'!node_modules/**',
'!build/**',
'!bin/**',
'!.git/**',
'!tests/**',
'!.travis.yml',
'!.jshintrc',
'!README.md',
'!phpunit.xml',
'!vendor/**',
'!Gruntfile.js',
'!package.json',
'!package-lock.json',
'!npm-debug.log',
'!composer.json',
'!composer.lock',
'!assets/less/**',
'!wp-assets/**',
'!.gitignore',
'!.gitmodules',
'!*~'
],
expand: true,
dest: 'build/'
}
},
clean: {
//Clean up build folder
main: [
'build'
]
},
wp_deploy: {
deploy:{
options: {
plugin_slug: '<%= pkg.slug %>',
svn_user: 'KingYes',
build_dir: 'build/',
assets_dir: 'wp-assets/'
}
}
},
phpunit: {
classes: {
dir: ''
},
options: {
bin: 'phpunit',
bootstrap: 'tests/bootstrap.php',
colors: true
}
}
} );
// Default task(s).
grunt.registerTask( 'default', [
'checktextdomain',
'scripts',
'styles',
'usebanner',
'wp_readme_to_markdown',
//'phpunit'
] );
grunt.registerTask( 'build', [
'default',
'clean',
'copy'
] );
grunt.registerTask( 'scripts', [
'jshint',
'uglify'
] );
grunt.registerTask( 'styles', [
'less'
] );
grunt.registerTask( 'publish', [
'default',
'bumpup',
'replace',
'shell:git_add_all',
'release'
] );
};

273
README.md
View file

@ -1,48 +1,114 @@
# One Click Accessibility [![Build Status](https://travis-ci.org/pojome/one-click-accessibility.svg?branch=master)](https://travis-ci.org/pojome/one-click-accessibility) [![Dependency Status](https://david-dm.org/pojome/pojo-accessibility/dev-status.svg)](https://david-dm.org/pojome/pojo-accessibility#info=devDependencies) [![WordPress](https://img.shields.io/wordpress/v/pojo-accessibility.svg?style=flat-square)](https://wordpress.org/plugins/pojo-accessibility/) [![WordPress](https://img.shields.io/wordpress/plugin/r/pojo-accessibility.svg?style=flat-square)](https://wordpress.org/plugins/pojo-accessibility/) [![WordPress](https://img.shields.io/wordpress/plugin/v/pojo-accessibility.svg?style=flat-square)](https://wordpress.org/plugins/pojo-accessibility/) [![WordPress](https://img.shields.io/wordpress/plugin/dt/pojo-accessibility.svg?style=flat-square)](https://wordpress.org/plugins/pojo-accessibility/) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com/)
# Ally - Web Accessibility & Usability
**Contributors:** elemntor, pojo.me, KingYes, ariel.k, jzaltzberg, bainternet \
**Tags:** Web Accessibility, Accessibility, A11y, Accessibility statement, wcag \
**Requires at least:** 4.1 \
**Tested up to:** 6.3 \
**Requires PHP:** 5.4 \
**Stable tag:** 3.0.0 \
**License:** GPLv2 or later
Ally Web Accessibility (formerly One Click Accessibility) enables you to include easy-to-set accessibility features that make your website more inclusive to visitors regardless of their abilities.
## Description
Ally Web Accessibility is a plugin aimed to help web creators design websites that align with global accessibility standards. With regulations like the European Accessibility Act set to take effect in 2025 - there is a growing shift towards a greater focus on accessibility.
Our Ally Web Accessibility plugin replaces the One Click Accessibility plugin and introduces a newly designed interface with an optimized editing experience.
# Its the Law
As accessibility standards continue to evolve and become mandatory, its important to start making website adjustments to comply with fast-approaching global regulations. Among its other capabilities, this plugin lets you instantly generate your website's Accessibility Statement, which is now mandatory in most locales.
The Ally Web Accessibility plugin is not a substitute for a thorough accessibility audit and is NOT intended to completely make your website legally compliant. However, with it, youre one step closer to making your website inclusive to more visitors, including those with visual, auditory or cognitive challenges.
Ensuring that your website complies with all applicable accessibility requirements is your responsibility. We recommend working with qualified accessibility professionals to help achieve full compliance.
# Why Prioritize Accessibility
Every user deserves a seamless online experience, regardless of their abilities. An accessible website isn't just ethical; it's good business. By prioritizing inclusivity, you improve user experience for everyone, boost your search engine rankings, and attract a wider audience.
# Putting Your Visitors in Control
Accessible websites allow people with various disabilities to access your website using assistive technologies where visitors can adjust design elements to fit their needs, such as increasing font sizes, adjusting color contrasts, pausing animations, and more.
## Features:
Ally Web Accessibilitys features so far:
### Customization Options for Web Creators
- **Add Accessibility Statement URL**: Link an existing Accessibility Statement URL, ensuring it appears directly within the accessibility widget.
- **Generate the Accessibility Statement**: Effortlessly create your Accessibility Statement with our built-in generator without the need to search the web. Simply fill in a form with your details and a custom Accessibility Statement will be instantly generated and placed on a dedicated page of your website.
- **Show icon / Hide icon**: Choose to show or hide the accessibility icon separately for desktop and mobile versions of the website.
- **Color Selection**: Select the color scheme that the end-user will see when the icon is opened. This includes options for background, text, and highlight colors.
- **Icon Color**: Decide on the color of the selected icon to match your websites branding. The same color will also be displayed in the widgets header and footer for brand consistency.
- **Icon Positioning**: Define the position of the icon on any of the four corners of the page (top-left, top-right, bottom-left, bottom-right).
- **Icon Size**: Choose the size of the accessibility icon: Large, Medium, and Small.
- **Sitemap URL**: A default sitemap link is available in the widget to enhance the visibility of the site structure to end users. You can override the default and set any desired sitemap URL.
- **Skip to Main Content**: "Skip to Content" enables users to quickly navigate to the main content. Set to On by default, web creators have the option to edit the anchor ID to navigate to the best location.
### Customization Options for Website Visitors
Below is a list of features website visitors can control. (Each feature may be enabled/disabled by the web creator during website setup, determining which are shown to website visitors.)
- **Hide the icon**: Allows the website visitor to turn off or hide the widget from the website interface, enabling them to use their own accessibility tools without interference.
- **Restore Defaults**: Provides a one-click option to reset all customized accessibility settings back to their default state.
- **Increase/Decrease Text Size**: For users who have reading difficulties or low vision
- **Grayscale**: Apply a grayscale filter to the page for color blind users.
- **Contrast Scale**: A slider that adjusts between different contrast modes: Negative, High, Light, and Dark Contrast for color blind or the visually challenged.
- **Links Underline**: Underlines all links on the page to make them more visible for the visually challenged.
- **Readable Font**: Changes the font to one that is more legible for dyslexic users.
- **Sitemap**: Displays all headers, landmarks, and links in a structured list for easier navigation including for those who use screen readers.
- **Hide Images**: Hides all images on the page to reduce distractions and make the page more readable for the visually challenged or ADHD users.
- **Pause Animations**: Stops animations running on the page, helping users with ADHD or cognitive difficulties to focus on content
- **Reading Guide**: Makes the cursor bigger or turns it into a reading guide to focus on selected elements for users with reading difficulties.
- **Line Height**: Increases space between text lines to make the content easier to read for dyslexic or visually challenged users.
- **Text Align**: Allows users to align text (left, right, or center) to improve readability.
- **Focus Outline**: Enables users to see which element is currently active as they Tab through the page. Geared for keyboard-only navigation.
- **Skip to Main Content**: When users land on your site, pressing the Tab key brings the Skip to main content link into focus. By selecting this link, users can skip directly to the main content, enabling them to quickly navigate to the main content. Useful for keyboard-only and screen-reader users.
# Get Started Today
## Make your site more accessible with Ally!
For more information about Ally visit our [help center](https://go.elementor.com/acc-wp-repo-learn-more) If you have any questions or need support, feel free to [contact us](https://go.elementor.com/wp-repo-wp-dash-sm-contact-us/).
This plugin requires a connection to an active Elementor account in order to identify the user and provide the user with the purchased service. This connection is triggered manually by the user via the plugins settings panel. Learn more about our [terms and conditions](https://go.elementor.com/acc-wp-repo-term-and-conditions). This plugin uses a 3rd party service operated by Elementor.
More great features are on the horizon, so stay tuned!
More great features are on the horizon, so stay tuned!
## Related Plugins
* [Site Mailer](https://wordpress.org/plugins/site-mailer/): Effortlessly manage transactional emails with Site Mailer. High deliverability, logs and statistics, and no SMTP plugins needed
* [Image Optimizer](https://wordpress.org/plugins/image-optimization/): Compress and optimize your images, giving you leaner, faster websites. Automatically optimize any new image you upload to your site or run a bulk optimization process on all previously uploaded images.
**Contributors:** [pojo.me](https://profiles.wordpress.org/pojo.me), [KingYes](https://profiles.wordpress.org/KingYes), [ariel.k](https://profiles.wordpress.org/ariel.k), [jzaltzberg](https://profiles.wordpress.org/jzaltzberg), [bainternet](https://profiles.wordpress.org/bainternet)
**Tags:** Accessibility, A11y, Toolbar, Tools, wcag, accessible
**Requires at least:** 4.1
**Tested up to:** 6.1
**Requires PHP:** 5.4
**Stable tag:** 2.1.0
**License:** GPLv2 or later
The One Click Accessibility toolbar is the fastest plugin to help you make your WordPress website more accessible.
## Description ##
The One Click Accessibility toolbar is the fastest plugin to help you make your WordPress website more accessible.
While most accessibility issues cant be addressed without directly changing your content, One Click Accessibility adds a number of helpful accessibility features with the minimum amount of setup and without the need for expert knowledge.
**Accessibility Toolbar:**
Add a toolbar toggling hat allows you to set:
* Resize font (increase/decrease)
* Grayscale
* Negative Contrast
* High Contrast
* Light Background
* Links Underline
* Readable Font
* Link to Sitemap / Feedback / Help pages
**Accessibility Features:**
* Enable skip to content
* Add outline focus for focusable elements
* Remove the target attribute from links
* Add landmark roles to all links
* Customizer for style adjustment
**Contributions:**
Would you like to contribute to One Click Accessibility? You are more than welcome to submit your requests on the [GitHub repo](https://github.com/pojome/one-click-accessibility/). Also, if you have any notes about the code, please open a ticket on this issue tracker.
## Installation ##
## Installation
**Automatic Installation**
@ -58,16 +124,76 @@ Would you like to contribute to One Click Accessibility? You are more than welco
1. Go to the plugin page (under Dashboard > Accessibility)
1. Enjoy!
## Screenshots ##
## Frequently Asked Questions
1. Accessibility Customizer
2. Accessibility Toolbar
3. Grayscale Mode
4. Negative Contrast Mode
### What is Ally - Web Accessibility widget?
## Changelog ##
The Ally - Web Accessibility widget is a free, easy-to-use plugin designed to enhance website usability for individuals with disabilities. It is designed to help web creators make their websites more inclusive, and works seamlessly on all WordPress and Elementor websites.
### Can I customize the widget's appearance?
Yes, you can fully customize the widget. Adjust the icons style, color, and size, and personalize the header and footer to match your site's branding. You can also choose its placement—position it in any corner, at a custom location, or hide it on mobile or desktop—and decide which accessibility features are visible to your users.
### Does the widget support multiple languages?
Yes, the widget automatically adapts to your website's language, ensuring a smooth and consistent experience for every visitor.
### Can website visitors hide the widget?
Yes, website visitors can temporarily hide the widget. They can choose to dismiss it for the current session, for 24 hours, or even for an entire week.
### How do I set up the widget?
Once installed and connected, the widget appears on your site with default settings. You can customize its appearance and functionality via the WordPress admin panel. Additionally, you can link your existing accessibility statement or create a custom one using our built-in generator, which automatically creates a dedicated page for your statement.
### Is the widget compatible with WordPress and Elementor?
Yes, the widget is fully integrated with Elementor and works seamlessly with any WordPress site, delivering a smooth and effective accessibility experience.
### What is an accessibility statement, and how do I generate one?
An accessibility statement is a declaration of your commitment to providing an inclusive online experience. With the widget, you can generate a custom accessibility statement and seamlessly publish it on a dedicated page. Our Accessibility Statement Generator is provided as a tool to assist you in creating a customized accessibility statement for your website. However, the generated statement should be reviewed and tailored to ensure it accurately reflects your website's specific accessibility practices and compliance efforts.
## Screenshots
### 1. Design: Customize the button's icon, size, and color and widget branding.
![Design: Customize the button's icon, size, and color and widget branding.](https://ps.w.org/ally---web-accessibility-&-usability/assets/screenshot-1.png)
### 2. Position Settings: Set widget placement or hide it on desktop and mobile.
![Position Settings: Set widget placement or hide it on desktop and mobile.](https://ps.w.org/ally---web-accessibility-&-usability/assets/screenshot-2.png)
### 3. Capabilities: Toggle accessibility features on or off as needed.
![Capabilities: Toggle accessibility features on or off as needed.](https://ps.w.org/ally---web-accessibility-&-usability/assets/screenshot-3.png)
### 4. Accessibility Statement: Add or create a custom accessibility statement.
![Accessibility Statement: Add or create a custom accessibility statement.](https://ps.w.org/ally---web-accessibility-&-usability/assets/screenshot-4.png)
### 5. Widget on Site: This is how the accessibility widget appears on a live website.
![Widget on Site: This is how the accessibility widget appears on a live website.](https://ps.w.org/ally---web-accessibility-&-usability/assets/screenshot-5.png)
## Changelog
### 3.0.0 2025-02-18
* 🚀 Introducing Ally Web Accessibility! One Click Accessibility is now Ally Web Accessibility! Discover the new experience: Learn More.
* New: Brand-New Interface Redesigned for seamless touch-screen and mobile support.
* New: Revamped Infrastructure Overhauled backend to support new and future capabilities.
* New: Page Structure Overview Navigate your pages structure for better accessibility.
* New: Image Hiding Option Hides all images on the page to reduce distractions and make the page more readable.
* New: Pause animations option Stops animations running on the page option, helping users focus on content.
* New: Reading Mask option- helps users focus on specific text, reducing distractions and improving readability.
* New: Accessibility Statement Generator Easily generate an accessibility statement page for your site or link an existing one.
* New: Auto translations of the widget according to the page language
### 2.1.0 - 2022-12-18
### 2.1.0 - 2022-12-18 ###
* New: Added custom element ID for "Skip Content" link ([#35](https://github.com/pojome/one-click-accessibility/issues/35)), ([#36](https://github.com/pojome/one-click-accessibility/issues/36))
* Tweak: Added title for all SVG icons ([#30](https://github.com/pojome/one-click-accessibility/issues/30))
* Tweak: Add `role="button"` to the toggle ([#57](https://github.com/pojome/one-click-accessibility/pull/57))
@ -76,20 +202,24 @@ Would you like to contribute to One Click Accessibility? You are more than welco
* Fix: High Contrast and Negative Contrast still appear after disabled from the settings ([#39](https://github.com/pojome/one-click-accessibility/issues/39))
* Fix: Added compatibility with Twenty Twenty theme
### 2.0.3 - 2018-05-28 ###
### 2.0.3 - 2018-05-28
* Fix: Toolbar settings panel empty in non-English sites
* Fix: Avoid showing Help & Feedback links if not defined ([#27](https://github.com/pojome/one-click-accessibility/pull/27))
### 2.0.2 - 2018-05-03 ###
### 2.0.2 - 2018-05-03
* Fix! - Added default settings for toolbar title
* Fix! - Print CSS rules before code to fix responsive issues
* Fix! - Added visibility CSS to fix dependency issues
### 2.0.1 - 2018-05-02 ###
### 2.0.1 - 2018-05-02
* Fix! - Increased icon size in the toolbar
* Tweak! - Restored `sr-only` class for smooth update
### 2.0.0 - 2018-05-01 ###
### 2.0.0 - 2018-05-01
* Tweak! - Renamed plugin to One Click Accessibility
* Tweak! - Added an option to select an toolbar icon
* Tweak! - Split Settings Panel to Settings and Toolbar
@ -97,38 +227,49 @@ Would you like to contribute to One Click Accessibility? You are more than welco
* Tweak! - Removed Bootstrap dependency
* Tweak! - Removed Pojo Framework dependency
### 1.1.6 - 2017-10-26 ###
### 1.1.6 - 2017-10-26
* Fix! - Border with RGBA is hidden in high contrast mode
### 1.1.5 - 2016-03-09 ###
### 1.1.5 - 2016-03-09
* Tweak! - Fix minor style issue
### 1.1.4 - 2016-02-23 ###
### 1.1.4 - 2016-02-23
* Fixed! - tabindex on custom links issue
### 1.1.3 - 2016-02-21 ###
### 1.1.3 - 2016-02-21
* Tweak! - Resolve the focus issues by another way
### 1.1.2 - 2016-02-10 ###
### 1.1.2 - 2016-02-10
* Fixed! - Triggering blur after focus to avoid auto-focus by browser on tab reactivated
### 1.1.1 - 2016-02-08 ###
* Tweak! - Added more hebrew strings translate
### 1.1.1 - 2016-02-08
* Tweak! - Added more hebrew strings translate
* Tested up to WordPress v4.4
### 1.1.0 - 2016-02-07 ###
### 1.1.0 - 2016-02-07
* New! - Remember toolbar options
### 1.0.3 - 2016-01-27 ###
### 1.0.3 - 2016-01-27
* Fixed! - Issue with sidebar in Resize text
* Tweak! - Added more string fields for WPML/Polylang plugins
### 1.0.2 - 2015-12-08 ###
### 1.0.2 - 2015-12-08
* Tweak! - Fixed hebrew translate
### 1.0.1 - 2015-11-17 ###
### 1.0.1 - 2015-11-17
* New! - Added distance from top toolbar, in Desktop/Mobile devices
* Fixed! - Skip to content in Chrome
### 1.0.0 - 2015-10-29 ###
### 1.0.0 - 2015-10-29
* Initial Public Release!

8
assets/css/admin.css Normal file
View file

@ -0,0 +1,8 @@
#adminmenu .toplevel_page_accessibility-settings .wp-menu-image img {
width: 20px;
height: 20px;
padding: 7px 0;
opacity: 1;
filter: none;
}

54
assets/css/skip-link.css Normal file
View file

@ -0,0 +1,54 @@
.ea11y-skip-to-content-link {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
pointer-events: none;
position: fixed;
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
display: flex;
align-items: center;
gap: 8px;
border-radius: 12px;
border: 1px solid #2563EB;
background-color: #fff;
color: #0C0D0E;
text-decoration: none;
word-wrap: normal !important;
}
.ea11y-skip-to-content-link:focus {
clip: auto !important;
clip-path: none;
pointer-events: auto;
top: 30px;
left: 30px;
z-index: 100000;
height: auto;
width: auto;
padding: 16px 24px;
}
.ea11y-skip-to-content-link:focus + .ea11y-skip-to-content-backdrop {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.50);
}

4
assets/images/logo.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<circle cx="24.001" cy="24" r="24" fill="#fff"/>
<path fill="#000" d="M21.309.15A23.934 23.934 0 0 0 7.063 7.008C3.5 10.55 1.165 14.958.298 19.819c-.396 2.223-.396 6.255 0 8.479C2.05 38.075 9.927 45.95 19.707 47.703c2.223.396 6.256.396 8.48 0 4.861-.866 9.27-3.203 12.813-6.764 6.803-6.858 8.857-16.918 5.239-25.905C42.263 5.124 31.937-1.056 21.309.15Zm3.957 11.568c1.677.867 1.96 2.995.565 4.333-.452.434-.773.565-1.658.622-1.037.075-1.15.038-1.79-.509-1.489-1.319-1.338-3.353.301-4.352.716-.452 1.828-.49 2.582-.094Zm-8.103 5.294c4.353.377 9.196.377 13.982-.019 3.637-.301 3.957-.301 4.39-.019.604.396.66 1.432.114 1.979-.434.433-.697.49-4.259.847-2.28.245-2.676.34-2.959.697-.32.358-.339 1.037-.339 7.763 0 7.291 0 7.366-.414 7.8-.509.546-1.508.584-2.017.075-.414-.414-.49-.885-.772-4.258-.245-2.958-.302-3.221-.66-3.353-.603-.245-.791.357-1.017 3.297-.283 3.353-.452 4.22-.961 4.484-.584.301-1.357.207-1.828-.226l-.433-.396v-7.404c0-6.745-.02-7.424-.32-7.782-.302-.357-.717-.452-3.638-.753-2.92-.32-3.316-.396-3.618-.773-.433-.527-.433-1.47-.018-1.921.376-.415.621-.415 4.767-.038Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="8 5 25 25" fill="none">
<rect width="24" height="24" x="8" y="5.533" fill="#a7aaad" rx="12"/>
<path fill="#000" d="M20.124 13.944a1.406 1.406 0 1 0 0-2.811 1.406 1.406 0 0 0 0 2.811Z"/>
<path fill="#000" d="M22.187 14.335c-1.385.145-2.788.14-4.21 0l-3.133-.492a.817.817 0 0 0-.844.816c0 .442.278.758.793.817l3.56.469-.293 7.27a.747.747 0 0 0 1.493.056l.235-3.119a.376.376 0 0 1 .752 0l.235 3.12a.749.749 0 0 0 1.493-.057l-.26-6.403a.87.87 0 0 1 .84-.868l2.604-.468c.518-.093.793-.375.793-.817a.818.818 0 0 0-.844-.818l-3.218.493.004.001Z"/>
<path fill="#a7aaad" d="m16.795 16.28-1.49-.729c3.247.368 3.565.543 5.062.576-1.547.085-2.903.264-3.572.15v.002Z"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="7.5 5 25 25" fill="none">
<rect width="24" height="24" x="8" y="5.533" fill="#FF7BE5" rx="12"/>
<path fill="#fff" d="M20.124 13.944a1.406 1.406 0 1 0 0-2.811 1.406 1.406 0 0 0 0 2.811Z"/>
<path fill="#fff" d="M22.187 14.335c-1.385.145-2.788.14-4.21 0l-3.133-.492a.817.817 0 0 0-.844.816c0 .442.278.758.793.817l3.56.469-.293 7.27a.747.747 0 0 0 1.493.056l.235-3.119a.376.376 0 0 1 .752 0l.235 3.12a.749.749 0 0 0 1.493-.057l-.26-6.403a.87.87 0 0 1 .84-.868l2.604-.468c.518-.093.793-.375.793-.817a.818.818 0 0 0-.844-.818l-3.218.493.004.001Z"/>
<path fill="#FF7BE5" d="m16.795 16.28-1.49-.729c3.247.368 3.565.543 5.062.576-1.547.085-2.903.264-3.572.15v.002Z"/>
</svg>

After

Width:  |  Height:  |  Size: 754 B

33
bin/build Executable file
View file

@ -0,0 +1,33 @@
#!/bin/sh
echo "Web Accessibility build script:"
echo "basename: [$(basename "$0")]"
echo "dirname : [$(dirname "$0")]"
echo "pwd : [$(pwd)]"
BUILD_FILE="pojo-accessibility.zip"
#change working dir to the directory containing your package.json and composer.json
cd "$(dirname "$0")"
cd ..
# Remove old build file
echo "Removing old build file..."
rm -f $BUILD_FILE
# Run npm install
echo "Running npm install..."
npm install
# Run npm build
echo "Running npm install..."
npm run build
# Run composer install
echo "Running composer install..."
composer install --no-dev
# Pack everything excluding dev stuff
echo "Packing everything excluding .git folder into a zip file..."
zip -r pojo-accessibility.zip . -x "*.bpg.json" -x "*.babelrc" -x "*ruleset.xml" -x "*webpack.config.js" -x "*phpunit.xml.dist" -x "*.git*" -x "*.DS_Store" -x "node_modules/*" -x "*.gitignore" -x "*.gitmodules" -x "*.editorconfig" -x "*.gitattributes" -x "*.eslintrc" -x "*.eslintignore" -x "*.phpcs.xml.dist" -x "*.phpunit.xml.d" -x "*coverage/*" -x "*assets/dev*" -x ".phpunit.result*" -x "bin*" -x "*tests/*" -x "package.json" -x "composer.json" -x "composer.lock" -x "package-lock.json"
#
echo "Done."

View file

@ -0,0 +1,73 @@
<?php
namespace EA11y\Classes\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Database_Constants {
// column types
const INT = 'INT';
const VARCHAR = 'VARCHAR';
const TEXT = 'TEXT';
const LONGTEXT = 'LONGTEXT';
const DATETIME = 'DATETIME';
const TIMESTAMP = 'TIMESTAMP';
const ENUM = 'ENUM';
const BOOLEAN = 'BOOLEAN';
const TINYINT = 'TINYINT';
// Flags
const NOT_NULL = 'NOT NULL';
const NULL = 'NULL';
const DEFAULT = 'DEFAULT';
const AUTO_INCREMENT = 'AUTO_INCREMENT';
const COMMENT = 'COMMENT';
const UNSIGNED = 'UNSIGNED';
const ON_UPDATE = 'ON UPDATE';
const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP';
const KEY = 'KEY';
const PRIMARY_KEY = 'PRIMARY KEY';
const UNIQUE = 'UNIQUE';
public static function get_col_type( $type, $length = null ) {
switch ( $type ) {
case self::INT:
return 'INT(' . $length . ')';
case self::VARCHAR:
return 'VARCHAR(' . $length . ')';
case self::TEXT:
return 'TEXT';
case self::LONGTEXT:
return 'LONGTEXT';
case self::DATETIME:
return 'DATETIME';
case self::TIMESTAMP:
return 'TIMESTAMP';
case self::ENUM:
return 'ENUM(' . $length . ')';
case self::BOOLEAN:
return 'BOOLEAN';
case self::TINYINT:
return 'TINYINT(' . $length . ')';
default:
return 'VARCHAR(255)';
}
}
public static function build_flags_string( $flags = [] ): string {
return implode( ' ', $flags );
}
public static function build_key_string( $key, $column, $key_name = null ): string {
$key_name = $ke_name ?? $column;
return $key . ' `' . $key_name . '` (`' . $column . '`)';
}
public static function get_primary_key_string( $column ): string {
return 'PRIMARY KEY (`' . $column . '`)';
}
}

386
classes/database/entry.php Normal file
View file

@ -0,0 +1,386 @@
<?php
namespace EA11y\Classes\Database;
use EA11y\Classes\Database\Exceptions\Missing_Table_Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class DB_Entry_Base
* This class represents a single row (or part of a record) from a database table
* @property int|mixed|null id
*/
class Entry {
/**
* @var Table $db_table
*/
public $db_table = null;
protected $entry_data = [];
/**
* This class should return the name of the /Table/ derivative class representing the database table holding this
* entry
* @return string - name of the class
*/
protected static function get_helper_class(): string {
return '';
}
/**
* init_by
*
* Return the entry representing the first row in the table that satisfies the conditions set by the parameters
* and its associated data.
*
* @param string $by The name of the field to compare the value parameter to in case the
* /value/ parameter is a string, otherwise, ignored.
* @param string|array $value If a string, the value to compare against the column specified
* by the /by/ parameter.
* If an array, can be either a list of column => value entries for direct comparison
* joined with the AND logical operator, or in the format of column => [column =>
* string, value =>string|int|array<string|array>, comparison operator => string,
* relation_before=>string, optional, relation_after=>string, optional].
*
* @return Entry Returns the entry with the data found.
*/
private function init_by( string $by, $value ): Entry {
$data = static::get_by( $by, $value );
return $this->init_by_data( $data );
}
/**
* get_by
*
* Returns the first row in the table that satisfies the conditions set by the parameters.
*
* @param string $by The name of the field to compare the value parameter to in case the
* /value/ parameter is a string.
* Optional.
* Defaults to an empty string.
* @param array|string $value If a string, the value to compare against the column specified
* by the /by/ parameter.
* If an array, can be either a list of column => value entries for direct comparison
* joined with the AND logical operator, or in the format of column => [column =>
* string, value =>string|int|array<string|array>, comparison operator => string,
* relation_before=>string, optional, relation_after=>string, optional]. Optional.
* Defaults to an empty string.
*
* @return mixed|\stdClass|null An object representing the row or NULL in case of an error.
*/
protected function get_by( string $by = '', $value = '' ) {
$fields = '*';
$where = is_array( $value ) ? $value : [ $by => $value ];
return $this->db_table::first( $fields, $where );
}
/**
* init_by_data
*
* Returns an Entry object with its data set to the data passed by the /data/ parameter.
*
* @param array|object $data The data to set the entry with.
*
* @return $this Returns an entry loaded with the data passes in the parameter, or en Empty entry (representing
* null) on error
*/
protected function init_by_data( $data ): Entry {
if ( ! $data || is_wp_error( $data ) ) {
return $this->return_empty();
}
return $this->set_data( $data );
}
/**
* return_empty
*
* Returns an empty entry project, representing an empty set/null.
*
* NOTE: An empty entry cannot be saved in the database unless you either unset the
* /id/ field or set it for a valid value for an update.
*
* @return Entry Returns the object back
*/
private function return_empty(): Entry {
$this->set_data( [] );
return $this->set( 'id', 0 );
}
/**
* set_data
*
* Sets the data of this entry object
*
* @param array|object $data The data to set in the current object in the format of key => value
*
* @return $this Returns the object back
*/
public function set_data( $data ): Entry {
$this->entry_data = (array) $data;
return $this;
}
/**
* init_by_id
*
* Loads the Entry with data from the database fetched by the /id/ column compared to the value passed with the
* /id/ parameter.
*
* @param string $id The value of the /id/ field
*
* @return $this The Entry loaded with the data gotten from the database by the specified ID.
*/
protected function init_by_id( $id ): Entry {
$data = static::get_by( 'id', $id );
return $this->init_by_data( $data );
}
/**
* __get
* magic get properties
*
* Returns an Entry's data field by key name
*
* @param string $name The name of the field to return
*
* @return mixed|null The value of the field or null if it doesn't exist'
*/
public function __get( $name ) {
return $this->entry_data[ $name ] ?? null;
}
/**
* __set
* magic set properties
*
* Sets an Entry's data field value by the specified key
*
* @param string $name The key of the field being set
* @param mixed $value The new value for the field
*
* @return $this Returns the current object back
*/
public function __set( string $name, $value ) {
return $this->set( $name, $value );
}
/**
* set
*
* Sets and entry data field value by the specified key
*
* @param string $name The key of the field being set
* @param mixed $value The new value for the field
*
* @return $this Returns the current object back
*/
public function set( string $name, $value ): Entry {
$this->entry_data[ $name ] = $value;
return $this;
}
/**
* class_short_name
*
* Returns just the name of the class, without its namespace. Used for hooks.
* Taken from https://coderwall.com/p/cpxxxw/php-get-class-name-without-namespace
*
* NOTE: Called on a class without a namespace will return the name of the class
* without the first letter in the name
* @return string The name of the current class
*/
private function class_short_name(): string {
$class_name = get_called_class();
return ( substr( $class_name, strrpos( $class_name, '\\' ) + 1 ) );
}
/**
* trigger_change
*
* Raises action hooks following the create, delete and update operations.
* Raises the hook [ea11y/db/class_name>/change]. If the /event/
* parameter is not null, then it also raises the hook [ea11y/db/class_name/event].
*
* The parameters sent to both hooks is a reference to the current object and the value of the /data/ parameter.
*
* @param int|bool $data Numbers of rows changed or FALSE on database action failure
* @param string|null $event The name of the custom event hook to raise in addition to the /changed/ event hook
* Optional.
* Defaults to null. In this case, will raise only the defaults /changed/ event.
*/
private function trigger_change( $data, string $event = null ): void {
if ( $event ) {
/**
* event specific
* @var Entry $this
* @var false|int $data
*/
do_action( 'ea11y/db/' . $this->class_short_name() . '/' . $event, $this, $data );
}
/**
* entity change
* @var Entry $this
* @var false|int $data
*/
do_action( 'ea11y/db/' . $this->class_short_name() . '/change', $this, $data );
}
/**
* save
*
* Writes the entry to the database.
* If the entry has a field called /id/ set, will
* perform an update, if it doesn't, will perform an insert.
* Update and create function triggers the change action hooks and the respective custom event
* @return false|int The number of rows inserted or FALSE on error.
*/
public function save() {
if ( isset( $this->entry_data[ 'id' ] ) ) {
return $this->update( [ 'id' => $this->entry_data[ 'id' ] ] );
}
return $this->create();
}
/**
* delete
*
* Delete entries from the table.
* Bases on the field specified by the /by/ parameter and its current value.
* Triggers change action hooks and raises the /delete/ custom event.
*
* @param string $by The field name to delete by.
* Optional.
* Defaults to 'id'.
*
* @return false|int The number of rows deleted or false on error.
*/
public function delete( string $by = 'id' ) {
$results = $this->db_table::delete( [ $by => $this->{$by} ] );
$this->trigger_change( $results, 'delete' );
return $results;
}
/**
* update
*
* Updates the database with the data of this entry based on the conditions passed
* by the /where/ parameter.
* Triggers change action hooks and raises the /update/ custom event.
*
* NOTE: If no conditions are supplied, the update is going to be performed on all rows,
*
* @param array $where Array of column => (raw) values as a group of AND WHERE conditionals for the UPDATE
* statement. Optional. Defaults to an empty array.
*
* @return false|int The number of rows updated or false on error.
*/
public function update( array $where = [] ) {
$results = $this->db_table::update( $this->entry_data, $where );
$this->trigger_change( $results, 'update' );
return $results;
}
/**
* create
*
* Inserts the entry to the database table.
* Trigger change action hooks and raises the /create/ custom event,
*
* @param string $id On successful insertion, will set the field passes in the /id/ parameter
* ot the value of the last inserted ID as returned from the database.
* Optional.
* Defaults to 'id'
*
* @return false|int The numbers of rows affected or FALSE on error
*/
public function create( string $id = 'id' ) {
$results = $this->db_table::insert( $this->entry_data );
if ( $results ) {
// Set row id once created
$this->set( $id, $this->db_table::db()->insert_id );
}
$this->trigger_change( $results, 'create' );
return $results;
}
/**
* reset
*
* Clears all of this entry's data.
*/
public function reset(): void {
$this->entry_data = [];
}
/**
* @return bool
*/
public function exists(): bool {
return isset( $this->entry_data['id'] ) && 0 < $this->entry_data['id'];
}
/**
* DB_Entry_Base constructor.
* Uses the passed on arguments to initialize/set the Entry.
* Will through an exception in case the Entry's table property is not set correctly, and the /Table/ class
* linking this entry to a database table is not found.
*
* @param array $args The arguments to set this Entry by.
* If arguments /by/ and /value/ are set,
* will fetch the data according to the column name specified by /by/ and the value(s) specified
* by /value/. See doc for /init_by/ for details.
*
* if argument /data/ is set, will set the Entry's data with the data specified.
* See doc for /init_by_data/ for details.
*
* if argument /id/ is specified, will load the data of the row whose /id/ column has the value specified by the
* argument.
*
* Optional.
* Defaults to an empty array which will make this entry as the Empty Entry representing null,
* which cannot be saved unless properly modified to a non-null entry.
*
* @throws Missing_Table_Exception If the Entry table property is not found.
*/
public function __construct( array $args = [] ) {
$this->db_table = static::get_helper_class();
if ( empty( $this->db_table ) ) {
throw new Missing_Table_Exception();
}
if ( isset( $args[ 'by' ] ) && isset( $args[ 'value' ] ) ) {
return $this->init_by( $args[ 'by' ], $args[ 'value' ] );
}
if ( isset( $args[ 'data' ] ) ) {
return $this->init_by_data( $args[ 'data' ] );
}
if ( isset( $args[ 'id' ] ) ) {
return $this->init_by_id( $args[ 'id' ] );
}
return $this->return_empty();
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace EA11y\Classes\Database\Exceptions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Missing_Table_Exception
*
* represent the exception thrown when the Entry's get_helper_class() function is not properly overridden
* to return the Table-derivative class which represents the Entry's database representation.
*/
class Missing_Table_Exception extends \Exception {
protected $message = 'You must provide a PlatformCommon\Database\Table in extending class';
protected $code = 'missing_table';
}

574
classes/database/table.php Normal file
View file

@ -0,0 +1,574 @@
<?php
namespace EA11y\Classes\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Table
* This class represents a database table
*/
class Table {
/**
* The current table version. Will be compared to the version installed in the database
*/
const DB_VERSION = '1';
/**
* Should hold the name of the WordPress option to hold the database version
*/
const DB_VERSION_FLAG_NAME = '';
/**
* @var string database table name
*/
public static $table_name = '';
/**
* @var bool flag whether the table name is already prefixed or not
*/
protected static $table_prefixed = false;
/**
* Get a reference to the WordPress database object.
* @return \wpdb A reference to the WordPress database object
*/
public static function db(): \wpdb {
global $wpdb;
if ( ! static::$table_prefixed ) {
static::$table_prefixed = true;
static::set_table_prefix();
}
return $wpdb;
}
/**
* table_name
*
* Returns the name of the table including the table prefix
* @return string The full name of the table
*/
public static function table_name(): string {
return self::db()->prefix . static::$table_name;
}
/**
* set_table_prefix
*
* Saves the table name as a property in the WP database object and
* sets it value to the table name and its prefix.
*/
protected static function set_table_prefix(): void {
static::db()->{static::$table_name} = static::table_name();
}
/**
* get_columns
*
* Should return an array of table columns details in the format of
* column_name => [ type => db_type, key => key_data (optional), flags => other_modifiers (optional) ]
*
* NOTE: A primary key column named /id/ which is an auto-incremented int/big int is assumed to exist and must be one
* of the columns this function returns.
* @return array The table column data.
*/
public static function get_columns(): array {
return [];
}
/**
* get_extra_keys
*
* Extra keys to the table definitions to be merged with column key definitions
* @return string[] SQL table key definitions
*/
protected static function get_extra_keys(): array {
return [];
}
/**
* get_keys
*
* Extracts the key definitions from the table's columns and merges with
* any extra key definitions
* @return string[] SQL table key definitions
*/
protected static function get_keys(): array {
$columns = static::get_columns();
$keys = [];
foreach ( $columns as $column ) {
if ( ! isset( $column['key'] ) ) {
continue;
}
$keys[] = $column['key'];
}
return array_merge( $keys, static::get_extra_keys() );
}
/**
* install
*
* This function compares the version of the installed table and the current version as reported by
* the class.
* If the versions are different, the table will be installed or updated, and the option
* will be set to the current version.
*/
public static function install(): void {
$installed_ver = get_option( static::DB_VERSION_FLAG_NAME, -1 );
if ( static::DB_VERSION !== $installed_ver ) {
$sql = static::get_create_table_sql();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
update_option( static::DB_VERSION_FLAG_NAME, static::DB_VERSION, false );
}
static::set_table_prefix();
}
/**
* get_create_table_sql
*
* Generates the SQL command to run in the database to create the table
* based on the definitions of columns and keys.
* @return string The SQL command to create the table
*/
protected static function get_create_table_sql(): string {
$table = static::table_name();
$keys = static::get_keys();
$charset_collate = static::db()->get_charset_collate();
$table_columns = [];
$sql = [];
$sql[] = 'CREATE TABLE ' . $table . ' (';
$columns = static::get_columns();
foreach ( $columns as $column_name => $column ) {
$table_columns[] = sprintf( '`%s` %s %s,',
$column_name,
$column['type'],
$column['flags'] ?? ''
);
}
$sql[] = "\t" . implode( "\n\t", $table_columns );
$sql[] = "\t" . implode( ",\n\t", $keys );
$sql[] = ") AUTO_INCREMENT=11 {$charset_collate};";
return implode( "\n", $sql );
}
/**
* where
*
* Generates a proper WHERE clause for an SQL query.
* @param string|array $where Either a string of where clause (returns as is) or an array of
* conditions join with an AND, and in the format of column => (int|string) for exact value comparison
* or in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
*
* @return string WHERE clause built from the function input
*/
public static function where( $where ): string {
if ( ! is_array( $where ) ) {
return $where;
}
$needs_relationship = false;
$where_string = '';
foreach ( $where as $key => $filter ) {
if ( ! is_array( $filter ) ) {
if ( $needs_relationship ) {
$where_string .= ' AND';
}
$where_string .= ' ' . self::get_where_string( $key, $filter );
$needs_relationship = true;
continue;
}
$where_string .= self::maybe_add_relation( $filter );
$where_string .= self::get_where_string( $filter['column'], $filter['value'], $filter['operator'] );
$where_string .= self::maybe_add_relation( $filter, false );
}
return $where_string;
}
/**
* maybe_add_relation
*
* Adds a logical relation ship (AND, OR...) if exists, based on the position (before|after) related to the condition.
* @param array $filter Object array describing the where condition that may contain the keys /relation_before/
* or /relation_after/ containing the logical relationship to add to the main WHERE condition.
* @param bool $is_before Whether the current position in the text is before the condition the object describes.
* Optional.
* Defaults to TRUE.
*
* @return string If the logical relationship exists, returns it. Otherwise - an empty string.
*/
private static function maybe_add_relation( array $filter, bool $is_before = true ): string {
$key_to_check = $is_before ? 'relation_before' : 'relation_after';
return isset( $filter[ $key_to_check ] ) ? ' ' . $filter[ $key_to_check ] : ' ';
}
/**
* get_where_string
*
* @param string $key The column name in the condition
* @param string|int|array $value The value being compared. If an array will be translated to a set.
* @param string $operator The comparison operator.
* Optional.
* Defaults to '='.
* @param null $format Unused.
*
* @return string An SQL condition based on the parameters.
*/
private static function get_where_string( string $key, $value, string $operator = '=', $format = null ): string {
$param_string = is_int( $value ) ? '%d' : '%s';
if ( is_array( $value ) ) {
$param_string = '(';
$count = count( $value );
for ( $i = 0; $i < $count; $i++ ) {
$param_string .= is_int( $value[ $i ] ) ? '%d' : '%s';
$param_string .= ( $i !== $count - 1 ) ? ', ' : '';
}
$param_string .= ')';
}
return static::db()->prepare( "$key $operator $param_string", $value );
}
/**
* get_columns_for_insert
* This function tries to get the column names for an INSERT operation based on the table column
* definition, and if that fails based on the /data/ parameter.
* The function will remove any column called /id/.
* @param mixed $data If a two-dimensional array, where the elements are in the form of column => value,
* the function will try to get the names of the columns off the first element.
*
* @return false|string A string representing the list of columns, comma separated and surrounded by parenthesis;
* or false in case of function failure.
*/
private static function get_columns_for_insert( $data ) {
$cols = static::get_columns();
if ( count( $cols ) ) {
$columns = array_keys( $cols );
} elseif ( is_array( $data ) ) {
//try to get from data
$columns = array_keys( $data[0] );
}
if ( empty( $columns ) || ! is_array( $columns ) ) {
return false;
}
// remove id $column
if ( ! empty( $columns['id'] ) ) {
unset( $columns['id'] );
}
$index = array_search( 'id', $columns, true );
if ( false !== $index ) {
unset( $columns[ $index ] );
}
return ' (`' . implode( '`,`', $columns ) . '`) ';
}
/**
* select_var
*
* Selects a single cell in the table and returns its value as string.
* Will return the first cell of the first row in the result set.
* @param string|array $fields A string of comma-separated list, or an array of columns from the table.
* Optional.
* Defaults to '*' (all table columns)
* @param string|array $where A string of WHERE conditions or an array of column => value entries connected with the
* AND logical operator. Or in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
* Optional.
* Defaults tp '1', which is evaluated to true and will bring all records (no condition).
* @param int|null $limit Limit the number of results to return to this number, or NULL for no limit.
* Optional.
* Defaults to NULL (no limit)
* @param int|null $offset Skip this number of results or NULL for no skip
* Optional.
* Defaults to NULL (no offset)
* @param string $join JOIN table clause.
* Optional.
* Defaults to an empty string (no join)
*
* @return string|null The query result or NULL on error.
*/
public static function select_var( $fields = '*', $where = '1', int $limit = null, int $offset = null, string $join = '' ): ?string {
return static::db()->get_var( static::build_sql_string( $fields, $where, $limit, $offset, $join ) );
}
/**
* build_sql_string
*
* Generates a SELECT query based on the function input.
* @param string|array $fields A string of comma-separated list, or an array of columns from the table.
* Optional.
* Defaults to '*' (all table columns)
* @param string|array $where A string of WHERE conditions or an array of column => value entries connected with
* the AND logical operator. r in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
* Optional.
* Defaults to '1', which is evaluated to true and will bring all records (no condition).
* @param int|null $limit Maximum number of results to return.
* Optional.
* Defaults to NULL (no limit)
* @param int|null $offset Start the results from a certain ordinal position.
* Optional.
* Defaults to NULL (no offset)
* @param string $join A table JOIN clause.
* Optional.
* Defaults to an empty string (no join)
* @param array $order_by an array of column => direction (asc|desc) to sort the results by.
* Optional.
* Defaults to an empty array (Default sort).
*
* @return string The SQL SELECT statement built according to the function parameters.
*/
private static function build_sql_string( $fields = '*', $where = '1', int $limit = null, int $offset = null, string $join = '', array $order_by = [] ): string {
if ( is_array( $fields ) ) {
$fields = implode( ', ', $fields );
}
$db = static::db();
$query_string = 'SELECT %s FROM %s %s WHERE %s';
$query_string = sprintf( $query_string,
$fields,
static::table_name(),
$join,
static::where( $where )
);
if ( $order_by ) {
$query_string .= static::build_order_by_sql_string( $order_by );
}
if ( $limit ) {
$query_string .= $db->prepare( ' LIMIT %d', $limit );
}
if ( $offset ) {
$query_string .= $db->prepare( ' OFFSET %d', $offset );
}
return $query_string;
}
/**
* build_order_by_sql_string
*
* Generates the ORDER BY clause of the query based on the passed on parameter
* @param array<string, string> $order_by An array of column => direction (asc/desc) pairs
*
* @return string The ORDER BY clause for a query
*/
public static function build_order_by_sql_string( array $order_by ): string {
return ' ORDER BY ' . implode( ', ', array_map( function( $column, $direction ) {
return "{$column} {$direction}";
}, array_keys( $order_by ), $order_by ) );
}
/**
* select
*
* Runs a SELECT query and returns the results as an array of objects, each object represents a row,
* @param string|array $fields A string of comma-separated list, or an array of columns from the table.
* Optional.
* Defaults to '*' (all table columns)
* @param string|array $where A string of WHERE conditions, or an array of column => value enteries
* for direct comparison joined with the AND logical operator, or in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
* Optional.
* Defaults to '1', which is evaluated to true and will bring all records (no condition).
* @param int|null $limit Maximum number of results to return.
* Optional.
* Defaults to NULL (no limit)
* @param int|null $offset Start the results from a certain ordinal position.
* Optional.
* Defaults to NULL (no offset)
* @param string|array $join A table JOIN clause.
* Optional.
* Defaults to an empty string (no join)
* @param array $order_by an array of column => direction (asc|desc) to sort the results by.
* Optional.
* Defaults to an empty array (Default sort).
*
* @return array|object|\stdClass[]|null On success, an array of objects. Null on error.
*/
public static function select( $fields = '*', $where = '1', int $limit = null, int $offset = null, $join = '', array $order_by = [] ) {
// TODO: handle $wpdb->last_error
$query = static::build_sql_string( $fields, $where, $limit, $offset, $join, $order_by );
return static::db()->get_results( $query );
}
/**
* get_col
*
* Returns the first column of the result set.
* @param string $column The column to return.
* Optional.
* Defaults to an empty string.
* @param string|array $where A string of WHERE conditions or an array of column => value entries
* for direct comparison joined with the AND logical operator or in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
* Optional.
* Defaults to '1', which is evaluated to true and will bring all records (no condition).
* @param int|null $limit Maximum number of results to return.
* Optional.
* Defaults to NULL (no limit)
* @param int|null $offset Start the results from a certain ordinal position.
* Optional.
* Defaults to NULL (no offset)
* @param string $join A table JOIN clause.
* Optional.
* Defaults to an empty string (no join)
* @param array $order_by an array of column => direction (asc|desc) to sort the results by.
* Optional.
* Defaults to an empty array (Default sort).
*
* @return string[] Array of the values of the column as strings, or an empty one on error.
*/
public static function get_col( string $column = '', $where = '1', int $limit = null, int $offset = null, string $join = '', array $order_by = [] ) : array {
return static::db()->get_col( static::build_sql_string( $column, $where, $limit, $offset, $join, $order_by ) );
}
/**
* first
*
* Returns the first row in the table according to the query filters.
* @param string|array $fields A string of comma-separated list, or an array of columns from the table.
* Optional.
* Defaults to '*' (all table columns)
* @param string|array $where A string of WHERE conditions or an array of column => value entries
* for direct comparison joined with the logical AND operator,
* or in the format of column => [column => string, value =>string|int|array<string|array>,
* comparison operator => string, relation_before=>string, optional, relation_after=>string, optional]
* Optional.
* Defaults to '1', which is evaluated to true and will bring all records (no condition).
* @param int $limit Unnecessary since we are only returning the first row.
* Optional.
* Defaults to 1.
* @param null $offset Start the results from a certain ordinal position.
* Optional.
* Defaults to NULL (no offset)
* @param string $join A table JOIN clause.
* Optional.
* Defaults to an empty string (no join)
*
* @return \stdClass|null An object representing the first row, or null on error
*/
public static function first( $fields = '*', $where = '1', int $limit = 1, $offset = null, string $join = '' ): ?\stdClass {
$result = static::select( $fields, $where, $limit, $offset, $join );
return ( ! empty( $result[0] ) ) ? $result[0] : null;
}
/**
* replace
*
* Replace a row in a table if it exists or insert a new row in a table if the row does not already exist.
* @param array $data Array of data in the form of column => (raw) value.
* Optional.
* Defaults to an empty array.
*
* @return false|int The number of rows affected or FALSE on error.
*/
public static function replace( array $data = [] ) {
return static::db()->replace( static::table_name(), $data );
}
/**
* insert
*
* Insert a single row into the table.
* @param array $data Array of data to insert in column => (raw) value format.
* Optional, defaults to an empty array.
*
* @return false|int The number of rows affected or FALSE on error
*/
public static function insert( array $data = [] ) {
return static::db()->insert( static::table_name(), $data );
}
/**
* insert_many
* Performs a bulk INSERT of many datasets/rows to the table
*
* @param array $data Optional. Defaults to an empty array.
* An array of datasets to be INSERTed into the table. Each value needs to be seperated by a comma,
* each data set needs to be surrounded by parenthesis.
* @param string|null $columns Optional. The columns being inserted. Defaults to NULL.
* Either a string of comma-separated column names surrounded by parenthesis, or NULL for the
* function to try guessing based on the data and column definitions.
*
* @return false|int Number of rows affected or false on error
*/
public static function insert_many( array $data = [], string $columns = null ) {
if ( null === $columns ) {
$columns = static::get_columns_for_insert( $data );
if ( ! $columns ) {
return false;
}
}
$insert_sql = 'INSERT INTO ' . static::table_name() . $columns . ' VALUES ' . implode( ",\n", $data ) . ';';
return static::db()->query( $insert_sql ); // no params so no need for `prepare`.
}
/**
* update
*
* Updates data in the table, based on where conditionals
* @param array<string, mixed> $data Optional. Array of column => (raw) values to be updated.
* Defaults to an empty array.
* @param array<string, mixed> $where Optional. Array of column => (raw) values as a group of AND
* WHERE conditionals for the UPDATE statement. Defaults to an empty array.
*
* @return false|int The numbers of rows affected, or FALSE on error
*/
public static function update( array $data = [], array $where = [] ) {
return static::db()->update( static::table_name(), $data, $where );
}
/**
* delete
*
* Delete rows from this table based on optional where conditions.
*
* @param array<string, mixed> $where Optional. And array of column => (raw) values
* as a group of AND conditions for the DELETE statement. Defaults to an empty array.
*
* @return false|int The number of rows updated, or false on error.
*/
public static function delete( array $where = [] ) {
return static::db()->delete( static::table_name(), $where );
}
/**
* query
*
* Execute any SQL query on the database.
* It is best used when there is a need for specific,
* custom, or otherwise complex SQL queries.
* @param string $query The query to be executed. Defaults to an empty string
*
* @return false|int Boolean true for CREATE, ALTER, TRUNCATE and DROP queries.
* Number of rows affected/selected for all other queries. Boolean false on error.
*/
public static function query( string $query = '' ) {
return static::db()->query( $query );
}
/**
* get_class_name
*
* Returns the name of this /Table/ class (or its derivative)
* @return string - The name of the current class
*/
public static function get_class_name(): string {
return get_called_class();
}
}

37
classes/logger.php Normal file
View file

@ -0,0 +1,37 @@
<?php
namespace EA11y\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Logger {
public const LEVEL_ERROR = 'error';
public const LEVEL_WARN = 'warn';
public const LEVEL_INFO = 'info';
private static function log( string $log_level, $message ): void {
$backtrace = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$class = $backtrace[2]['class'] ?? null;
$type = $backtrace[2]['type'] ?? null;
$function = $backtrace[2]['function'];
if ( $class ) {
$message = '[EA11Y]: ' . $log_level . ' in ' . "$class$type$function()" . ': ' . $message;
} else {
$message = '[EA11Y]: ' . $log_level . ' in ' . "$function()" . ': ' . $message;
}
error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
public static function error( $message ): void {
self::log( self::LEVEL_ERROR, $message );
}
public static function info( $message ): void {
self::log( self::LEVEL_INFO, $message );
}
}

268
classes/module-base.php Normal file
View file

@ -0,0 +1,268 @@
<?php
namespace EA11y\Classes;
use EA11y\Modules\Legacy\Module as LegacyModule;
use ReflectionClass;
use ReflectionException;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Module Base.
*
* An abstract class providing the properties and methods needed to
* manage and handle modules in inheriting classes.
*
* @abstract
*/
abstract class Module_Base {
/**
* Module class reflection.
*
* Holds the information about a class.
* @access private
*
* @var ReflectionClass
*/
private $reflection = null;
/**
* Module components.
*
* Holds the module components.
* @access private
*
* @var array
*/
private $components = [];
/**
* Module routes.
*
* Holds the module registered routes.
* @access public
*
* @var array
*/
public $routes = [];
/**
* Module instance.
*
* Holds the module instance.
* @access protected
*
* @var Module_Base[]
*/
// @phpcs:ignore
protected static $_instances = [];
/**
* Get module name.
*
* Retrieve the module name.
* @access public
* @abstract
*
* @return string Module name.
*/
abstract public function get_name();
/**
* Instance.
*
* Ensures only one instance of the module class is loaded or can be loaded.
* @access public
* @static
*
* @return Module_Base An instance of the class.
*/
public static function instance() {
$class_name = static::class_name();
if ( empty( static::$_instances[ $class_name ] ) ) {
static::$_instances[ $class_name ] = new static(); // @codeCoverageIgnore
}
return static::$_instances[ $class_name ];
}
public static function set_instance( $instance ) {
$class_name = static::class_name();
static::$_instances[ $class_name ] = $instance;
}
/**
* is_active
* @access public
* @static
* @return bool
*/
public static function is_active(): bool {
return ! LegacyModule::is_active();
}
/**
* Class name.
*
* Retrieve the name of the class.
* @access public
* @static
*/
public static function class_name() {
return get_called_class();
}
/**
* Clone.
*
* Disable class cloning and throw an error on object clone.
*
* The whole idea of the singleton design pattern is that there is a single
* object. Therefore, we don't want the object to be cloned.
*
* @access public
*/
public function __clone() {
// Cloning instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, esc_html__( 'Something went wrong.', 'pojo-accessibility' ), '0.0.1' ); // @codeCoverageIgnore
}
/**
* Wakeup.
*
* Disable un-serializing of the class.
* @access public
*/
public function __wakeup() {
// un-serializing instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, esc_html__( 'Something went wrong.', 'pojo-accessibility' ), '0.0.1' ); // @codeCoverageIgnore
}
/**
* @access public
*/
public function get_reflection() {
if ( null === $this->reflection ) {
try {
$this->reflection = new ReflectionClass( $this );
} catch ( ReflectionException $e ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( $e->getMessage() );
}
}
}
return $this->reflection;
}
/**
* Add module component.
*
* Add new component to the current module.
* @access public
*
* @param string $id Component ID.
* @param mixed $instance An instance of the component.
*/
public function add_component( $id, $instance ) {
$this->components[ $id ] = $instance;
}
/**
* Add module route.
*
* Add new route to the current module.
* @access public
*
* @param string $id Route ID.
* @param mixed $instance An instance of the route.
*/
public function add_route( string $id, $instance ) {
$this->routes[ $id ] = $instance;
}
/**
* @access public
* @return Module[]
*/
public function get_components(): array {
return $this->components;
}
/**
* Get module component.
*
* Retrieve the module component.
* @access public
*
* @param string $id Component ID.
*
* @return mixed An instance of the component, or `false` if the component
* doesn't exist.
* @codeCoverageIgnore
*/
public function get_component( $id ) {
if ( isset( $this->components[ $id ] ) ) {
return $this->components[ $id ];
}
return false;
}
/**
* Retrieve the namespace of the class
*
* @access public
* @static
*/
public static function namespace_name() {
$class_name = static::class_name();
return substr( $class_name, 0, strrpos( $class_name, '\\' ) );
}
public static function routes_list() : array {
return [];
}
public static function component_list() : array {
return [];
}
/**
* Adds an array of components.
* Assumes namespace structure contains `\Components\`
*
* @param array $components_ids => component's class name.
*/
public function register_components( $components_ids = null ) {
$namespace = static::namespace_name();
$components_ids = $components_ids ?? static::component_list();
foreach ( $components_ids as $component_id ) {
$class_name = $namespace . '\\Components\\' . $component_id;
$this->add_component( $component_id, new $class_name() );
}
}
/**
* Adds an array of routes.
* Assumes namespace structure contains `\Rest\`
*/
public function register_routes() {
$namespace = static::namespace_name();
$routes_ids = static::routes_list();
foreach ( $routes_ids as $route_id ) {
$class_name = $namespace . '\\Rest\\' . $route_id;
$this->add_route( $route_id, new $class_name() );
}
}
}

387
classes/rest/route.php Normal file
View file

@ -0,0 +1,387 @@
<?php
namespace EA11y\Classes\Rest;
use ReflectionClass;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_User;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Route {
/**
* Should the endpoint be validated for user authentication?
* If set to TRUE, the default permission callback will make sure the user is logged in and has a valid user id
* @var bool
*/
protected $auth = true;
/**
* holds current authenticated user id
* @var int
*/
protected $current_user_id;
/**
* Rest Endpoint namespace
* @var string
*/
protected $namespace = 'ea11y/v1';
/**
* @var array The valid HTTP methods. The list represents the general REST methods. Do not modify.
*/
private $valid_http_methods = [
'GET',
'PATCH',
'POST',
'PUT',
'DELETE',
];
/**
* Route_Base constructor.
*/
public function __construct() {
add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
}
/**
* rest_api_init
*
* Registers REST endpoints.
* Loops through the REST methods for this route, creates an endpoint configuration for
* each of them and registers all the endpoints with the WordPress system.
*/
public function rest_api_init(): void {
$methods = $this->get_methods();
if ( empty( $methods ) ) {
return;
}
//$callbacks = false;
$callbacks = [];
foreach ( (array) $methods as $method ) {
if ( ! in_array( $method, $this->valid_http_methods, true ) ) {
continue;
}
$callbacks[] = $this->build_endpoint_method_config( $method );
}
$arguments = $this->get_arguments();
if ( ! $callbacks && empty( $arguments ) ) {
return;
}
$arguments = array_merge( $arguments, (array) $callbacks );
register_rest_route( $this->namespace, '/' . $this->get_endpoint() . '/', $arguments, $this->override );
}
/**
* get_methods
* Rest Endpoint methods
*
* Returns an array of the supported REST methods for this route
* @return array<string> REST methods being configured for this route.
*/
abstract public function get_methods(): array;
/**
* get_callback
*
* Returns a reference to the callback function to handle the REST method specified by the /method/ parameter.
* @param string $method The REST method name
*
* @return callable A reference to a member function with the same name as the REST method being passed as a parameter,
* or a reference to the default function /callback/.
*/
public function get_callback_method( string $method ): callable {
$method_name = strtolower( $method );
$callback = $this->method_exists_in_current_class( $method_name ) ? $method_name : 'callback';
return [ $this, $callback ];
}
/**
* get_permission_callback_method
*
* Returns a reference to the permission callback for the method if exists or the default one if it doesn't.
* @param string $method The REST method name
*
* @return callable If a method called (rest-method)_permission_callback exists, returns a reference to it, otherwise
* returns a reference to the default member method /permission_callback/.
*/
public function get_permission_callback_method( string $method ): callable {
$method_name = strtolower( $method );
$permission_callback_method = $method_name . '_permission_callback';
$permission_callback = $this->method_exists_in_current_class( $permission_callback_method ) ? $permission_callback_method : 'permission_callback';
return [ $this, $permission_callback ];
}
/**
* maybe_add_args_to_config
*
* Checks if the class has a method call (rest-method)_args.
* If it does, the function calls it and adds its response to the config object passed to the function, under the /args/ key.
* @param string $method The REST method name being configured
* @param array $config The configuration object for the method
*
* @return array The configuration object for the method, possibly after being amended
*/
public function maybe_add_args_to_config( string $method, array $config ): array {
$method_name = strtolower( $method );
$method_args = $method_name . '_args';
if ( $this->method_exists_in_current_class( $method_args ) ) {
$config['args'] = $this->{$method_args}();
}
return $config;
}
/**
* maybe_add_response_to_swagger
*
* If the function method /(rest-method)_response_callback/ exists, adds the filter
* /swagger_api_response_(namespace with slashes replaced with underscores)_(endpoint with slashes replaced with underscores)/
* with the aforementioned function method.
* This filter is used with the WP API Swagger UI plugin to create documentation for the API.
* The value being passed is an array: [
'200' => ['description' => 'OK'],
'404' => ['description' => 'Not Found'],
'400' => ['description' => 'Bad Request']
]
* @param string $method REST method name
*/
public function maybe_add_response_to_swagger( string $method ): void {
$method_name = strtolower( $method );
$method_response_callback = $method_name . '_response_callback';
if ( $this->method_exists_in_current_class( $method_response_callback ) ) {
$response_filter = $method_name . '_' . str_replace(
'/',
'_',
$this->namespace . '/' . $this->get_endpoint()
);
add_filter( 'swagger_api_responses_' . $response_filter, [ $this, $method_response_callback ] );
}
}
/**
* build_endpoint_method_config
*
* Builds a configuration array for the endpoint based on the presence of the callback, permission, additional parameters,
* and response to Swagger member functions.
* @param string $method The REST method for the endpoint
*
* @return array The endpoint configuration for the method specified by the parameter
*/
private function build_endpoint_method_config( string $method ): array {
$config = [
'methods' => $method,
'callback' => $this->get_callback_method( $method ),
'permission_callback' => $this->get_permission_callback_method( $method ),
];
$config = $this->maybe_add_args_to_config( $method, $config );
return $config;
}
/**
* method_exists_in_current_class
*
* Uses reflection to check if this class has the /method/ method.
* @param string $method The name of the method being checked.
*
* @return bool TRUE if the class has the /method/ method, FALSE otherwise.
*/
private function method_exists_in_current_class( string $method ): bool {
$class_name = get_class( $this );
try {
$reflection = new ReflectionClass( $class_name );
} catch ( \ReflectionException $e ) {
return false;
}
if ( ! $reflection->hasMethod( $method ) ) {
return false;
}
$method_ref = $reflection->getMethod( $method );
return ( $method_ref && $class_name === $method_ref->class );
}
/**
* @param $data
*
* @return WP_REST_Response
*/
public function respond_success_json( $data = [] ): WP_REST_Response {
return new WP_REST_Response([
'success' => true,
'data' => $data,
]);
}
/**
* @param array{message: string, code: string} $data
*
* @return WP_Error
*/
public function respond_error_json( array $data ): WP_Error {
if ( ! isset( $data['message'] ) || ! isset( $data['code'] ) ) {
_doing_it_wrong(
__FUNCTION__,
esc_html__( 'Both `message` and `code` keys must be provided', 'pojo-accessibility' ),
'1.0.0'
); // @codeCoverageIgnore
}
return new WP_Error(
$data['code'] ?? 'internal_server_error',
$data['message'] ?? esc_html__( 'Internal server error', 'pojo-accessibility' ),
);
}
public function verify_capability( $capability = 'manage_options' ) {
if ( ! current_user_can( $capability ) ) {
return $this->respond_error_json([
'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'pojo-accessibility' ),
'code' => '401 Unauthorized',
]);
}
}
/**
* permission_callback
* Permissions callback fallback for the endpoint
* Gets the current user ID and sets the /current_user_id/ property.
* If the /auth/ property is set to /true/ will make sure that the user is logged in (has an id greater than 0)
*
* @param WP_REST_Request $request unused
*
* @return bool TRUE, if permission granted, FALSE otherwise
*/
public function permission_callback( WP_REST_Request $request ): bool {
// try to get current user
$this->current_user_id = get_current_user_id();
if ( $this->auth ) {
return $this->current_user_id > 0;
}
return true;
}
/**
* callback
* Fallback callback function, returns a response consisting of the string /ok/.
*
* @param WP_REST_Request $request unused
*
* @return WP_REST_Response Default Response of the string 'ok'.
*/
public function callback( WP_REST_Request $request ): WP_REST_Response {
return rest_ensure_response( [ 'OK' ] );
}
/**
* respond_wrong_method
*
* Creates a WordPress error object with the /rest_no_route/ code and the message and code supplied or the defaults.
* @param null $message The error message for the wrong method.
* Optional.
* Defaults to null, which makes sets the message to /No route was found matching the URL and request method/
* @param int $code The HTTP status code.
* Optional.
* Defaults to 404 (Not found).
*
* @return WP_Error The WordPress error object with the error message and status code supplied
*/
public function respond_wrong_method( $message = null, int $code = 404 ): WP_Error {
if ( null === $message ) {
$message = __( 'No route was found matching the URL and request method', 'pojo-accessibility' );
}
return new WP_Error( 'rest_no_route', $message, [ 'status' => $code ] );
}
/**
* respond_with_code
* Create a new /WP_REST_Response/ object with the specified data and HTTP response code.
*
* @param array|null $data The data to return in this response
* @param int $code The HTTP response code.
* Optional.
* Defaults to 200 (OK).
*
* @return WP_REST_Response The WordPress response object loaded with the data and the response code.
*/
public function respond_with_code( ?array $data = null, int $code = 200 ): WP_REST_Response {
return new WP_REST_Response( $data, $code );
}
/**
* get_user_from_request
*
* Returns the current user object.
* Depends on the property /current_user_id/ to be set.
* @return WP_User|false The user object or false if not found or on error.
*/
public function get_user_from_request() {
return get_user_by( 'id', $this->current_user_id );
}
/**
* get_arguments
* Rest Endpoint extra arguments
* @return array Additional arguments for the route configuration
*/
public function get_arguments(): array {
return [];
}
/**
* get_endpoint
* Rest route Endpoint
* @return string Endpoint uri component (comes after the route namespace)
*/
abstract public function get_endpoint(): string;
/**
* get_name
* @return string The name of the route
*/
abstract public function get_name(): string;
/**
* get_self_url
*
* @param string $endpoint
*
* @return string
*/
public function get_self_url( string $endpoint = '' ): string {
return rest_url( $this->namespace . '/' . $endpoint );
}
public function verify_nonce( $nonce = '', $name = '' ) {
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), $name ) ) {
return $this->respond_error_json([
'message' => esc_html__( 'Invalid nonce', 'pojo-accessibility' ),
'code' => 'bad_request',
]);
}
}
public function verify_nonce_and_capability( $nonce = '', $name = '', $capability = 'manage_options' ) {
$this->verify_nonce( $nonce, $name );
if ( ! current_user_can( $capability ) ) {
return $this->respond_error_json([
'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'pojo-accessibility' ),
'code' => 'bad_request',
]);
}
}
}

207
classes/services/client.php Normal file
View file

@ -0,0 +1,207 @@
<?php
namespace EA11y\Classes\Services;
use EA11y\Modules\Connect\Classes\Data;
use EA11y\Modules\Connect\Classes\Exceptions\Service_Exception;
use EA11y\Modules\Connect\Classes\Service;
use EA11y\Modules\Connect\Module as Connect;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class Client
*/
class Client {
private const BASE_URL = 'https://my.elementor.com/apps/api/v1/a11y/';
private bool $refreshed = false;
public static ?Client $instance = null;
/**
* set_instance
* used for testing
* @param $instance
*/
public static function set_instance( $instance ) {
self::$instance = $instance;
}
/**
* get_instance
* @return Client|null
*/
public static function get_instance(): ?Client {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public static function get_site_info(): array {
return [
// Which API version is used.
'app_version' => EA11Y_VERSION,
// Which language to return.
'site_lang' => get_bloginfo( 'language' ),
// site to connect
'site_url' => trailingslashit( home_url() ),
// current user
'local_id' => get_current_user_id(),
// User Agent
'user_agent' => ! empty( $_SERVER['HTTP_USER_AGENT'] )
? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
: 'Unknown',
'webhook_url' => self::webhook_endpoint(),
];
}
/**
* Log update endpoint
* @return string
*/
private static function webhook_endpoint(): string {
$blog_id = get_current_blog_id();
return get_rest_url( $blog_id, 'a11y/v1/webhooks/common' );
}
public function make_request( $method, $endpoint, $body = [], array $headers = [], $send_json = false ) {
$headers = array_replace_recursive( [
'x-elementor-a11y' => EA11Y_VERSION,
'x-elementor-apps' => 'a11y',
], $headers );
$headers = array_replace_recursive(
$headers,
$this->is_connected() ? $this->generate_authentication_headers( $endpoint ) : []
);
$body = array_replace_recursive( $body, $this->get_site_info() );
if ( $send_json ) {
$headers['Content-Type'] = 'application/json';
$body = wp_json_encode( $body );
}
return $this->request(
$method,
$endpoint,
[
'timeout' => 100,
'headers' => $headers,
'body' => $body,
]
);
}
public static function get_client_base_url() {
return apply_filters( 'ea11y_client_base_url', self::BASE_URL );
}
private static function get_remote_url( $endpoint ): string {
return self::get_client_base_url() . $endpoint;
}
protected function is_connected(): bool {
return Connect::is_connected();
}
public function add_bearer_token( $headers ) {
if ( $this->is_connected() ) {
$headers['Authorization'] = 'Bearer ' . Data::get_access_token();
}
return $headers;
}
protected function generate_authentication_headers( $endpoint ): array {
$headers = [
'endpoint' => $endpoint,
];
return $this->add_bearer_token( $headers );
}
/**
* @throws Service_Exception
*/
protected function request( $method, $endpoint, $args = [] ) {
$args['method'] = $method;
$response = wp_remote_request(
self::get_remote_url( $endpoint ),
$args
);
if ( is_wp_error( $response ) ) {
$message = $response->get_error_message();
return new WP_Error(
$response->get_error_code(),
is_array( $message ) ? join( ', ', $message ) : $message
);
}
$body = wp_remote_retrieve_body( $response );
$response_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! $response_code ) {
return new WP_Error( 500, 'No Response' );
}
// Server sent a success message without content.
if ( 'null' === $body ) {
$body = true;
}
// Return with no content on successful deletion of domain from service.
if ( 204 === $response_code ) {
$body = true;
return $body;
}
$body = json_decode( $body );
if ( false === $body ) {
return new WP_Error( 422, 'Wrong Server Response' );
}
// If the token is invalid, refresh it and try again once only.
if ( ! $this->refreshed && ! empty( $body->message ) && ( false !== strpos( $body->message, 'Invalid Token' ) ) ) {
Service::refresh_token();
$this->refreshed = true;
$args['headers'] = $this->add_bearer_token( $args['headers'] );
return $this->request( $method, $endpoint, $args );
}
if ( ! in_array( $response_code, [ 200, 201 ], true ) ) {
// In case $as_array = true.
$message = $body->message ?? wp_remote_retrieve_response_message( $response );
$message = is_array( $message ) ? join( ', ', $message ) : $message;
$code = isset( $body->code ) ? (int) $body->code : $response_code;
return new WP_Error( $code, $message );
}
return $body;
}
/**
* get_site_data
* @return mixed|WP_Error Site data
*/
public static function get_site_data() {
return self::get_instance()->make_request( 'POST', 'stats' );
}
/**
* register_website
* @return mixed|WP_Error Site data
*/
public static function register_website() {
return self::get_instance()->make_request( 'POST', 'site' );
}
}

27
classes/utils.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace EA11y\Classes;
use EA11y\Classes\Services\Client;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Utils {
public static function get_api_client(): ?Client {
return Client::get_instance();
}
public static function is_plugin_settings_page(): bool {
$current_screen = get_current_screen();
return str_contains( $current_screen->id, 'toplevel_page_accessibility-settings' );
}
public static function is_elementor_installed() :bool {
$file_path = 'elementor/elementor.php';
$installed_plugins = get_plugins();
return isset( $installed_plugins[ $file_path ] );
}
}

144
classes/utils/assets.php Normal file
View file

@ -0,0 +1,144 @@
<?php
namespace EA11y\Classes\Utils;
use const EA11Y_ASSETS_PATH;
use const EA11Y_ASSETS_URL;
use const EA11Y_VERSION;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Assets
*/
class Assets {
/**
* enqueue_scripts
*
* @param string $handle
* @param string $script_name
* @param array $dependencies
* @param string $version
* @param bool $footer
*/
public static function enqueue_scripts( string $handle, string $script_name, array $dependencies = [], string $version = '', bool $footer = false ) : void {
$asset_data = self::get_asset_version_and_suffix( $version );
wp_enqueue_script(
$handle,
self::get_assets_path( $script_name, 'js', $asset_data['suffix'] ),
$dependencies,
$asset_data['version'],
$footer
);
}
/**
* enqueue_styles
*
* @param string $handle
* @param string $style_name
* @param array $dependencies
* @param string $version
*/
public static function enqueue_styles( string $handle, string $style_name, array $dependencies = [], string $version = '' ) {
$asset_data = self::get_asset_version_and_suffix( $version );
wp_enqueue_style(
$handle,
self::get_assets_path( $style_name, 'css', $asset_data['suffix'] ),
$dependencies,
$asset_data['version']
);
}
/**
* get_assets_version
*
* @param string $version
*
* @return string
*/
private static function get_assets_version( string $version = '' ) : string {
return empty( $version ) ? \EA11Y_VERSION : $version;
}
/**
* get_assets_suffix
* @return string
*/
private static function get_assets_suffix() : string {
return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
}
/**
* get_asset_version_and_suffix
*
* @param string $version
*
* @return array
*/
private static function get_asset_version_and_suffix( string $version = '' ) : array {
return [
'version' => self::get_assets_version( $version ),
'suffix' => self::get_assets_suffix(),
];
}
/**
* get_assets_path
*
* @param string $asset_name
* @param string $asset_type
* @param string $suffix
*
* @return string
*/
private static function get_assets_path( string $asset_name, string $asset_type, string $suffix = '' ) : string {
return \EA11Y_ASSETS_URL . '/build/' . $asset_name . $suffix . '.' . $asset_type;
}
/**
* enqueue_app_assets
*
* @param string $handle
* @param bool $with_css
*/
public static function enqueue_app_assets( string $handle = '', bool $with_css = true ) : void {
$dir = \EA11Y_ASSETS_PATH . 'build/';
$url = \EA11Y_ASSETS_URL . 'build/';
$script_asset_path = $dir . $handle . '.asset.php';
if ( ! file_exists( $script_asset_path ) ) {
throw new \Error(
'You need to run `npm start` or `npm run build` for the "' . esc_html( $handle ) . '" script first.'
);
}
// enqueue js
$script_asset = require $script_asset_path;
wp_enqueue_script(
$handle,
$url . $handle . '.js',
array_merge( $script_asset['dependencies'], [ 'wp-util' ] ),
$script_asset['version'],
true,
);
// add translation support
wp_set_script_translations( $handle, 'pojo-accessibility' );
if ( ! $with_css ) {
return;
}
// enqueue css
$css_file_name = 'style-' . $handle . '.css';
$css_version = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? filemtime( $dir . $css_file_name ) : \EA11Y_VERSION;
wp_enqueue_style(
$handle,
$url . $css_file_name,
[ 'wp-components' ],
$css_version
);
}
}

View file

@ -0,0 +1,215 @@
<?php
namespace EA11y\Classes\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Notice_Base
*/
class Notice_Base {
/**
* The action prefix for the notice
*
* @var string
*/
const ACTION_PREFIX = 'ea11y_admin_notice';
/**
* The option name / meta key for dismissed notices
*
* @var string
*/
const DISMISSED_NOTICES = 'ea11y_dismissed_notices';
/**
* @var bool $conditions
*/
public bool $conditions = true;
/**
* Whether the notice is dismissible
*
* @var bool
*/
public bool $is_dismissible = false;
/**
* Whether the notice is dismissible per user or globally
*
* @var bool
*/
public bool $per_user = true;
/**
* The notice ID
*
* @var string
*/
public string $id = '';
/**
* The notice content
*
* @var string
*/
public string $content;
/**
* should be one of 'info', 'warning', 'error', 'success'
* @var string
*/
public string $type;
/**
* @var mixed|string
*/
public $capability;
/**
* Get the notice ID
*
* @return string
*/
public function get_id() : string {
return $this->id;
}
/**
* Get the action name for the notice
*
* @return string
*/
public function get_action_name() : string {
return self::ACTION_PREFIX . '_' . $this->get_id();
}
public function show(): bool {
if ( ! $this->conditions ) {
return false;
}
if ( $this->is_dismissible && $this->is_dismissed() ) {
return false;
}
if ( $this->capability && ! current_user_can( $this->capability ) ) {
return false;
}
return true;
}
public function is_dismissed() : bool {
if ( $this->per_user ) {
$dismissed = get_user_meta( get_current_user_id(), self::DISMISSED_NOTICES, true );
} else {
$dismissed = get_option( self::DISMISSED_NOTICES, [] );
}
return in_array( $this->get_id(), (array) $dismissed );
}
public function dismiss() {
$dismissed = get_option( self::DISMISSED_NOTICES, [] );
$dismissed[] = $this->get_id();
update_option( self::DISMISSED_NOTICES, $dismissed, false );
}
public function dismiss_per_user() {
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( [ 'message' => 'Invalid user' ] );
}
$dismissed = get_user_meta( $user_id, self::DISMISSED_NOTICES, true );
if ( ! $dismissed ) {
$dismissed = [];
}
$dismissed[] = $this->get_id();
update_user_meta( $user_id, self::DISMISSED_NOTICES, $dismissed );
}
public function handle_dismiss() {
if ( ! $this->is_dismissible ) {
return;
}
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), $this->get_action_name() ) ) {
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
}
if ($this->per_user) {
$this->dismiss_per_user();
} else {
$this->dismiss();
}
wp_send_json_success( [] );
}
public function render() {
echo sprintf( '<div class="notice_wrapper notice %1$s notice-%2$s %2$s %3$s" data-notice-id="%3$s" data-notice-action="%4$s" data-notice-nonce="%5$s">%6$s</div>',
$this->is_dismissible ? 'ea11y-dismiss is-dismissible' : '',
esc_attr( $this->type ),
esc_attr( $this->get_id() ),
esc_attr( $this->get_action_name() ),
wp_create_nonce( $this->get_action_name() ),
$this->content()
);
}
public function print_js() {
// used to make sure we only print this js once per page
$action = 'admin_notices_print_js';
if ( did_action( $action ) ) {
return;
}
do_action( $action );
?>
<script>
jQuery( document ).ready( function() {
jQuery( '.ea11y-dismiss' ).on( 'click', function() {
const $this = jQuery( this );
const data = {
action: 'ea11y_admin_notice_dismiss',
nonce: $this.data( 'notice-nonce' ),
notice_id: $this.data( 'notice-id' ),
};
jQuery.post( ajaxurl, data, function( response ) {
if ( response.success ) {
$this.slideUp();
}
} );
} );
} );
</script>
<?php
}
public function maybe_show_notice() {
if ( ! $this->show() ) {
return;
}
if ( $this->is_dismissible ) {
add_action( 'admin_footer', [ $this, 'print_js' ] );
}
$this->render();
}
public function content(): string {
return $this->content;
}
/**
* @throws \Exception
*/
public function __construct() {
if ( ! $this->content() ) {
throw new \Exception( 'Notice content is required' );
}
if ( ! in_array( $this->type, [ 'info', 'warning', 'error', 'success' ] ) ) {
throw new \Exception( 'Invalid notice type' );
}
}
}

View file

@ -9,7 +9,45 @@
"email": "yakir@pojo.me"
}
],
"require": {
"phpunit/phpunit": "4.8.8"
}
"require-dev": {
"johnpbloch/wordpress-core": "^6.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
"squizlabs/php_codesniffer": "^3.6",
"wp-coding-standards/wpcs": "^2.3.0",
"php-stubs/wordpress-stubs": "^6.0",
"elementor/eunit": "^0.0.10",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"phpunit/php-code-coverage": "^9.2",
"wildwolf/wordpress-test-library-stubs": "^6.0",
"wp-phpunit/wp-phpunit": "^6.0",
"yoast/phpunit-polyfills": "^2.0"
},
"scripts": {
"lint": "phpcs --standard=./ruleset.xml ./**/*.php"
},
"require": {
"firebase/php-jwt": "^6.10",
"ext-json": "*"
},
"config": {
"allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"extra": {
"imposter": {
"namespace": "...",
"excludes": [ "firebase/php-jwt"]
},
"installer-paths": {
"vendor/{$vendor}/{$name}/": [ "firebase/php-jwt"]
}
},
"repositories":[
{
"type": "vcs",
"url": "git@github.com:elementor/eunit.git"
}
]
}

61
includes/manager.php Normal file
View file

@ -0,0 +1,61 @@
<?php
namespace EA11y;
use EA11y\Classes\Module_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
final class Manager {
/**
* @var Module_Base[]
*/
private array $modules = [];
public static function get_module_list(): array {
return [
'Legacy',
'Connect',
'Settings',
'Widget',
'Core',
];
}
/**
* @codeCoverageIgnore
*/
public function __construct() {
$modules = self::get_module_list();
foreach ( $modules as $module_name ) {
$class_name = str_replace( '-', ' ', $module_name );
$class_name = str_replace( ' ', '', ucwords( $class_name ) );
$class_name = __NAMESPACE__ . '\\Modules\\' . $class_name . '\Module';
/** @var Module_Base $class_name */
if ( $class_name::is_active() ) {
$this->modules[ $module_name ] = $class_name::instance();
}
}
}
/**
* @param string $module_name
*
* @return Module_Base|Module_Base[]
*/
public function get_modules( string $module_name ) {
if ( $module_name ) {
if ( isset( $this->modules[ $module_name ] ) ) {
return $this->modules[ $module_name ];
}
return null;
}
return $this->modules;
}
}

View file

@ -1,285 +0,0 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class Pojo_A11y_Customizer {
private $css_rules = array();
private $css_code = '';
public function get_customizer_fields() {
$fields = array();
$fields[] = array(
'id' => 'a11y_toolbar_icon',
'title' => __( 'Toolbar Icon', 'pojo-accessibility' ),
'type' => 'select',
'choices' => array(
'one-click' => __( 'One Click', 'pojo-accessibility' ),
'wheelchair' => __( 'Wheelchair', 'pojo-accessibility' ),
'accessibility' => __( 'Accessibility', 'pojo-accessibility' ),
),
'std' => 'one-click',
'description' => __( 'Set Toolbar Icon', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_toolbar_position',
'title' => __( 'Toolbar Position', 'pojo-accessibility' ),
'type' => 'select',
'choices' => array(
'left' => __( 'Left', 'pojo-accessibility' ),
'right' => __( 'Right', 'pojo-accessibility' ),
),
'std' => is_rtl() ? 'right' : 'left',
'description' => __( 'Set Toolbar Position', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_toolbar_distance_top',
'title' => __( 'Offset from Top (Desktop)', 'pojo-accessibility' ),
'type' => 'text',
'std' => '100px',
'description' => __( 'Set Toolbar top offset (Desktop)', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_toolbar_distance_top_mobile',
'title' => __( 'Offset from Top (Mobile)', 'pojo-accessibility' ),
'type' => 'text',
'std' => '50px',
'description' => __( 'Set Toolbar top offset (Mobile)', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_bg_toolbar',
'title' => __( 'Toolbar Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay',
'change_type' => 'bg_color',
'description' => __( 'Set Toolbar background color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_color_toolbar',
'title' => __( 'Toolbar Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#333333',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a, #pojo-a11y-toolbar .pojo-a11y-toolbar-overlay p.pojo-a11y-toolbar-title',
'change_type' => 'color',
'description' => __( 'Set Toolbar text color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_toggle_button_bg_color',
'title' => __( 'Toggle Button Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#4054b2',
'description' => __( 'Set Toolbar toggle button background color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_toggle_button_color',
'title' => __( 'Toggle Button Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-toggle a',
'change_type' => 'color',
'description' => __( 'Set Toolbar toggle button icon color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_bg_active',
'title' => __( 'Active Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#4054b2',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a.active',
'change_type' => 'bg_color',
'description' => __( 'Set Toolbar active background color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_color_active',
'title' => __( 'Active Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a.active',
'change_type' => 'color',
'description' => __( 'Set Toolbar active text color', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_focus_outline_style',
'title' => __( 'Focus Outline Style', 'pojo-accessibility' ),
'type' => 'select',
'choices' => array(
'solid' => __( 'Solid', 'pojo-accessibility' ),
'dotted' => __( 'Dotted', 'pojo-accessibility' ),
'dashed' => __( 'Dashed', 'pojo-accessibility' ),
'double' => __( 'Double', 'pojo-accessibility' ),
'groove' => __( 'Groove', 'pojo-accessibility' ),
'ridge' => __( 'Ridge', 'pojo-accessibility' ),
'outset' => __( 'Outset', 'pojo-accessibility' ),
'initial' => __( 'Initial', 'pojo-accessibility' ),
),
'std' => 'solid',
'description' => __( 'Set Focus outline style', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_focus_outline_width',
'title' => __( 'Focus Outline Width', 'pojo-accessibility' ),
'type' => 'select',
'choices' => array(
'1px' => '1px',
'2px' => '2px',
'3px' => '3px',
'4px' => '4px',
'5px' => '5px',
'6px' => '6px',
'7px' => '7px',
'8px' => '8px',
'9px' => '9px',
'10px' => '10px',
),
'std' => '1px',
'description' => __( 'Set Focus outline width', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'a11y_focus_outline_color',
'title' => __( 'Focus Outline Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#FF0000',
'description' => __( 'Set Focus outline color', 'pojo-accessibility' ),
);
return $fields;
}
public function customize_a11y( $wp_customize ) {
$fields = $this->get_customizer_fields();
$section_description = '<p>' . __( 'Use the control below to customize the appearance and layout of the Accessibility Toolbar', 'pojo-accessibility' ) . '</p><p>' .
sprintf( __( 'Additional Toolbar settings can be configured at the %s page.', 'pojo-accessibility' ),
'<a href="' . admin_url( 'admin.php?page=accessibility-toolbar' ) . '" target="blank">' . __( 'Accessibility Toolbar', 'pojo-accessibility' ) . '</a>'
) . '</p>';
$wp_customize->add_section( 'accessibility', array(
'title' => __( 'Accessibility', 'pojo-accessibility' ),
'priority' => 30,
'description' => $section_description,
) );
foreach ( $fields as $field ) {
$customizer_id = POJO_A11Y_CUSTOMIZER_OPTIONS . '[' . $field['id'] . ']';
$wp_customize->add_setting( $customizer_id, array(
'default' => $field['std'] ? $field['std'] : null,
'type' => 'option',
) );
switch ( $field['type'] ) {
case 'color':
$wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, $field['id'], array(
'label' => $field['title'],
'section' => 'accessibility',
'settings' => $customizer_id,
'description' => isset( $field['description'] ) ? $field['description'] : null,
) ) );
break;
case 'select':
case 'text':
$wp_customize->add_control( $field['id'], array(
'label' => $field['title'],
'section' => 'accessibility',
'settings' => $customizer_id,
'type' => $field['type'],
'choices' => isset( $field['choices'] ) ? $field['choices'] : null,
'description' => isset( $field['description'] ) ? $field['description'] : null,
) );
break;
}
}
}
public function get_custom_css_code() {
$options = $this->get_customizer_options();
$bg_color = $options['a11y_toggle_button_bg_color']; // get_theme_mod( 'a11y_toggle_button_bg_color', '#4054b2' );
if ( ! empty( $bg_color ) ) {
$this->add_css_rule( '#pojo-a11y-toolbar .pojo-a11y-toolbar-toggle a', 'background-color', $bg_color );
$this->add_css_rule( '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay, #pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items.pojo-a11y-links', 'border-color', $bg_color );
}
$outline_style = $options['a11y_focus_outline_style']; //get_theme_mod( 'a11y_focus_outline_style', 'solid' );
if ( ! empty( $outline_style ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-style', $outline_style . ' !important' );
}
$outline_width = $options['a11y_focus_outline_width']; // get_theme_mod( 'a11y_focus_outline_width', '1px' );
if ( ! empty( $outline_width ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-width', $outline_width . ' !important' );
}
$outline_color = $options['a11y_focus_outline_color']; //get_theme_mod( 'a11y_focus_outline_color', '#FF0000' );
if ( ! empty( $outline_color ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-color', $outline_color . ' !important' );
}
$distance_top = $options['a11y_toolbar_distance_top']; //get_theme_mod( 'a11y_toolbar_distance_top', '100px' );
if ( ! empty( $distance_top ) ) {
$this->add_css_rule( '#pojo-a11y-toolbar', 'top', $distance_top . ' !important' );
}
$distance_top_mobile = $options['a11y_toolbar_distance_top_mobile']; // get_theme_mod( 'a11y_toolbar_distance_top_mobile', '50px' );
if ( ! empty( $distance_top_mobile ) ) {
$this->add_css_code( "@media (max-width: 767px) { #pojo-a11y-toolbar { top: {$distance_top_mobile} !important; } }" );
}
$fields = $this->get_customizer_fields();
foreach ( $fields as $field ) {
if ( empty( $field['selector'] ) || empty( $field['change_type'] ) ) {
continue;
}
$option = $options[ $field['id'] ];
if ( 'color' === $field['change_type'] ) {
$this->add_css_rule( $field['selector'], 'color', $option );
} elseif ( 'bg_color' === $field['change_type'] ) {
$this->add_css_rule( $field['selector'], 'background-color', $option );
}
}
}
private function get_customizer_options() {
static $options = false;
if ( false === $options ) {
$options = get_option( POJO_A11Y_CUSTOMIZER_OPTIONS );
}
return $options;
}
private function add_css_rule( $selector, $rule, $value ) {
if ( ! isset( $this->css_rules[ $selector ] ) ) {
$this->css_rules[ $selector ] = array();
}
$this->css_rules[ $selector ][] = $rule . ': ' . $value . ';';
}
private function add_css_code( $code ) {
$this->css_code .= "\n" . $code;
}
public function print_css_code() {
$this->get_custom_css_code();
$css = '';
foreach ( $this->css_rules as $selector => $css_rules ) {
$css .= "\n" . $selector . '{ ' . implode( "\t", $css_rules ) . '}';
}
echo '<style type="text/css">' . $css . $this->css_code . '</style>';
}
public function __construct() {
add_filter( 'customize_register', array( &$this, 'customize_a11y' ), 610 );
add_filter( 'wp_head', array( &$this, 'print_css_code' ) );
}
}

View file

@ -1,645 +0,0 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class Pojo_A11y_Settings {
public $menu_slug = null;
const PAGE_ID = 'pojo-a11y';
const SETTINGS_PAGE = 'toplevel_page_accessibility-settings';
const TOOLBAR_PAGE = 'accessibility_page_accessibility-toolbar';
const FIELD_TEXT = 'text';
const FIELD_SELECT = 'select';
const FIELD_CHECKBOX_LIST = 'checkbox_list';
protected $_fields = array();
protected $_sections = array();
protected $_defaults = array();
protected $_pages = array();
/**
* Setup Toolbar fields
*
* @param array $sections
*
* @return array
*/
public function section_a11y_toolbar( $sections = array() ) {
$fields = array();
$fields[] = array(
'id' => 'pojo_a11y_toolbar',
'title' => __( 'Display Toolbar', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'options' => array(
'enable' => __( 'Show on all devices', 'pojo-accessibility' ),
'visible-desktop' => __( 'Visible Desktop', 'pojo-accessibility' ),
'visible-tablet' => __( 'Visible Tablet', 'pojo-accessibility' ),
'visible-phone' => __( 'Visible Phone', 'pojo-accessibility' ),
'hidden-desktop' => __( 'Hidden Desktop', 'pojo-accessibility' ),
'hidden-tablet' => __( 'Hidden Tablet', 'pojo-accessibility' ),
'hidden-phone' => __( 'Hidden Phone', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$toolbar_options_classes = 'pojo-a11y-toolbar-button';
$fields[] = array(
'id' => 'pojo_a11y_toolbar_title',
'title' => __( 'Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'desc' => __( 'Title top of the toolbar (recommended).', 'pojo-accessibility' ),
'class' => $toolbar_options_classes,
'std' => __( 'Accessibility Tools', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_resize_font',
'title' => __( 'Resize Font', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_resize_font_add_title',
'title' => __( 'Increase Text', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row no-border',
'std' => __( 'Increase Text', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_resize_font_less_title',
'title' => __( 'Decrease Text', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Decrease Text', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_grayscale',
'title' => __( 'Grayscale', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_grayscale_title',
'title' => __( 'Grayscale Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Grayscale', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_high_contrast',
'title' => __( 'High Contrast', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_high_contrast_title',
'title' => __( 'High Contrast Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'High Contrast', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_negative_contrast',
'title' => __( 'Negative Contrast', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_negative_contrast_title',
'title' => __( 'Negative Contrast Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Negative Contrast', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_light_bg',
'title' => __( 'Light Background', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_light_bg_title',
'title' => __( 'Light Background Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Light Background', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_links_underline',
'title' => __( 'Links Underline', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_links_underline_title',
'title' => __( 'Links Underline Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Links Underline', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_readable_font',
'title' => __( 'Readable Font', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_readable_font_title',
'title' => __( 'Readable Font Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Readable Font', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_sitemap_title',
'title' => __( 'Sitemap Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Sitemap', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_sitemap_link',
'title' => __( 'Sitemap Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/sitemap',
'desc' => __( 'Link for sitemap page in your website. Leave blank to disable.', 'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_help_title',
'title' => __( 'Help Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Help', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_help_link',
'title' => __( 'Help Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/help',
'desc' => __( 'Link for help page in your website. Leave blank to disable.', 'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_feedback_title',
'title' => __( 'Feedback Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Feedback', 'pojo-accessibility' ),
);
$fields[] = array(
'id' => 'pojo_a11y_toolbar_button_feedback_link',
'title' => __( 'Feedback Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/feedback',
'desc' => __( 'Link for feedback page in your website. Leave blank to disable.', 'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
);
$sections[] = array(
'id' => 'section-a11y-toolbar',
'page' => self::TOOLBAR_PAGE,
'title' => __( 'Toolbar Settings', 'pojo-accessibility' ),
'intro' => '',
'fields' => $fields,
);
$sections[] = array(
'id' => 'section-a11y-styles',
'page' => self::TOOLBAR_PAGE,
'title' => __( 'Style Settings', 'pojo-accessibility' ),
'intro' => sprintf( __( 'For style settings of accessibility tools go to > Customize > <a href="%s">Accessibility</a>.', 'pojo-accessibility' ), $this->get_admin_url( 'customizer' ) ),
'fields' => array(),
);
return $sections;
}
public function section_a11y_settings( $sections ) {
$fields = array();
$fields[] = array(
'id' => 'pojo_a11y_focusable',
'title' => __( 'Add Outline Focus', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Add outline to elements on keyboard focus.', 'pojo-accessibility' ),
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'disable',
);
$fields[] = array(
'id' => 'pojo_a11y_skip_to_content_link',
'title' => __( 'Skip to Content link', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Add skip to content link when using keyboard.', 'pojo-accessibility' ),
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_skip_to_content_link_element_id',
'title' => __( 'Skip to Content Element ID', 'pojo-accessibility' ),
'placeholder' => 'content',
'type' => self::FIELD_TEXT,
'std' => 'content',
);
$fields[] = array(
'id' => 'pojo_a11y_remove_link_target',
'title' => __( 'Remove target attribute from links', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'This option will reset all your target links to open in the same window or tab.', 'pojo-accessibility' ),
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'disable',
);
$fields[] = array(
'id' => 'pojo_a11y_add_role_links',
'title' => __( 'Add landmark roles to all links', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'This option will add <code>role="link"</code> to all links on the page.', 'pojo-accessibility' ),
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_save',
'title' => __( 'Sitewide Accessibility', 'pojo-accessibility' ),
'desc' => __( 'Consistent accessibility throughout your site visit. Site remembers you and stays accessible.', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'options' => array(
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
),
'std' => 'enable',
);
$fields[] = array(
'id' => 'pojo_a11y_save_expiration',
'title' => __( 'Remember user for', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Define how long your toolbar settings will be remembered', 'pojo-accessibility' ),
'options' => array(
'1' => __( '1 Hour', 'pojo-accessibility' ),
'6' => __( '6 Hours', 'pojo-accessibility' ),
'12' => __( '12 Hours', 'pojo-accessibility' ),
'24' => __( '1 Day', 'pojo-accessibility' ),
'48' => __( '2 Days', 'pojo-accessibility' ),
'72' => __( '3 Days', 'pojo-accessibility' ),
'168' => __( '1 Week', 'pojo-accessibility' ),
'720' => __( '1 Month', 'pojo-accessibility' ),
),
'std' => '12',
);
$sections[] = array(
'id' => 'section-a11y-settings',
'page' => self::SETTINGS_PAGE,
'title' => __( 'General Settings', 'pojo-accessibility' ),
'intro' => '',
'fields' => $fields,
);
return $sections;
}
public function print_js() {
// TODO: Maybe need to move to other file
?>
<script>
jQuery( document ).ready( function( $ ) {
var $a11yToolbarOption = $( 'table.form-table #pojo_a11y_toolbar' ),
$a11yToolbarButtons = $( 'tr.pojo-a11y-toolbar-button' );
$a11yToolbarOption.on( 'change', function() {
if ( 'disable' !== $( this ).val() ) {
$a11yToolbarButtons.fadeIn( 'fast' );
} else {
$a11yToolbarButtons.hide();
}
} );
$a11yToolbarOption.trigger( 'change' );
} );
</script>
<?php
}
public function get_settings_sections() {
$sections = array();
$sections = $this->section_a11y_toolbar( $sections );
$sections = $this->section_a11y_settings( $sections );
$this->_sections = $sections;
return $this->_sections;
}
public function add_settings_section( $args = array() ) {
$args = wp_parse_args( $args, array(
'id' => '',
'title' => '',
) );
foreach ( $this->_sections as $section ) {
if ( $args['id'] !== $section['id'] ) {
continue;
}
if ( empty( $section['intro'] ) ) {
return;
}
printf( '<p>%s</p>', $section['intro'] );
break;
}
}
public function add_settings_field( $args = array() ) {
if ( empty( $args ) ) {
return;
}
$args = wp_parse_args( $args, array(
'id' => '',
'std' => '',
'type' => self::FIELD_TEXT,
) );
if ( empty( $args['id'] ) || empty( $args['type'] ) ) {
return;
}
$field_callback = 'render_' . $args['type'] . '_field';
if ( method_exists( $this, $field_callback ) ) {
call_user_func( array( $this, $field_callback ), $args );
}
}
public function render_select_field( $field ) {
$options = array();
foreach ( $field['options'] as $option_key => $option_value ) {
$options[] = sprintf(
'<option value="%1$s"%2$s>%3$s</option>',
esc_attr( $option_key ),
selected( get_option( $field['id'], $field['std'] ), $option_key, false ),
$option_value
);
}
?>
<select id="<?php echo $field['id']; ?>" name="<?php echo $field['id']; ?>">
<?php echo implode( '', $options ); ?>
</select>
<?php if ( ! empty( $field['sub_desc'] ) ) echo $field['sub_desc']; ?>
<?php if ( ! empty( $field['desc'] ) ) : ?>
<p class="description"><?php echo $field['desc']; ?></p>
<?php endif; ?>
<?php
}
public function render_text_field( $field ) {
if ( empty( $field['classes'] ) )
$field['classes'] = array( 'regular-text' );
?>
<input type="text" class="<?php echo implode( ' ', $field['classes'] ); ?>" id="<?php echo $field['id']; ?>" name="<?php echo $field['id']; ?>" value="<?php echo esc_attr( get_option( $field['id'], $field['std'] ) ); ?>"<?php echo ! empty( $field['placeholder'] ) ? ' placeholder="' . $field['placeholder'] . '"' : ''; ?> />
<?php if ( ! empty( $field['sub_desc'] ) ) echo $field['sub_desc']; ?>
<?php if ( ! empty( $field['desc'] ) ) : ?>
<p class="description"><?php echo $field['desc']; ?></p>
<?php endif; ?>
<?php
}
public function admin_init() {
foreach ( $this->get_settings_sections() as $section_key => $section ) {
add_settings_section(
$section['id'],
$section['title'],
array( &$this, 'add_settings_section' ),
$section['page']
);
if ( empty( $section['fields'] ) ) {
continue;
}
foreach ( $section['fields'] as $field ) {
add_settings_field(
$field['id'],
$field['title'],
array( &$this, 'add_settings_field' ),
$section['page'],
$section['id'],
$field
);
$sanitize_callback = array( $this, 'field_html' );
if ( ! empty( $field['type'] ) && self::FIELD_CHECKBOX_LIST === $field['type'] ) {
$sanitize_callback = array( $this, 'field_checkbox_list' );
}
if ( ! empty( $field['sanitize_callback'] ) ) {
$sanitize_callback = $field['sanitize_callback'];
}
register_setting( $section['page'], $field['id'], $sanitize_callback );
}
}
}
public static function field_html( $input ) {
return stripslashes( wp_filter_post_kses( addslashes( $input ) ) );
}
public static function field_checkbox_list( $input ) {
if ( empty( $input ) ) {
$input = array();
}
return $input;
}
public function display_settings_page() {
$screen = get_current_screen();
$screen_id = $screen->id;
if ( false !== strpos( $screen_id, 'toolbar' ) ) {
$screen_id = self::TOOLBAR_PAGE;
}
?>
<div class="wrap">
<h2><?php echo $this->_page_title; ?></h2>
<?php settings_errors( $screen_id ); ?>
<form method="post" action="options.php">
<?php
settings_fields( $screen_id );
do_settings_sections( $screen_id );
submit_button();
?>
</form>
</div><!-- /.wrap -->
<?php
}
public function admin_menu() {
$this->menu_slug = add_menu_page(
__( 'Accessibility', 'pojo-accessibility' ),
__( 'Accessibility', 'pojo-accessibility' ),
'manage_options',
'accessibility-settings',
array( &$this, 'display_settings_page' ),
'dashicons-universal-access-alt'
);
add_submenu_page(
'accessibility-settings',
__( 'Accessibility Settings', 'pojo-accessibility' ),
__( 'Settings', 'pojo-accessibility' ),
'manage_options',
'accessibility-settings',
array( &$this, 'display_settings_page' )
);
add_submenu_page(
'accessibility-settings',
__( 'Accessibility Toolbar', 'pojo-accessibility' ),
__( 'Toolbar', 'pojo-accessibility' ),
'manage_options',
'accessibility-toolbar',
array( &$this, 'display_settings_page' )
);
add_submenu_page(
'accessibility-settings',
__( 'Customize', 'pojo-accessibility' ),
__( 'Customize', 'pojo-accessibility' ),
'manage_options',
'/customize.php?autofocus[section]=accessibility'
);
}
public function plugin_action_links( $links, $plugin_file ) {
if ( POJO_A11Y_BASE === $plugin_file ) {
$settings = '<a href="' . $this->get_admin_url( 'general' ) . '" aria-label="' . esc_attr__( 'Set Accessibility settings', 'pojo-accessibility' ) . '">' . __( 'Settings', 'pojo-accessibility' ) . '</a>';
$toolbar = '<a href="' . $this->get_admin_url( 'toolbar' ) . '" aria-label="' . esc_attr__( 'Set Accessibility Toolbar Settings', 'pojo-accessibility' ) . '">' . __( 'Toolbar', 'pojo-accessibility' ) . '</a>';
$customizer = '<a href="' . $this->get_admin_url( 'customizer' ) . '" aria-label="' . esc_attr__( 'Customize Toolbar', 'pojo-accessibility' ) . '" target="_blank">' . __( 'Customize', 'pojo-accessibility' ) . '</a>';
array_unshift( $links, $customizer );
array_unshift( $links, $toolbar );
array_unshift( $links, $settings );
}
return $links;
}
private function get_admin_url( $type ) {
switch ( $type ) {
case 'customizer':
return admin_url( 'customize.php?autofocus[section]=accessibility' );
break;
case 'general':
return admin_url( 'admin.php?page=accessibility-settings' );
break;
case 'toolbar':
return admin_url( 'admin.php?page=accessibility-toolbar' );
break;
}
}
private function get_default_values() {
if ( empty( $this->_defaults ) ) {
if ( empty( $this->_sections ) ) {
$this->get_settings_sections();
}
$defaults = array();
foreach ( $this->_sections as $section ) {
foreach ( $section['fields'] as $field ) {
$defaults[ $field['id'] ] = isset( $field['std'] ) ? $field['std'] : '';
}
}
$this->_defaults = $defaults;
}
}
public function get_default_title_text( $option ) {
$this->get_default_values();
$default = isset( $this->_defaults[ $option ] ) ? $this->_defaults[ $option ] : '';
return get_option( $option, $default );
}
public function __construct() {
$this->_page_title = __( 'One Click Accessibility', 'pojo-accessibility' );
$this->_page_menu_title = __( 'One Click Accessibility', 'pojo-accessibility' );
$this->_menu_parent = 'themes.php';
add_action( 'admin_menu', array( &$this, 'admin_menu' ), 20 );
add_action( 'admin_init', array( &$this, 'admin_init' ), 20 );
add_action( 'admin_footer', array( &$this, 'print_js' ) );
add_filter( 'plugin_action_links_' . POJO_A11Y_BASE, [ $this, 'plugin_action_links' ], 10, 2 );
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace EA11y\Modules\Connect\Classes;
use ReflectionClass;
use ReflectionException;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Basic_Enum {
private static array $entries = [];
/**
* @throws ReflectionException
*/
public static function get_values(): array {
return array_values( self::get_entries() );
}
/**
* @throws ReflectionException
*/
protected static function get_entries(): array {
$caller = get_called_class();
if ( ! array_key_exists( $caller, self::$entries ) ) {
$reflect = new ReflectionClass( $caller );
self::$entries[ $caller ] = $reflect->getConstants();
}
return self::$entries[ $caller ];
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace EA11y\Modules\Connect\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Config
*/
class Config {
const APP_NAME = 'once-click-accessibility';
const APP_PREFIX = 'ea11y';
const APP_REST_NAMESPACE = 'ea11y';
const BASE_URL = 'https://my.elementor.com/connect';
const ADMIN_PAGE = 'accessibility-settings';
const APP_TYPE = 'app_access';
const SCOPES = 'openid offline_access share_usage_data';
const STATE_NONCE = 'ea11y_auth_nonce';
const CONNECT_MODE = 'site';
}

View file

@ -0,0 +1,357 @@
<?php
namespace EA11y\Modules\Connect\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Data
*/
class Data {
const CLIENT_ID = '_client_id';
const CLIENT_SECRET = '_client_secret';
const ACCESS_TOKEN = '_access_token';
const REFRESH_TOKEN = '_refresh_token';
const TOKEN_ID = '_token_id';
const SUBSCRIPTION_ID = '_subscription_id';
const OPTION_OWNER_USER_ID = '_owner_user_id';
const HOME_URL = '_home_url';
/**
* get_option
* @param $option_name
* @param $default
*
* @return false|mixed|null
*/
public static function get_option( $option_name, $default ) {
return get_option( Config::APP_PREFIX . $option_name, $default );
}
/**
* set_option
* @param $option_name
* @param $option_value
* @param $auto_load
*
* @return bool
*/
public static function set_option( $option_name, $option_value, $auto_load = false ): bool {
return update_option( Config::APP_PREFIX . $option_name, $option_value, $auto_load );
}
/**
* delete_option
* @param $option_name
*
* @return bool
*/
public static function delete_option( $option_name ) : bool {
return delete_option( Config::APP_PREFIX . $option_name );
}
/**
* get_user_data
* @param $user_id
* @param $data_name
* @param mixed|bool $default
*
* @return false|mixed
*/
public static function get_user_data( $user_id, $data_name, $default = false ) {
$data = get_user_meta( $user_id, Config::APP_PREFIX . $data_name, true );
return empty( $data ) ? $default : $data;
}
/**
* set_user_data
* @param $user_id
* @param $data_name
* @param $value
*
* @return bool|int
*/
public static function set_user_data( $user_id, $data_name, $value ) {
return update_user_meta( $user_id, Config::APP_PREFIX . $data_name, $value );
}
/**
* delete_user_data
* @param $user_id
* @param $data_name
*
* @return bool
*/
public static function delete_user_data( $user_id, $data_name ): bool {
return delete_user_meta( $user_id, Config::APP_PREFIX . $data_name );
}
/**
* get_connect_mode_data
* @param ...$data
*
* @return false|mixed|null|string
*/
public static function get_connect_mode_data( ...$data ) {
if ( Config::CONNECT_MODE === 'site' ) {
return self::get_option( ...$data );
}
$user_id = get_current_user_id();
return self::get_user_data( ...( [ $user_id, ...$data ] ) );
}
/**
* set_connect_mode_data
* @param ...$data
*
* @return bool|int
*/
public static function set_connect_mode_data( ...$data ) {
if ( Config::CONNECT_MODE === 'site' ) {
return self::set_option( ...$data );
}
$user_id = get_current_user_id();
return self::set_user_data( ...( [ $user_id, ...$data ] ) );
}
/**
* get_client_id
*
* @return string|bool|mixed
*/
public static function get_client_id() {
return self::get_connect_mode_data( self::CLIENT_ID, false );
}
/**
* get_client_secret
* @return false|mixed|string|null
*/
public static function get_client_secret() {
return self::get_connect_mode_data( self::CLIENT_SECRET, false );
}
/**
* set_client_id
* @param $value
*
* @return bool
*/
public static function set_client_id( $value ): bool {
return self::set_connect_mode_data( self::CLIENT_ID, $value );
}
public static function get_subscription_id() {
return self::get_connect_mode_data( self::SUBSCRIPTION_ID, false );
}
public static function set_subscription_id( $value ): bool {
return self::set_connect_mode_data( self::SUBSCRIPTION_ID, $value );
}
/**
* set_client_secret
* @param $value
*
* @return bool
*/
public static function set_client_secret( $value ): bool {
return self::set_connect_mode_data( self::CLIENT_SECRET, $value );
}
/**
* get_access_token
* @return false|mixed|string|null
*/
public static function get_access_token() {
return self::get_connect_mode_data( self::ACCESS_TOKEN, false );
}
public static function get_token_id() {
return self::get_connect_mode_data( self::TOKEN_ID, false );
}
/**
* get_refresh_token
* @return false|mixed|string|null
*/
public static function get_refresh_token() {
return self::get_connect_mode_data( self::REFRESH_TOKEN, false );
}
/**
* get_home_url
*
* Return a plain text version of a based64 encoded home URL if its stored as base64 encoded
* @return string|null
*/
public static function get_home_url() {
$raw = self::get_connect_mode_data( self::HOME_URL, false );
$is_base64 = base64_encode( base64_decode( $raw, true ) ) === $raw;
return $is_base64 ? base64_decode( $raw ) : $raw;
}
/**
* set_home_url
*
* Stores home URL as a base64 string to avoid migration/stg tools from overriding value
*/
public static function set_home_url( ?string $home_url = null ): bool {
$home_url = $home_url ?? home_url();
return self::set_connect_mode_data( self::HOME_URL, base64_encode( $home_url ) );
}
/**
* set_user_is_owner_option
*/
public static function set_user_is_owner_option( $value ) {
return self::set_connect_mode_data( self::OPTION_OWNER_USER_ID, $value );
}
/**
* get_user_is_owner_option
*/
public static function get_user_is_owner_option() {
return self::get_connect_mode_data( self::OPTION_OWNER_USER_ID, false );
}
/**
* fetch_option
* direct query to avoid cache and race condition issues
*
* @param $option_name
* @param $default
*
* @return mixed|null
*/
public static function fetch_option( $option_name, $default = null ) {
global $wpdb;
if ( ! self::is_option_whitelisted_for_direct_access( $option_name ) ) {
return $default;
}
$cache_buster = wp_generate_uuid4();
$option = $wpdb->get_col(
$wpdb->prepare(
"SELECT option_value
FROM $wpdb->options
WHERE option_name = %s
AND %s = %s
LIMIT 1",
$option_name,
$cache_buster,
$cache_buster
)
);
if ( ! empty( $option ) ) {
return $option[0];
}
return $default;
}
/**
* insert_option_uniquely
*
* used to insert option if not there already
* direct query to avoid cache and race condition issues
*
* @param $option_name
* @param $option_value
*
* @return bool
*/
public static function insert_option_uniquely( $option_name, $option_value ): bool {
global $wpdb;
if ( ! self::is_option_whitelisted_for_direct_access( $option_name ) ) {
return false;
}
$cache_buster = wp_generate_uuid4();
$result = $wpdb->query(
$wpdb->prepare(
"INSERT INTO $wpdb->options (option_name, option_value, autoload)
SELECT * FROM (SELECT %s, %s, 'no') AS tmp
WHERE NOT EXISTS (
SELECT option_name
FROM $wpdb->options
WHERE option_name = %s
AND option_value = %s
AND %s = %s
) LIMIT 1",
$option_name,
$option_value,
$option_name,
$option_value,
$cache_buster,
$cache_buster
)
);
if ( false === $result || 0 === $result ) {
// false means query failed, 0 means no row inserted because it exists
return false;
}
return true;
}
/**
* is_option_whitelisted_for_direct_access
* allowed only list of option names
*
* @param string $option_name
*
* @return boolean
*/
public static function is_option_whitelisted_for_direct_access( string $option_name ): bool {
$options_whitelist = [
Config::APP_NAME . Service::REFRESH_TOKEN_LOCK,
];
return in_array( $option_name, $options_whitelist, true );
}
/**
* User is subscription owner.
*
* Check if current user is subscription owner.
*
* @return boolean
*/
public static function user_is_subscription_owner(): bool {
$owner_id = (int) self::get_connect_mode_data( self::OPTION_OWNER_USER_ID, false );
return get_current_user_id() === $owner_id;
}
/**
* clear_session
*/
public static function clear_session( $with_client = false ) {
if ( Config::CONNECT_MODE === 'site' ) {
if ( $with_client ) {
self::delete_option( self::CLIENT_ID );
self::delete_option( self::CLIENT_SECRET );
}
self::delete_option( self::ACCESS_TOKEN );
self::delete_option( self::REFRESH_TOKEN );
self::delete_option( self::TOKEN_ID );
self::delete_option( self::SUBSCRIPTION_ID );
self::delete_option( self::OPTION_OWNER_USER_ID );
self::delete_option( self::HOME_URL );
} else {
$user_id = get_current_user_id();
if ( $with_client ) {
self::delete_user_data( $user_id, self::CLIENT_ID );
self::delete_user_data( $user_id, self::CLIENT_SECRET );
}
self::delete_user_data( $user_id, self::ACCESS_TOKEN );
self::delete_user_data( $user_id, self::REFRESH_TOKEN );
self::delete_user_data( $user_id, self::TOKEN_ID );
self::delete_user_data( $user_id, self::SUBSCRIPTION_ID );
self::delete_user_data( $user_id, self::OPTION_OWNER_USER_ID );
self::delete_user_data( $user_id, self::HOME_URL );
}
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace EA11y\Modules\Connect\Classes\Exceptions;
use Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Service_Exception extends Exception {
protected $message = 'Service Exception';
}

View file

@ -0,0 +1,16 @@
<?php
namespace EA11y\Modules\Connect\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class GrantTypes
*/
class GrantTypes extends Basic_Enum {
const CLIENT_CREDENTIALS = 'client_credentials';
const AUTHORIZATION_CODE = 'authorization_code';
const REFRESH_TOKEN = 'refresh_token';
}

View file

@ -0,0 +1,43 @@
<?php
namespace EA11y\Modules\Connect\Classes;
use EA11y\Classes\Rest\Route;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Route_Base
*/
class Route_Base extends Route {
protected bool $override = false;
protected $auth = true;
protected string $path = '';
public function get_methods(): array {
return [];
}
public function get_endpoint(): string {
return 'connect/' . $this->get_path();
}
public function get_path(): string {
return $this->path;
}
public function get_name(): string {
return '';
}
public function post_permission_callback( \WP_REST_Request $request ): bool {
return $this->get_permission_callback( $request );
}
public function get_permission_callback( \WP_REST_Request $request ): bool {
$valid = $this->permission_callback( $request );
return $valid && user_can( $this->current_user_id, 'manage_options' );
}
}

View file

@ -0,0 +1,323 @@
<?php
namespace EA11y\Modules\Connect\Classes;
use EA11y\Classes\Logger;
use EA11y\Modules\Connect\Classes\Exceptions\Service_Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class Service
*/
class Service {
const REFRESH_TOKEN_LOCK = '_connect_refresh_token';
/**
* Registers new client and returns client ID
*
* @return string
* @throws Service_Exception
*/
public static function register_client(): string {
$clients_url = Utils::get_clients_url();
if ( ! $clients_url ) {
throw new Service_Exception( 'Missing client registration URL' );
}
$client_data = self::request( $clients_url, [
'method' => 'POST',
'headers' => [
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'redirect_uri' => Utils::get_redirect_uri(),
'app_type' => Config::APP_TYPE,
]),
], 201 );
$client_id = $client_data['client_id'] ?? null;
$client_secret = $client_data['client_secret'] ?? null;
Data::set_client_id( $client_id );
Data::set_client_secret( $client_secret );
Data::set_home_url();
return $client_id;
}
/**
* Deactivate license
*
* @return void
* @throws Service_Exception
*/
public static function deactivate_license(): void {
$client_id = Data::get_client_id();
if ( ! $client_id ) {
throw new Service_Exception( 'Missing client ID' );
}
$deactivation_url = Utils::get_deactivation_url( $client_id );
if ( ! $deactivation_url ) {
throw new Service_Exception( 'Missing deactivation URL' );
}
$access_token = Data::get_access_token();
if ( ! $access_token ) {
throw new Service_Exception( 'Missing access token' );
}
$refresh_token = Data::get_refresh_token();
if ( ! $refresh_token ) {
throw new Service_Exception( 'Missing refresh token' );
}
self::request($deactivation_url, [
'method' => 'DELETE',
'headers' => [
'Authorization' => "Bearer {$access_token}",
],
], 204);
self::get_token( 'refresh_token', $refresh_token );
}
/**
* disconnect
*
* @return void
* @throws Service_Exception
*/
public static function disconnect(): void {
$sessions_url = Utils::get_sessions_url();
if ( ! $sessions_url ) {
throw new Service_Exception( 'Missing sessions URL' );
}
$access_token = Data::get_access_token();
if ( ! $access_token ) {
throw new Service_Exception( 'Missing access token' );
}
self::request( $sessions_url, [
'method' => 'DELETE',
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => "Bearer {$access_token}",
],
], 204 );
Data::clear_session();
}
/**
* disconnect
*
* @return void
* @throws Service_Exception
*/
public static function reconnect(): void {
$sessions_url = Utils::get_sessions_url();
if ( ! $sessions_url ) {
throw new Service_Exception( 'Missing sessions URL' );
}
Data::clear_session();
}
/**
* Get token & optionally save to user
*
* @param string $grant_type
*
* @param string|null $credential
* @param bool|null $update
*
* @return array
* @throws Service_Exception
*/
public static function get_token( string $grant_type, ?string $credential = null, ?bool $update = true ): array {
$token_url = Utils::get_token_url();
if ( ! $token_url ) {
throw new Service_Exception( 'Missing token URL' );
}
$client_id = Data::get_client_id();
$client_secret = Data::get_client_secret();
if ( empty( $client_id ) || empty( $client_secret ) ) {
throw new Service_Exception( 'Missing client ID or secret' );
}
$body = [
'grant_type' => $grant_type,
'redirect_uri' => Utils::get_redirect_uri(),
];
switch ( $grant_type ) {
case GrantTypes::AUTHORIZATION_CODE:
$body['code'] = $credential;
break;
case GrantTypes::REFRESH_TOKEN:
$body[ GrantTypes::REFRESH_TOKEN ] = $credential;
break;
case GrantTypes::CLIENT_CREDENTIALS:
$body['redirect_uri'] = Utils::get_redirect_uri( Data::get_home_url() );
break;
default:
throw new Service_Exception( 'Invalid grant type' );
}
$data = self::request( $token_url, [
'method' => 'POST',
'headers' => [
'x-elementor-apps' => Config::APP_NAME,
'Authorization' => 'Basic ' . base64_encode( "{$client_id}:{$client_secret}" ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
],
'body' => $body,
] );
if ( $update ) {
Data::set_connect_mode_data( Data::TOKEN_ID, $data['id_token'] ?? null );
Data::set_connect_mode_data( Data::ACCESS_TOKEN, $data['access_token'] ?? null );
Data::set_connect_mode_data( Data::REFRESH_TOKEN, $data['refresh_token'] ?? null );
Data::set_connect_mode_data( Data::OPTION_OWNER_USER_ID, get_current_user_id() ?? null );
}
return $data;
}
public static function jwt_decode( $payload ): string {
static $jwks = null;
$jwks_url = Utils::get_jwks_url();
if ( ! $jwks_url ) {
return __( 'Missing JWKS URL', 'pojo-accessibility' );
}
if ( ! $jwks ) {
$jwks = self::request($jwks_url, [
'method' => 'GET',
]);
}
if ( ! class_exists( 'JWT' ) ) {
require_once EA11Y_PATH . 'vendor/autoload.php';
if ( ! class_exists( 'JWT' ) ) {
return __( 'JWT class not found', 'pojo-accessibility' );
}
}
try {
$decoded = \Firebase\JWT\JWT::decode( $payload, \Firebase\JWT\JWK::parseKeySet( $jwks ) );
return wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
} catch ( \Throwable $th ) {
if ( $th instanceof \Firebase\JWT\ExpiredException ) {
self::get_token( GrantTypes::REFRESH_TOKEN, Data::get_refresh_token() );
return self::jwt_decode( $payload );
}
return $th->getMessage();
}
}
/**
* @param string $url
* @param array $args
* @param int $valid_response_code
*
* @return array|null
* @throws Service_Exception
*/
public static function request( string $url, array $args, int $valid_response_code = 200 ): ?array {
$args['timeout'] = 30;
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
Logger::error( $response->get_error_message() );
throw new Service_Exception( esc_html( $response->get_error_message() ) );
}
if ( wp_remote_retrieve_response_code( $response ) !== $valid_response_code ) {
Logger::error( 'Invalid status code ' . wp_remote_retrieve_response_code( $response ) );
throw new Service_Exception( esc_html( wp_remote_retrieve_body( $response ) ) );
}
return json_decode( wp_remote_retrieve_body( $response ), true );
}
/**
* @throws Service_Exception
*/
public static function refresh_token() {
$lock_key = Config::APP_NAME . self::REFRESH_TOKEN_LOCK;
$last_token = Data::fetch_option( $lock_key, '' );
$current_refresh_token = Data::get_refresh_token();
if ( ! empty( $last_token ) && $last_token === $current_refresh_token ) {
sleep( 1 );
delete_option( $lock_key );
return;
}
delete_option( $lock_key );
$locked = Data::insert_option_uniquely( $lock_key, $current_refresh_token );
if ( ! $locked ) {
sleep( 1 );
delete_option( $lock_key );
return;
}
self::get_token( GrantTypes::REFRESH_TOKEN, $current_refresh_token );
delete_option( $lock_key );
}
/**
* @throws Service_Exception
*/
public static function update_redirect_uri(): void {
$client_id = Data::get_client_id();
if ( ! $client_id ) {
throw new Service_Exception( 'Missing client ID' );
}
$client_patch_url = Utils::get_clients_patch_url( $client_id );
[ 'access_token' => $access_token ] = self::get_token(
GrantTypes::CLIENT_CREDENTIALS,
null,
false
);
self::request( $client_patch_url, [
'method' => 'PATCH',
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => "Bearer {$access_token}",
],
'body' => wp_json_encode( [
'redirect_uri' => Utils::get_redirect_uri(),
] ),
] );
self::refresh_token();
Data::set_home_url();
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace EA11y\Modules\Connect\Classes;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Utils
*/
class Utils {
/**
* get_clients_url
* @return string
*/
public static function get_clients_url(): string {
return self::get_base_url() . '/api/v1/clients';
}
/**
* get_redirect_uri
* @return string
*/
public static function get_redirect_uri( string $domain = '' ) : string {
if ( false !== strpos( Config::ADMIN_PAGE, '?page=' ) ) {
$admin_url = admin_url( Config::ADMIN_PAGE );
} else {
$admin_url = admin_url( 'options-general.php?page=' . Config::ADMIN_PAGE );
}
if ( $domain ) {
$parsed_url = wp_parse_url( $admin_url );
$path = $parsed_url['path'] . ( isset( $parsed_url['query'] ) ? '?' . $parsed_url['query'] : '' );
return rtrim( $domain, '/' ) . $path;
}
return $admin_url;
}
public static function get_auth_url(): string {
return self::get_base_url() . '/v1/oauth2/auth';
}
/**
* Get full authorization URL with all required parameters
*
* @param string $client_id
*
* @return string
*/
public static function get_authorize_url( string $client_id ): string {
return add_query_arg( [
'client_id' => $client_id,
'redirect_uri' => rawurlencode( self::get_redirect_uri() ),
'response_type' => 'code',
'scope' => Config::SCOPES,
'state' => wp_create_nonce( Config::STATE_NONCE ),
], self::get_auth_url() );
}
/**
* get_deactivation_url
* @param string $client_id
*
* @return string
*/
public static function get_deactivation_url( string $client_id ): string {
return self::get_base_url() . "/api/v1/clients/{$client_id}/activation";
}
public static function get_jwks_url(): string {
return self::get_base_url() . '/v1/.well-known/jwks.json';
}
/**
* get_sessions_url
* @return string
*/
public static function get_sessions_url(): string {
return self::get_base_url() . '/api/v1/session';
}
public static function get_token_url(): string {
return self::get_base_url() . '/api/v1/oauth2/token';
}
/**
* Get clients URL
*
* @param string $client_id
*
* @return string
*/
public static function get_clients_patch_url( string $client_id ): string {
return Utils::get_base_url() . "/api/v1/clients/{$client_id}";
}
/**
* get_base_url
* @return string
*/
public static function get_base_url(): string {
return apply_filters( 'ea11y_connect_get_base_url', Config::BASE_URL );
}
/**
* is_valid_home_url
* @return bool
*/
public static function is_valid_home_url(): bool {
static $valid = null;
if ( null === $valid ) {
if ( empty( Data::get_home_url() ) ) {
$valid = true;
} else {
$valid = Data::get_home_url() === home_url();
}
}
return $valid;
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace EA11y\Modules\Connect\Components;
use EA11y\Modules\Connect\Classes\{
Config,
Data,
GrantTypes,
Service,
Utils,
};
use EA11y\Classes\Logger;
use Throwable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Handler
*/
class Handler {
private function should_handle_auth_code(): bool {
global $plugin_page;
$page_slug = explode( 'page=', Config::ADMIN_PAGE );
$is_connect_admin_page = false;
if ( ! empty( $page_slug[1] ) && $page_slug[1] === $plugin_page ) {
$is_connect_admin_page = true;
}
if ( ! $is_connect_admin_page && Config::ADMIN_PAGE === $plugin_page ) {
$is_connect_admin_page = true;
}
if ( ! $is_connect_admin_page ) {
return false;
}
$code = $_GET['code'] ?? null;
$state = $_GET['state'] ?? null;
if ( empty( $code ) || empty( $state ) ) {
return false;
}
return true;
}
private function validate_nonce( $state ) {
if ( ! wp_verify_nonce( $state, Config::STATE_NONCE ) ) {
wp_die( 'Invalid state' );
}
}
public function handle_auth_code() {
if ( ! $this->should_handle_auth_code() ) {
return;
}
$code = sanitize_text_field( $_GET['code'] );
$state = sanitize_text_field( $_GET['state'] );
// Check if the state is valid
$this->validate_nonce( $state );
try {
// Exchange the code for an access token and store it
Service::get_token( GrantTypes::AUTHORIZATION_CODE, $code ); // Makes sure we won't stick in the mismatch limbo
Data::set_home_url();
do_action( 'on_connect_' . Config::APP_PREFIX . '_connected' ); // Redirect to the redirect URI
} catch ( Throwable $t ) {
Logger::error( 'Unable to handle auth code: ' . $t->getMessage() );
}
wp_redirect( Utils::get_redirect_uri() );
exit;
}
/**
* Handler constructor.
*/
public function __construct() {
add_action( 'admin_init', [ $this, 'handle_auth_code' ] );
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace EA11y\Modules\Connect;
use EA11y\Classes\Module_Base;
use EA11y\Modules\Connect\Classes\Data;
use EA11y\Modules\Connect\Classes\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Module
*/
class Module extends Module_Base {
/**
* Get module name.
* Retrieve the module name.
* @access public
* @return string Module name.
*/
public function get_name() {
return 'connect';
}
/**
* component_list
* @return string[]
*/
public static function component_list() : array {
return [
'Handler',
];
}
/**
* routes_list
* @return string[]
*/
public static function routes_list() : array {
return [
'Authorize',
'Disconnect',
'Deactivate',
'Deactivate_And_Disconnect',
'Switch_Domain',
'Reconnect',
];
}
public static function is_connected() : bool {
return ! ! Data::get_access_token() && Utils::is_valid_home_url();
}
public function authorize_url( $authorize_url ) {
$utm_params = [];
$a11y_campaign = get_transient( 'elementor_ea11y_campaign' );
if ( false === $a11y_campaign ) {
return $authorize_url;
}
foreach ( [ 'source', 'medium', 'campaign' ] as $key ) {
if ( ! empty( $a11y_campaign[ $key ] ) ) {
$utm_params[ 'utm_' . $key ] = $a11y_campaign[ $key ];
}
}
if ( ! empty( $utm_params ) ) {
$authorize_url = add_query_arg( $utm_params, $authorize_url );
}
return $authorize_url;
}
public function __construct() {
$this->register_components();
$this->register_routes();
add_filter( 'ea11y_connect_authorize_url', [ $this, 'authorize_url' ] );
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Data,
Route_Base,
Service,
Utils
};
use EA11y\Modules\Connect\Module as Connect;
use Throwable;
use WP_REST_Request;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Authorize
*/
class Authorize extends Route_Base {
public string $path = 'authorize';
public const NONCE_NAME = 'wp_rest';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'authorize';
}
public function POST( WP_REST_Request $request ) {
$this->verify_nonce_and_capability(
$request->get_param( self::NONCE_NAME ),
self::NONCE_NAME
);
if ( Connect::is_connected() && Utils::is_valid_home_url() ) {
return $this->respond_error_json( [
'message' => esc_html__( 'You are already connected', 'pojo-accessibility' ),
'code' => 'forbidden',
] );
}
try {
$client_id = Data::get_client_id();
if ( ! $client_id ) {
$client_id = Service::register_client();
}
if ( ! Utils::is_valid_home_url() ) {
if ( $request->get_param( 'update_redirect_uri' ) ) {
Service::update_redirect_uri();
} else {
return $this->respond_error_json( [
'message' => esc_html__( 'Connected domain mismatch', 'pojo-accessibility' ),
'code' => 'forbidden',
] );
}
}
$authorize_url = Utils::get_authorize_url( $client_id );
$authorize_url = apply_filters( 'ea11y_connect_authorize_url', $authorize_url );
return $this->respond_success_json( $authorize_url );
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Data,
Route_Base,
Service,
};
use Throwable;
use WP_REST_Request;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Disconnect
*/
class Deactivate_And_Disconnect extends Route_Base {
public string $path = 'deactivate_and_disconnect';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'deactivate_and_disconnect';
}
public function POST( WP_REST_Request $request ) {
try {
if ( $request->get_param( 'clear_session' ) ) {
Data::clear_session( true );
return $this->respond_success_json();
}
Service::deactivate_license();
Service::disconnect();
return $this->respond_success_json();
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Route_Base,
Service,
};
use Throwable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Deactivate
*/
class Deactivate extends Route_Base {
public string $path = 'deactivate';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'deactivate';
}
public function POST() {
try {
Service::deactivate_license();
return $this->respond_success_json();
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Route_Base,
Service,
};
use Throwable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Disconnect
*/
class Disconnect extends Route_Base {
public string $path = 'disconnect';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'disconnect';
}
public function POST() {
try {
Service::refresh_token();
Service::disconnect();
return $this->respond_success_json();
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Route_Base,
Service,
};
use Throwable;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Reconnect
*/
class Reconnect extends Route_Base {
public string $path = 'reconnect';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'reconnect';
}
public function POST() {
try {
Service::reconnect();
return $this->respond_success_json();
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace EA11y\Modules\Connect\Rest;
use EA11y\Modules\Connect\Classes\{
Data,
Route_Base,
Service,
Utils
};
use Throwable;
use WP_REST_Request;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Switch_Domain
*/
class Switch_Domain extends Route_Base {
public string $path = 'switch_domain';
public const NONCE_NAME = 'wp_rest';
public function get_methods(): array {
return [ 'POST' ];
}
public function get_name(): string {
return 'switch_domain';
}
public function POST( WP_REST_Request $request ) {
$this->verify_nonce_and_capability(
$request->get_param( self::NONCE_NAME ),
self::NONCE_NAME
);
try {
$client_id = Data::get_client_id();
if ( ! $client_id ) {
return $this->respond_error_json( [
'message' => __( 'Client ID not found', 'pojo-accessibility' ),
'code' => 'ignore_error',
] );
}
Service::update_redirect_uri();
return $this->respond_success_json( [ 'message' => __( 'Domain updated!', 'pojo-accessibility' ) ] );
} catch ( Throwable $t ) {
return $this->respond_error_json( [
'message' => $t->getMessage(),
'code' => 'internal_server_error',
] );
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace EA11y\Modules\Core\Components;
use EA11y\Classes\Utils\Notice_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Notices {
const AJAX_ACTION = 'ea11y_admin_notice_dismiss';
/**
* @var Notice_Base[] $notices
*/
public array $notices = [];
public function register_notice( $notice_instance ) {
$this->notices[ $notice_instance->get_id() ] = $notice_instance;
}
public function show_notices() {
foreach ( $this->notices as $notice ) {
$notice->maybe_show_notice();
}
}
public function handle_dismiss() {
if ( empty( $_REQUEST['notice_id'] ) ) {
wp_send_json_error( [ 'message' => 'Invalid ID' ] );
}
$notice = $this->get_notice( sanitize_text_field( wp_unslash( $_REQUEST['notice_id'] ) ) );
if ( ! $notice ) {
wp_send_json_error( [ 'message' => 'Invalid ID' ] );
}
$notice->handle_dismiss();
wp_send_json_success( [] );
}
/**
* @param string $sanitize_text_field
*
* @return Notice_Base|null
*/
private function get_notice( string $sanitize_text_field ): ?Notice_Base {
return $this->notices[ $sanitize_text_field ] ?? null;
}
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'admin_notices', [ $this, 'show_notices' ] );
add_action( 'wp_ajax_' . self::AJAX_ACTION, [ $this, 'handle_dismiss' ] );
do_action( 'ea11y_register_notices', $this );
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace EA11y\Modules\Core\Components;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Pointers {
const DISMISSED_POINTERS_META_KEY = 'ea11y_dismissed_pointers';
public function dismiss_pointers() {
if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'ea11y-pointer-dismissed' ) ) {
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
}
$pointer = sanitize_text_field( $_POST['data']['pointer'] ) ?? null;
if ( empty( $pointer ) ) {
wp_send_json_error( [ 'message' => 'The pointer id must be provided' ] );
}
$pointer = explode( ',', $pointer );
$user_dismissed_meta = get_user_meta( get_current_user_id(), self::DISMISSED_POINTERS_META_KEY, true );
if ( ! $user_dismissed_meta ) {
$user_dismissed_meta = [];
}
foreach ( $pointer as $item ) {
$user_dismissed_meta[ $item ] = true;
}
update_user_meta( get_current_user_id(), self::DISMISSED_POINTERS_META_KEY, $user_dismissed_meta );
wp_send_json_success( [] );
}
public static function is_dismissed( string $slug ): bool {
$meta = (array) get_user_meta( get_current_user_id(), self::DISMISSED_POINTERS_META_KEY, true );
return key_exists( $slug, $meta );
}
public function __construct() {
add_action( 'wp_ajax_ea11y_pointer_dismissed', [ $this, 'dismiss_pointers' ] );
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace EA11y\Modules\Core\Components;
use EA11y\Modules\Settings\Classes\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Skip_Link {
private $settings;
public function get_name(): string {
return 'skip-link';
}
public function enqueue_skip_link_styles() {
wp_enqueue_style(
'ea11y-skip-link',
EA11Y_ASSETS_URL . '/css/skip-link.css',
[],
EA11Y_VERSION
);
}
public function render_skip_link() {
?>
<script>
const onSkipLinkClick = () => {
const htmlElement = document.querySelector('html');
htmlElement.style['scroll-behavior'] = 'smooth';
setTimeout( () => htmlElement.style['scroll-behavior'] = null, 1000 );
}
document.addEventListener("DOMContentLoaded", () => {
if (!document.querySelector('<?php echo esc_url( $this->settings['anchor'] ); ?>')) {
document.querySelector('.ea11y-skip-to-content-link').remove();
}
});
</script>
<a class="ea11y-skip-to-content-link"
href="<?php echo esc_url( $this->settings['anchor'] ); ?>"
tabindex="1"
onclick="onSkipLinkClick()"
>
<?php esc_attr_e( 'Skip to content', 'pojo-accessibility' ); ?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6V12C18 12.7956 17.6839 13.5587 17.1213 14.1213C16.5587 14.6839 15.7956 15 15 15H5M5 15L9 11M5 15L9 19"
stroke="black"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
<div class="ea11y-skip-to-content-backdrop"></div>
<?php
}
/**
* Module constructor.
*/
public function __construct() {
$this->settings = get_option( Settings::SKIP_TO_CONTENT );
if ( $this->settings && ! empty( $this->settings['enabled'] ) ) {
remove_action( 'wp_footer', 'the_block_template_skip_link' );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_skip_link_styles' ] );
add_action( 'wp_head', [ $this, 'render_skip_link' ] );
}
}
}

71
modules/core/module.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace EA11y\Modules\Core;
use EA11y\Classes\Module_Base;
use EA11y\Modules\Connect\Module as Connect;
use EA11y\Modules\Settings\Module as Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends Module_Base {
public function get_name(): string {
return 'core';
}
public static function component_list() : array {
return [
'Pointers',
'Notices',
'Skip_Link',
];
}
public function add_plugin_links( $links, $plugin_file_name ) : array {
if ( ! str_ends_with( $plugin_file_name, '/pojo-accessibility.php' ) ) {
return (array) $links;
}
$custom_links = [
'settings' => sprintf(
'<a href="%s">%s</a>',
admin_url( 'admin.php?page=' . Settings::SETTING_BASE_SLUG ),
esc_html__( 'Settings', 'pojo-accessibility' )
),
];
if ( ! Connect::is_connected() ) {
$custom_links['connect'] = sprintf(
'<a href="%s" style="color: #524CFF; font-weight: 700;">%s</a>',
admin_url( 'admin.php?page=' . Settings::SETTING_BASE_SLUG ),
esc_html__( 'Connect', 'pojo-accessibility' )
);
}
return array_merge( $custom_links, $links );
}
public static function is_active() : bool {
return true;
}
public function enqueue_scripts() : void {
wp_enqueue_style(
'ea11y-global-style',
EA11Y_ASSETS_URL . 'css/admin.css',
[],
EA11Y_VERSION
);
}
/**
* Module constructor.
*/
public function __construct() {
$this->register_components();
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_filter( 'plugin_action_links', [ $this, 'add_plugin_links' ], 10, 2 );
}
}

View file

@ -1,23 +1,27 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class Pojo_A11y_Admin_UI {
namespace EA11y\Modules\Legacy\Components;
use EA11y\Classes\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Admin
*/
class Admin {
const SETTINGS_SLUG = 'toplevel_page_accessibility-settings';
const TOOLBAR_SLUG = 'accessibility_page_accessibility-toolbar';
private function _is_elementor_installed() {
$file_path = 'elementor/elementor.php';
$installed_plugins = get_plugins();
return isset( $installed_plugins[ $file_path ] );
}
public function ajax_a11y_install_elementor_set_admin_notice_viewed() {
update_user_meta( get_current_user_id(), '_a11y_elementor_install_notice', 'true' );
}
public function admin_notices() {
if ( ! current_user_can( 'install_plugins' ) || $this->_is_elementor_installed() ) {
if ( ! current_user_can( 'install_plugins' ) || Utils::is_elementor_installed() ) {
return;
}
@ -152,7 +156,7 @@ class Pojo_A11y_Admin_UI {
$current_screen = get_current_screen();
if ( in_array( $current_screen->id, array( self::SETTINGS_SLUG, self::TOOLBAR_SLUG ) ) ) {
$footer_text = sprintf(
/* translators: 1: One Click Accessibility, 2: Link to plugin review */
/* translators: 1: One Click Accessibility, 2: Link to plugin review */
__( 'Enjoyed %1$s? Please leave us a %2$s rating. We really appreciate your support!', 'pojo-accessibility' ),
'<strong>' . __( 'One Click Accessibility', 'pojo-accessibility' ) . '</strong>',
'<a href="https://wordpress.org/support/plugin/pojo-accessibility/reviews/?filter=5#new-post" target="_blank">&#9733;&#9733;&#9733;&#9733;&#9733;</a>'

View file

@ -0,0 +1,295 @@
<?php
namespace EA11y\Modules\Legacy\Components;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Customizer
*/
class Customizer {
private $css_rules = [];
private $css_code = '';
public function get_customizer_fields() {
$fields = [];
$fields[] = [
'id' => 'a11y_toolbar_icon',
'title' => __( 'Toolbar Icon', 'pojo-accessibility' ),
'type' => 'select',
'choices' => [
'one-click' => __( 'One Click', 'pojo-accessibility' ),
'wheelchair' => __( 'Wheelchair', 'pojo-accessibility' ),
'accessibility' => __( 'Accessibility', 'pojo-accessibility' ),
],
'std' => 'one-click',
'description' => __( 'Set Toolbar Icon', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_toolbar_position',
'title' => __( 'Toolbar Position', 'pojo-accessibility' ),
'type' => 'select',
'choices' => [
'left' => __( 'Left', 'pojo-accessibility' ),
'right' => __( 'Right', 'pojo-accessibility' ),
],
'std' => is_rtl() ? 'right' : 'left',
'description' => __( 'Set Toolbar Position', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_toolbar_distance_top',
'title' => __( 'Offset from Top (Desktop)', 'pojo-accessibility' ),
'type' => 'text',
'std' => '100px',
'description' => __( 'Set Toolbar top offset (Desktop)', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_toolbar_distance_top_mobile',
'title' => __( 'Offset from Top (Mobile)', 'pojo-accessibility' ),
'type' => 'text',
'std' => '50px',
'description' => __( 'Set Toolbar top offset (Mobile)', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_bg_toolbar',
'title' => __( 'Toolbar Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay',
'change_type' => 'bg_color',
'description' => __( 'Set Toolbar background color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_color_toolbar',
'title' => __( 'Toolbar Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#333333',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a, #pojo-a11y-toolbar .pojo-a11y-toolbar-overlay p.pojo-a11y-toolbar-title',
'change_type' => 'color',
'description' => __( 'Set Toolbar text color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_toggle_button_bg_color',
'title' => __( 'Toggle Button Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#4054b2',
'description' => __( 'Set Toolbar toggle button background color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_toggle_button_color',
'title' => __( 'Toggle Button Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-toggle a',
'change_type' => 'color',
'description' => __( 'Set Toolbar toggle button icon color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_bg_active',
'title' => __( 'Active Background', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#4054b2',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a.active',
'change_type' => 'bg_color',
'description' => __( 'Set Toolbar active background color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_color_active',
'title' => __( 'Active Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#ffffff',
'selector' => '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items li.pojo-a11y-toolbar-item a.active',
'change_type' => 'color',
'description' => __( 'Set Toolbar active text color', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_focus_outline_style',
'title' => __( 'Focus Outline Style', 'pojo-accessibility' ),
'type' => 'select',
'choices' => [
'solid' => __( 'Solid', 'pojo-accessibility' ),
'dotted' => __( 'Dotted', 'pojo-accessibility' ),
'dashed' => __( 'Dashed', 'pojo-accessibility' ),
'double' => __( 'Double', 'pojo-accessibility' ),
'groove' => __( 'Groove', 'pojo-accessibility' ),
'ridge' => __( 'Ridge', 'pojo-accessibility' ),
'outset' => __( 'Outset', 'pojo-accessibility' ),
'initial' => __( 'Initial', 'pojo-accessibility' ),
],
'std' => 'solid',
'description' => __( 'Set Focus outline style', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_focus_outline_width',
'title' => __( 'Focus Outline Width', 'pojo-accessibility' ),
'type' => 'select',
'choices' => [
'1px' => '1px',
'2px' => '2px',
'3px' => '3px',
'4px' => '4px',
'5px' => '5px',
'6px' => '6px',
'7px' => '7px',
'8px' => '8px',
'9px' => '9px',
'10px' => '10px',
],
'std' => '1px',
'description' => __( 'Set Focus outline width', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'a11y_focus_outline_color',
'title' => __( 'Focus Outline Color', 'pojo-accessibility' ),
'type' => 'color',
'std' => '#FF0000',
'description' => __( 'Set Focus outline color', 'pojo-accessibility' ),
];
return $fields;
}
public function customize_a11y( $wp_customize ) {
$fields = $this->get_customizer_fields();
$section_description = '<p>' . __( 'Use the control below to customize the appearance and layout of the Accessibility Toolbar', 'pojo-accessibility' ) . '</p><p>' .
sprintf( __( 'Additional Toolbar settings can be configured at the %s page.', 'pojo-accessibility' ),
'<a href="' . admin_url( 'admin.php?page=accessibility-toolbar' ) . '" target="blank">' . __( 'Accessibility Toolbar', 'pojo-accessibility' ) . '</a>'
) . '</p>' . apply_filters( 'pojo_a11y_customizer_section_description', '' );
$wp_customize->add_section( 'accessibility', [
'title' => __( 'Accessibility', 'pojo-accessibility' ),
'priority' => 30,
'description' => $section_description,
] );
foreach ( $fields as $field ) {
$customizer_id = POJO_A11Y_CUSTOMIZER_OPTIONS . '[' . $field['id'] . ']';
$wp_customize->add_setting( $customizer_id, [
'default' => $field['std'] ? $field['std'] : null,
'type' => 'option',
] );
switch ( $field['type'] ) {
case 'color':
$wp_customize->add_control( new \WP_Customize_Color_Control( $wp_customize, $field['id'], [
'label' => $field['title'],
'section' => 'accessibility',
'settings' => $customizer_id,
'description' => isset( $field['description'] ) ? $field['description'] : null,
] ) );
break;
case 'select':
case 'text':
$wp_customize->add_control( $field['id'], [
'label' => $field['title'],
'section' => 'accessibility',
'settings' => $customizer_id,
'type' => $field['type'],
'choices' => isset( $field['choices'] ) ? $field['choices'] : null,
'description' => isset( $field['description'] ) ? $field['description'] : null,
] );
break;
}
}
}
public function get_custom_css_code() {
$options = $this->get_customizer_options();
$bg_color = $options['a11y_toggle_button_bg_color']; // get_theme_mod( 'a11y_toggle_button_bg_color', '#4054b2' );
if ( ! empty( $bg_color ) ) {
$this->add_css_rule( '#pojo-a11y-toolbar .pojo-a11y-toolbar-toggle a', 'background-color', $bg_color );
$this->add_css_rule( '#pojo-a11y-toolbar .pojo-a11y-toolbar-overlay, #pojo-a11y-toolbar .pojo-a11y-toolbar-overlay ul.pojo-a11y-toolbar-items.pojo-a11y-links',
'border-color', $bg_color );
}
$outline_style = $options['a11y_focus_outline_style']; //get_theme_mod( 'a11y_focus_outline_style', 'solid' );
if ( ! empty( $outline_style ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-style', $outline_style . ' !important' );
}
$outline_width = $options['a11y_focus_outline_width']; // get_theme_mod( 'a11y_focus_outline_width', '1px' );
if ( ! empty( $outline_width ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-width', $outline_width . ' !important' );
}
$outline_color = $options['a11y_focus_outline_color']; //get_theme_mod( 'a11y_focus_outline_color', '#FF0000' );
if ( ! empty( $outline_color ) ) {
$this->add_css_rule( 'body.pojo-a11y-focusable a:focus', 'outline-color', $outline_color . ' !important' );
}
$distance_top = $options['a11y_toolbar_distance_top']; //get_theme_mod( 'a11y_toolbar_distance_top', '100px' );
if ( ! empty( $distance_top ) ) {
$this->add_css_rule( '#pojo-a11y-toolbar', 'top', $distance_top . ' !important' );
}
$distance_top_mobile = $options['a11y_toolbar_distance_top_mobile']; // get_theme_mod( 'a11y_toolbar_distance_top_mobile', '50px' );
if ( ! empty( $distance_top_mobile ) ) {
$this->add_css_code( "@media (max-width: 767px) { #pojo-a11y-toolbar { top: {$distance_top_mobile} !important; } }" );
}
$fields = $this->get_customizer_fields();
foreach ( $fields as $field ) {
if ( empty( $field['selector'] ) || empty( $field['change_type'] ) ) {
continue;
}
$option = $options[ $field['id'] ];
if ( 'color' === $field['change_type'] ) {
$this->add_css_rule( $field['selector'], 'color', $option );
} elseif ( 'bg_color' === $field['change_type'] ) {
$this->add_css_rule( $field['selector'], 'background-color', $option );
}
}
}
private function get_customizer_options() {
static $options = false;
if ( false === $options ) {
$options = get_option( POJO_A11Y_CUSTOMIZER_OPTIONS );
}
return $options;
}
private function add_css_rule( $selector, $rule, $value ) {
if ( ! isset( $this->css_rules[ $selector ] ) ) {
$this->css_rules[ $selector ] = [];
}
$this->css_rules[ $selector ][] = $rule . ': ' . $value . ';';
}
private function add_css_code( $code ) {
$this->css_code .= "\n" . $code;
}
public function print_css_code() {
$this->get_custom_css_code();
$css = '';
foreach ( $this->css_rules as $selector => $css_rules ) {
$css .= "\n" . $selector . '{ ' . implode( "\t", $css_rules ) . '}';
}
echo '<style type="text/css">' . $css . $this->css_code . '</style>';
}
public function __construct() {
add_filter( 'customize_register', [ $this, 'customize_a11y' ], 610 );
add_filter( 'wp_head', [ $this, 'print_css_code' ] );
}
}

View file

@ -1,8 +1,15 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
final class Pojo_A11y_Elementor {
namespace EA11y\Modules\Legacy\Components;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Elementor
*/
class Elementor {
public function is_toolbar_active( $is_active ) {
if ( \Elementor\Plugin::$instance->preview->is_preview_mode() ) {
$is_active = false;

View file

@ -1,8 +1,18 @@
<?php
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
final class Pojo_A11y_Frontend {
namespace EA11y\Modules\Legacy\Components;
use EA11y\Modules\Legacy\Module;
use EA11y\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Frontend
*/
class Frontend {
public $svg_icons = null;
public function is_toolbar_active() {
@ -13,30 +23,33 @@ final class Pojo_A11y_Frontend {
return $is_active;
}
public function is_toolbar_button_active( $button_type ) {
public function is_toolbar_button_active( $button_type ): bool {
return 'disable' !== get_option( "pojo_a11y_toolbar_button_{$button_type}" );
}
public function get_toolbar_button_title( $button_type ) {
$title = Pojo_Accessibility::$instance->settings->get_default_title_text( "pojo_a11y_toolbar_button_{$button_type}_title" );
/**
* @var Settings $settings
*/
$settings = Module::get_settings();
$title = $settings->get_default_title_text( "pojo_a11y_toolbar_button_{$button_type}_title" );
return '<span class="pojo-a11y-toolbar-icon">' . $this->get_toolbar_svg( $button_type, $title ) . '</span><span class="pojo-a11y-toolbar-text">' . $title . '</span>';
}
public function enqueue_scripts() {
$assets_url = EA11Y_URL . 'modules/legacy/assets/';
wp_register_script(
'pojo-a11y',
POJO_A11Y_ASSETS_URL . 'js/app.min.js',
array(
'jquery',
),
$assets_url . 'js/app.min.js',
[ 'jquery' ],
'1.0.0',
true
);
wp_register_style(
'pojo-a11y',
POJO_A11Y_ASSETS_URL . 'css/style.min.css',
array(),
$assets_url . 'css/style.min.css',
[],
'1.0.0'
);
@ -46,13 +59,13 @@ final class Pojo_A11y_Frontend {
wp_localize_script(
'pojo-a11y',
'PojoA11yOptions',
array(
[
'focusable' => ( 'enable' === get_option( 'pojo_a11y_focusable' ) ),
'remove_link_target' => ( 'enable' === get_option( 'pojo_a11y_remove_link_target' ) ),
'add_role_links' => ( 'enable' === get_option( 'pojo_a11y_add_role_links' ) ),
'enable_save' => ( 'enable' === get_option( 'pojo_a11y_save' ) ),
'save_expiration' => get_option( 'pojo_a11y_save_expiration' ),
)
]
);
}
@ -77,16 +90,16 @@ final class Pojo_A11y_Frontend {
$customizer_options = get_option( POJO_A11Y_CUSTOMIZER_OPTIONS );
$toolbar_position = $customizer_options['a11y_toolbar_position'];
if ( empty( $toolbar_position ) || ! in_array( $toolbar_position, array( 'right', 'left' ) ) ) {
if ( empty( $toolbar_position ) || ! in_array( $toolbar_position, [ 'right', 'left' ] ) ) {
$toolbar_position = 'left';
}
$toolbar_title = Pojo_Accessibility::$instance->settings->get_default_title_text( 'pojo_a11y_toolbar_title' );
$settings = Module::get_settings();
$toolbar_title = $settings->get_default_title_text( 'pojo_a11y_toolbar_title' );
$toolbar_visibility = get_option( 'pojo_a11y_toolbar' );
$wrapper_classes = array(
$wrapper_classes = [
'pojo-a11y-toolbar-' . $toolbar_position,
);
];
if ( 'enable' !== $toolbar_visibility ) {
$wrapper_classes[] = 'pojo-a11y-' . $toolbar_visibility;
@ -114,7 +127,7 @@ final class Pojo_A11y_Frontend {
<div class="pojo-a11y-toolbar-overlay">
<div class="pojo-a11y-toolbar-inner">
<p class="pojo-a11y-toolbar-title"><?php echo $toolbar_title; ?></p>
<ul class="pojo-a11y-toolbar-items pojo-a11y-tools">
<?php do_action( 'pojo_a11y_toolbar_before_buttons' ); ?>
<?php if ( $this->is_toolbar_button_active( 'resize_font' ) ) : ?>
@ -123,7 +136,7 @@ final class Pojo_A11y_Frontend {
<?php echo $this->get_toolbar_button_title( 'resize_font_add' ); ?>
</a>
</li>
<li class="pojo-a11y-toolbar-item">
<a href="#" class="pojo-a11y-toolbar-link pojo-a11y-btn-resize-font pojo-a11y-btn-resize-minus" data-action="resize-minus" data-action-group="resize" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'resize_font_less' ); ?>
@ -188,29 +201,29 @@ final class Pojo_A11y_Frontend {
</li>
</ul>
<?php if ( $has_custom_links ) : ?>
<ul class="pojo-a11y-toolbar-items pojo-a11y-links">
<?php if ( ! empty( $sitemap_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $sitemap_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-sitemap" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'sitemap' ); ?>
</a>
</li>
<?php endif; ?>
<?php if ( ! empty( $help_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $help_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-help" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'help' ); ?>
</a>
</li>
<?php endif; ?>
<?php if ( ! empty( $feedback_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $feedback_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-feedback" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'feedback' ); ?>
</a>
</li>
<?php endif; ?>
</ul>
<ul class="pojo-a11y-toolbar-items pojo-a11y-links">
<?php if ( ! empty( $sitemap_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $sitemap_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-sitemap" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'sitemap' ); ?>
</a>
</li>
<?php endif; ?>
<?php if ( ! empty( $help_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $help_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-help" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'help' ); ?>
</a>
</li>
<?php endif; ?>
<?php if ( ! empty( $feedback_link ) ) : ?>
<li class="pojo-a11y-toolbar-item">
<a href="<?php echo esc_attr( $feedback_link ); ?>" class="pojo-a11y-toolbar-link pojo-a11y-link-feedback" tabindex="-1" role="button">
<?php echo $this->get_toolbar_button_title( 'feedback' ); ?>
</a>
</li>
<?php endif; ?>
</ul>
<?php endif; ?>
</div>
</div>
@ -219,7 +232,7 @@ final class Pojo_A11y_Frontend {
}
private function get_toolbar_svg( $icon, $icon_title = '' ) {
$icons = array(
$icons = [
'resize_font_add' => '<path fill="currentColor" d="M256 200v16c0 4.25-3.75 8-8 8h-56v56c0 4.25-3.75 8-8 8h-16c-4.25 0-8-3.75-8-8v-56h-56c-4.25 0-8-3.75-8-8v-16c0-4.25 3.75-8 8-8h56v-56c0-4.25 3.75-8 8-8h16c4.25 0 8 3.75 8 8v56h56c4.25 0 8 3.75 8 8zM288 208c0-61.75-50.25-112-112-112s-112 50.25-112 112 50.25 112 112 112 112-50.25 112-112zM416 416c0 17.75-14.25 32-32 32-8.5 0-16.75-3.5-22.5-9.5l-85.75-85.5c-29.25 20.25-64.25 31-99.75 31-97.25 0-176-78.75-176-176s78.75-176 176-176 176 78.75 176 176c0 35.5-10.75 70.5-31 99.75l85.75 85.75c5.75 5.75 9.25 14 9.25 22.5z"></path>',
'resize_font_less' => '<path fill="currentColor" d="M256 200v16c0 4.25-3.75 8-8 8h-144c-4.25 0-8-3.75-8-8v-16c0-4.25 3.75-8 8-8h144c4.25 0 8 3.75 8 8zM288 208c0-61.75-50.25-112-112-112s-112 50.25-112 112 50.25 112 112 112 112-50.25 112-112zM416 416c0 17.75-14.25 32-32 32-8.5 0-16.75-3.5-22.5-9.5l-85.75-85.5c-29.25 20.25-64.25 31-99.75 31-97.25 0-176-78.75-176-176s78.75-176 176-176 176 78.75 176 176c0 35.5-10.75 70.5-31 99.75l85.75 85.75c5.75 5.75 9.25 14 9.25 22.5z"></path>',
'grayscale' => '<path fill="currentColor" d="M15.75 384h-15.75v-352h15.75v352zM31.5 383.75h-8v-351.75h8v351.75zM55 383.75h-7.75v-351.75h7.75v351.75zM94.25 383.75h-7.75v-351.75h7.75v351.75zM133.5 383.75h-15.5v-351.75h15.5v351.75zM165 383.75h-7.75v-351.75h7.75v351.75zM180.75 383.75h-7.75v-351.75h7.75v351.75zM196.5 383.75h-7.75v-351.75h7.75v351.75zM235.75 383.75h-15.75v-351.75h15.75v351.75zM275 383.75h-15.75v-351.75h15.75v351.75zM306.5 383.75h-15.75v-351.75h15.75v351.75zM338 383.75h-15.75v-351.75h15.75v351.75zM361.5 383.75h-15.75v-351.75h15.75v351.75zM408.75 383.75h-23.5v-351.75h23.5v351.75zM424.5 383.75h-8v-351.75h8v351.75zM448 384h-15.75v-352h15.75v352z"></path>',
@ -232,7 +245,7 @@ final class Pojo_A11y_Frontend {
'sitemap' => '<path fill="currentColor" d="M448 312v80c0 13.25-10.75 24-24 24h-80c-13.25 0-24-10.75-24-24v-80c0-13.25 10.75-24 24-24h24v-48h-128v48h24c13.25 0 24 10.75 24 24v80c0 13.25-10.75 24-24 24h-80c-13.25 0-24-10.75-24-24v-80c0-13.25 10.75-24 24-24h24v-48h-128v48h24c13.25 0 24 10.75 24 24v80c0 13.25-10.75 24-24 24h-80c-13.25 0-24-10.75-24-24v-80c0-13.25 10.75-24 24-24h24v-48c0-17.5 14.5-32 32-32h128v-48h-24c-13.25 0-24-10.75-24-24v-80c0-13.25 10.75-24 24-24h80c13.25 0 24 10.75 24 24v80c0 13.25-10.75 24-24 24h-24v48h128c17.5 0 32 14.5 32 32v48h24c13.25 0 24 10.75 24 24z"></path>',
'help' => '<path fill="currentColor" d="M224 344v-48c0-4.5-3.5-8-8-8h-48c-4.5 0-8 3.5-8 8v48c0 4.5 3.5 8 8 8h48c4.5 0 8-3.5 8-8zM288 176c0-45.75-48-80-91-80-40.75 0-71.25 17.5-92.75 53.25-2.25 3.5-1.25 8 2 10.5l33 25c1.25 1 3 1.5 4.75 1.5 2.25 0 4.75-1 6.25-3 11.75-15 16.75-19.5 21.5-23 4.25-3 12.5-6 21.5-6 16 0 30.75 10.25 30.75 21.25 0 13-6.75 19.5-22 26.5-17.75 8-42 28.75-42 53v9c0 4.5 3.5 8 8 8h48c4.5 0 8-3.5 8-8v0c0-5.75 7.25-18 19-24.75 19-10.75 45-25.25 45-63.25zM384 224c0 106-86 192-192 192s-192-86-192-192 86-192 192-192 192 86 192 192z"></path>',
'feedback' => '<path fill="currentColor" d="M448 224c0 88.5-100.25 160-224 160-12.25 0-24.5-0.75-36.25-2-32.75 29-71.75 49.5-115 60.5-9 2.5-18.75 4.25-28.5 5.5-5.5 0.5-10.75-3.5-12-9.5v-0.25c-1.25-6.25 3-10 6.75-14.5 15.75-17.75 33.75-32.75 45.5-74.5-51.5-29.25-84.5-74.5-84.5-125.25 0-88.25 100.25-160 224-160s224 71.5 224 160z"></path>',
);
];
if ( isset( $icons[ $icon ] ) ) {
$icon_title_html = '';
@ -248,11 +261,11 @@ final class Pojo_A11y_Frontend {
private function get_svg_icon( $icon ) {
if ( null === $this->svg_icons ) {
$this->svg_icons = array(
$this->svg_icons = [
'wheelchair' => '<g><path d="M60.4,78.9c-2.2,4.1-5.3,7.4-9.2,9.8c-4,2.4-8.3,3.6-13,3.6c-6.9,0-12.8-2.4-17.7-7.3c-4.9-4.9-7.3-10.8-7.3-17.7c0-5,1.4-9.5,4.1-13.7c2.7-4.2,6.4-7.2,10.9-9.2l-0.9-7.3c-6.3,2.3-11.4,6.2-15.3,11.8C7.9,54.4,6,60.6,6,67.3c0,5.8,1.4,11.2,4.3,16.1s6.8,8.8,11.7,11.7c4.9,2.9,10.3,4.3,16.1,4.3c7,0,13.3-2.1,18.9-6.2c5.7-4.1,9.6-9.5,11.7-16.2l-5.7-11.4C63.5,70.4,62.5,74.8,60.4,78.9z"/><path d="M93.8,71.3l-11.1,5.5L70,51.4c-0.6-1.3-1.7-2-3.2-2H41.3l-0.9-7.2h22.7v-7.2H39.6L37.5,19c2.5,0.3,4.8-0.5,6.7-2.3c1.9-1.8,2.9-4,2.9-6.6c0-2.5-0.9-4.6-2.6-6.3c-1.8-1.8-3.9-2.6-6.3-2.6c-2,0-3.8,0.6-5.4,1.8c-1.6,1.2-2.7,2.7-3.2,4.6c-0.3,1-0.4,1.8-0.3,2.3l5.4,43.5c0.1,0.9,0.5,1.6,1.2,2.3c0.7,0.6,1.5,0.9,2.4,0.9h26.4l13.4,26.7c0.6,1.3,1.7,2,3.2,2c0.6,0,1.1-0.1,1.6-0.4L97,77.7L93.8,71.3z"/></g>',
'one-click' => '<path d="M50 .8c5.7 0 10.4 4.7 10.4 10.4S55.7 21.6 50 21.6s-10.4-4.7-10.4-10.4S44.3.8 50 .8zM92.2 32l-21.9 2.3c-2.6.3-4.6 2.5-4.6 5.2V94c0 2.9-2.3 5.2-5.2 5.2H60c-2.7 0-4.9-2.1-5.2-4.7l-2.2-24.7c-.1-1.5-1.4-2.5-2.8-2.4-1.3.1-2.2 1.1-2.4 2.4l-2.2 24.7c-.2 2.7-2.5 4.7-5.2 4.7h-.5c-2.9 0-5.2-2.3-5.2-5.2V39.4c0-2.7-2-4.9-4.6-5.2L7.8 32c-2.6-.3-4.6-2.5-4.6-5.2v-.5c0-2.6 2.1-4.7 4.7-4.7h.5c19.3 1.8 33.2 2.8 41.7 2.8s22.4-.9 41.7-2.8c2.6-.2 4.9 1.6 5.2 4.3v1c-.1 2.6-2.1 4.8-4.8 5.1z"/>',
'accessibility' => '<path d="M50 8.1c23.2 0 41.9 18.8 41.9 41.9 0 23.2-18.8 41.9-41.9 41.9C26.8 91.9 8.1 73.2 8.1 50S26.8 8.1 50 8.1M50 0C22.4 0 0 22.4 0 50s22.4 50 50 50 50-22.4 50-50S77.6 0 50 0zm0 11.3c-21.4 0-38.7 17.3-38.7 38.7S28.6 88.7 50 88.7 88.7 71.4 88.7 50 71.4 11.3 50 11.3zm0 8.9c4 0 7.3 3.2 7.3 7.3S54 34.7 50 34.7s-7.3-3.2-7.3-7.3 3.3-7.2 7.3-7.2zm23.7 19.7c-5.8 1.4-11.2 2.6-16.6 3.2.2 20.4 2.5 24.8 5 31.4.7 1.9-.2 4-2.1 4.7-1.9.7-4-.2-4.7-2.1-1.8-4.5-3.4-8.2-4.5-15.8h-2c-1 7.6-2.7 11.3-4.5 15.8-.7 1.9-2.8 2.8-4.7 2.1-1.9-.7-2.8-2.8-2.1-4.7 2.6-6.6 4.9-11 5-31.4-5.4-.6-10.8-1.8-16.6-3.2-1.7-.4-2.8-2.1-2.4-3.9.4-1.7 2.1-2.8 3.9-2.4 19.5 4.6 25.1 4.6 44.5 0 1.7-.4 3.5.7 3.9 2.4.7 1.8-.3 3.5-2.1 3.9z"/>',
);
];
}
if ( isset( $this->svg_icons[ $icon ] ) ) {
@ -263,10 +276,8 @@ final class Pojo_A11y_Frontend {
}
public function __construct() {
add_action( 'wp_enqueue_scripts', array( &$this, 'enqueue_scripts' ) );
add_action( 'wp_footer', array( &$this, 'print_skip_to_content_link' ), 20 );
add_action( 'wp_footer', array( &$this, 'print_toolbar' ), 30 );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'wp_footer', [ $this, 'print_skip_to_content_link' ], 20 );
add_action( 'wp_footer', [ $this, 'print_toolbar' ], 30 );
}
}

View file

@ -0,0 +1,671 @@
<?php
namespace EA11y\Modules\Legacy\Components;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Settings
*/
class Settings {
public $menu_slug = null;
const PAGE_ID = 'pojo-a11y';
const SETTINGS_PAGE = 'toplevel_page_accessibility-settings';
const TOOLBAR_PAGE = 'accessibility_page_accessibility-toolbar';
const FIELD_TEXT = 'text';
const FIELD_SELECT = 'select';
const FIELD_CHECKBOX_LIST = 'checkbox_list';
protected $_fields = [];
protected $_sections = [];
protected $_defaults = [];
protected $_pages = [];
/**
* Setup Toolbar fields
*
* @param array $sections
*
* @return array
*/
public function section_a11y_toolbar( $sections = [] ) {
$fields = [];
$fields[] = [
'id' => 'pojo_a11y_toolbar',
'title' => __( 'Display Toolbar', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'options' => [
'enable' => __( 'Show on all devices', 'pojo-accessibility' ),
'visible-desktop' => __( 'Visible Desktop', 'pojo-accessibility' ),
'visible-tablet' => __( 'Visible Tablet', 'pojo-accessibility' ),
'visible-phone' => __( 'Visible Phone', 'pojo-accessibility' ),
'hidden-desktop' => __( 'Hidden Desktop', 'pojo-accessibility' ),
'hidden-tablet' => __( 'Hidden Tablet', 'pojo-accessibility' ),
'hidden-phone' => __( 'Hidden Phone', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$toolbar_options_classes = 'pojo-a11y-toolbar-button';
$fields[] = [
'id' => 'pojo_a11y_toolbar_title',
'title' => __( 'Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'desc' => __( 'Title top of the toolbar (recommended).', 'pojo-accessibility' ),
'class' => $toolbar_options_classes,
'std' => __( 'Accessibility Tools', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_resize_font',
'title' => __( 'Resize Font', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_resize_font_add_title',
'title' => __( 'Increase Text', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row no-border',
'std' => __( 'Increase Text', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_resize_font_less_title',
'title' => __( 'Decrease Text', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Decrease Text', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_grayscale',
'title' => __( 'Grayscale', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_grayscale_title',
'title' => __( 'Grayscale Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Grayscale', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_high_contrast',
'title' => __( 'High Contrast', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_high_contrast_title',
'title' => __( 'High Contrast Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'High Contrast', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_negative_contrast',
'title' => __( 'Negative Contrast', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_negative_contrast_title',
'title' => __( 'Negative Contrast Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Negative Contrast', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_light_bg',
'title' => __( 'Light Background', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_light_bg_title',
'title' => __( 'Light Background Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Light Background', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_links_underline',
'title' => __( 'Links Underline', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_links_underline_title',
'title' => __( 'Links Underline Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Links Underline', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_readable_font',
'title' => __( 'Readable Font', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'class' => $toolbar_options_classes,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_readable_font_title',
'title' => __( 'Readable Font Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => __( 'Readable Font', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_sitemap_title',
'title' => __( 'Sitemap Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Sitemap', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_sitemap_link',
'title' => __( 'Sitemap Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/sitemap',
'desc' => __( 'Link for sitemap page in your website. Leave blank to disable.',
'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_help_title',
'title' => __( 'Help Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Help', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_help_link',
'title' => __( 'Help Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/help',
'desc' => __( 'Link for help page in your website. Leave blank to disable.', 'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_feedback_title',
'title' => __( 'Feedback Title', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'class' => $toolbar_options_classes,
'std' => __( 'Feedback', 'pojo-accessibility' ),
];
$fields[] = [
'id' => 'pojo_a11y_toolbar_button_feedback_link',
'title' => __( 'Feedback Link', 'pojo-accessibility' ),
'type' => self::FIELD_TEXT,
'placeholder' => 'http://your-domain.com/feedback',
'desc' => __( 'Link for feedback page in your website. Leave blank to disable.',
'pojo-accessibility' ),
'class' => $toolbar_options_classes . ' pojo-settings-child-row',
'std' => '',
];
$sections[] = [
'id' => 'section-a11y-toolbar',
'page' => self::TOOLBAR_PAGE,
'title' => __( 'Toolbar Settings', 'pojo-accessibility' ),
'intro' => '',
'fields' => $fields,
];
$sections[] = [
'id' => 'section-a11y-styles',
'page' => self::TOOLBAR_PAGE,
'title' => __( 'Style Settings', 'pojo-accessibility' ),
'intro' => sprintf( __( 'For style settings of accessibility tools go to > Customize > <a href="%s">Accessibility</a>.', 'pojo-accessibility' ),
$this->get_admin_url( 'customizer' )
),
'fields' => [],
];
return $sections;
}
public function section_a11y_settings( $sections ) {
$fields = [];
$fields[] = [
'id' => 'pojo_a11y_focusable',
'title' => __( 'Add Outline Focus', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Add outline to elements on keyboard focus.', 'pojo-accessibility' ),
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'disable',
];
$fields[] = [
'id' => 'pojo_a11y_skip_to_content_link',
'title' => __( 'Skip to Content link', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Add skip to content link when using keyboard.', 'pojo-accessibility' ),
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_skip_to_content_link_element_id',
'title' => __( 'Skip to Content Element ID', 'pojo-accessibility' ),
'placeholder' => 'content',
'type' => self::FIELD_TEXT,
'std' => 'content',
];
$fields[] = [
'id' => 'pojo_a11y_remove_link_target',
'title' => __( 'Remove target attribute from links', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'This option will reset all your target links to open in the same window or tab.',
'pojo-accessibility' ),
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'disable',
];
$fields[] = [
'id' => 'pojo_a11y_add_role_links',
'title' => __( 'Add landmark roles to all links', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'This option will add <code>role="link"</code> to all links on the page.',
'pojo-accessibility' ),
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_save',
'title' => __( 'Sitewide Accessibility', 'pojo-accessibility' ),
'desc' => __( 'Consistent accessibility throughout your site visit. Site remembers you and stays accessible.',
'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'options' => [
'enable' => __( 'Enable', 'pojo-accessibility' ),
'disable' => __( 'Disable', 'pojo-accessibility' ),
],
'std' => 'enable',
];
$fields[] = [
'id' => 'pojo_a11y_save_expiration',
'title' => __( 'Remember user for', 'pojo-accessibility' ),
'type' => self::FIELD_SELECT,
'desc' => __( 'Define how long your toolbar settings will be remembered', 'pojo-accessibility' ),
'options' => [
'1' => __( '1 Hour', 'pojo-accessibility' ),
'6' => __( '6 Hours', 'pojo-accessibility' ),
'12' => __( '12 Hours', 'pojo-accessibility' ),
'24' => __( '1 Day', 'pojo-accessibility' ),
'48' => __( '2 Days', 'pojo-accessibility' ),
'72' => __( '3 Days', 'pojo-accessibility' ),
'168' => __( '1 Week', 'pojo-accessibility' ),
'720' => __( '1 Month', 'pojo-accessibility' ),
],
'std' => '12',
];
$sections[] = [
'id' => 'section-a11y-settings',
'page' => self::SETTINGS_PAGE,
'title' => __( 'General Settings', 'pojo-accessibility' ),
'intro' => '',
'fields' => $fields,
];
return $sections;
}
public function print_js() {
// TODO: Maybe need to move to other file
?>
<script>
jQuery( document ).ready( function ( $ ) {
var $a11yToolbarOption = $( 'table.form-table #pojo_a11y_toolbar' ),
$a11yToolbarButtons = $( 'tr.pojo-a11y-toolbar-button' );
$a11yToolbarOption.on( 'change', function () {
if ( 'disable' !== $( this ).val() ) {
$a11yToolbarButtons.fadeIn( 'fast' );
} else {
$a11yToolbarButtons.hide();
}
} );
$a11yToolbarOption.trigger( 'change' );
} );
</script>
<?php
}
public function get_settings_sections() {
$sections = [];
$sections = $this->section_a11y_toolbar( $sections );
$sections = $this->section_a11y_settings( $sections );
$this->_sections = $sections;
return $this->_sections;
}
public function add_settings_section( $args = [] ) {
$args = wp_parse_args( $args, [
'id' => '',
'title' => '',
] );
foreach ( $this->_sections as $section ) {
if ( $args['id'] !== $section['id'] ) {
continue;
}
if ( empty( $section['intro'] ) ) {
return;
}
printf( '<p>%s</p>', $section['intro'] );
break;
}
}
public function add_settings_field( $args = [] ) {
if ( empty( $args ) ) {
return;
}
$args = wp_parse_args( $args, [
'id' => '',
'std' => '',
'type' => self::FIELD_TEXT,
] );
if ( empty( $args['id'] ) || empty( $args['type'] ) ) {
return;
}
$field_callback = 'render_' . $args['type'] . '_field';
if ( method_exists( $this, $field_callback ) ) {
call_user_func( [ $this, $field_callback ], $args );
}
}
public function render_select_field( $field ) {
$options = [];
foreach ( $field['options'] as $option_key => $option_value ) {
$options[] = sprintf(
'<option value="%1$s"%2$s>%3$s</option>',
esc_attr( $option_key ),
selected( get_option( $field['id'], $field['std'] ), $option_key, false ),
$option_value
);
}
?>
<select id="<?php echo $field['id']; ?>" name="<?php echo $field['id']; ?>">
<?php echo implode( '', $options ); ?>
</select>
<?php if ( ! empty( $field['sub_desc'] ) ) {
echo $field['sub_desc'];
} ?>
<?php if ( ! empty( $field['desc'] ) ) : ?>
<p class="description"><?php echo $field['desc']; ?></p>
<?php endif; ?>
<?php
}
public function render_text_field( $field ) {
if ( empty( $field['classes'] ) ) {
$field['classes'] = [ 'regular-text' ];
}
?>
<input type="text" class="<?php echo implode( ' ', $field['classes'] ); ?>" id="<?php echo $field['id']; ?>"
name="<?php echo $field['id']; ?>" value="<?php echo esc_attr( get_option( $field['id'],
$field['std'] ) ); ?>"<?php echo ! empty( $field['placeholder'] ) ? ' placeholder="' . $field['placeholder'] . '"' : ''; ?> />
<?php if ( ! empty( $field['sub_desc'] ) ) {
echo $field['sub_desc'];
} ?>
<?php if ( ! empty( $field['desc'] ) ) : ?>
<p class="description"><?php echo $field['desc']; ?></p>
<?php endif; ?>
<?php
}
public function admin_init() {
foreach ( $this->get_settings_sections() as $section_key => $section ) {
add_settings_section(
$section['id'],
$section['title'],
[ &$this, 'add_settings_section' ],
$section['page']
);
if ( empty( $section['fields'] ) ) {
continue;
}
foreach ( $section['fields'] as $field ) {
add_settings_field(
$field['id'],
$field['title'],
[ &$this, 'add_settings_field' ],
$section['page'],
$section['id'],
$field
);
$sanitize_callback = [ $this, 'field_html' ];
if ( ! empty( $field['type'] ) && self::FIELD_CHECKBOX_LIST === $field['type'] ) {
$sanitize_callback = [ $this, 'field_checkbox_list' ];
}
if ( ! empty( $field['sanitize_callback'] ) ) {
$sanitize_callback = $field['sanitize_callback'];
}
register_setting( $section['page'], $field['id'], $sanitize_callback );
}
}
}
public static function field_html( $input ) {
return stripslashes( wp_filter_post_kses( addslashes( $input ) ) );
}
public static function field_checkbox_list( $input ) {
if ( empty( $input ) ) {
$input = [];
}
return $input;
}
public function display_settings_page() {
$screen = get_current_screen();
$screen_id = $screen->id;
if ( false !== strpos( $screen_id, 'toolbar' ) ) {
$screen_id = self::TOOLBAR_PAGE;
}
?>
<div class="wrap">
<h2><?php echo $this->_page_title; ?></h2>
<?php settings_errors( $screen_id ); ?>
<form method="post" action="options.php">
<?php
settings_fields( $screen_id );
do_settings_sections( $screen_id );
submit_button();
?>
</form>
</div><!-- /.wrap -->
<?php
}
public function admin_menu() {
$this->menu_slug = add_menu_page(
__( 'Accessibility', 'pojo-accessibility' ),
__( 'Accessibility', 'pojo-accessibility' ) . ' <span class="awaiting-mod"></span>',
'manage_options',
'accessibility-settings',
[ &$this, 'display_settings_page' ],
'dashicons-universal-access-alt'
);
add_submenu_page(
'accessibility-settings',
__( 'Accessibility Settings', 'pojo-accessibility' ),
__( 'Settings', 'pojo-accessibility' ),
'manage_options',
'accessibility-settings',
[ &$this, 'display_settings_page' ]
);
add_submenu_page(
'accessibility-settings',
__( 'Accessibility Toolbar', 'pojo-accessibility' ),
__( 'Toolbar', 'pojo-accessibility' ),
'manage_options',
'accessibility-toolbar',
[ &$this, 'display_settings_page' ]
);
add_submenu_page(
'accessibility-settings',
__( 'Customize', 'pojo-accessibility' ),
__( 'Customize', 'pojo-accessibility' ),
'manage_options',
'/customize.php?autofocus[section]=accessibility'
);
}
public function plugin_action_links( $links, $plugin_file ) {
if ( EA11Y_BASE === $plugin_file ) {
$settings = '<a href="' . $this->get_admin_url( 'general' ) . '" aria-label="' . esc_attr__( 'Set Accessibility settings',
'pojo-accessibility' ) . '">' . __( 'Settings', 'pojo-accessibility' ) . '</a>';
$toolbar = '<a href="' . $this->get_admin_url( 'toolbar' ) . '" aria-label="' . esc_attr__( 'Set Accessibility Toolbar Settings',
'pojo-accessibility' ) . '">' . __( 'Toolbar', 'pojo-accessibility' ) . '</a>';
$customizer = '<a href="' . $this->get_admin_url( 'customizer' ) . '" aria-label="' . esc_attr__( 'Customize Toolbar',
'pojo-accessibility' ) . '" target="_blank">' . __( 'Customize', 'pojo-accessibility' ) . '</a>';
array_unshift( $links, $customizer );
array_unshift( $links, $toolbar );
array_unshift( $links, $settings );
}
return $links;
}
private function get_admin_url( $type ) {
switch ( $type ) {
case 'customizer':
return admin_url( 'customize.php?autofocus[section]=accessibility' );
break;
case 'general':
return admin_url( 'admin.php?page=accessibility-settings' );
break;
case 'toolbar':
return admin_url( 'admin.php?page=accessibility-toolbar' );
break;
}
}
private function get_default_values() {
if ( empty( $this->_defaults ) ) {
if ( empty( $this->_sections ) ) {
$this->get_settings_sections();
}
$defaults = [];
foreach ( $this->_sections as $section ) {
foreach ( $section['fields'] as $field ) {
$defaults[ $field['id'] ] = isset( $field['std'] ) ? $field['std'] : '';
}
}
$this->_defaults = $defaults;
}
}
public function get_default_title_text( $option ) {
$this->get_default_values();
$default = isset( $this->_defaults[ $option ] ) ? $this->_defaults[ $option ] : '';
return get_option( $option, $default );
}
public function __construct() {
$this->_page_title = __( 'One Click Accessibility', 'pojo-accessibility' );
$this->_page_menu_title = __( 'One Click Accessibility', 'pojo-accessibility' );
$this->_menu_parent = 'themes.php';
add_action( 'admin_menu', [ &$this, 'admin_menu' ], 20 );
add_action( 'admin_init', [ &$this, 'admin_init' ], 20 );
add_action( 'admin_footer', [ &$this, 'print_js' ] );
add_filter( 'plugin_action_links_' . EA11Y_BASE, [ $this, 'plugin_action_links' ], 10, 2 );
}
}

View file

@ -0,0 +1,649 @@
<?php
namespace EA11y\Modules\Legacy\Components;
use EA11y\Modules\Connect\Classes\Config;
use EA11y\Modules\Core\Components\Notices;
use EA11y\Modules\Core\Components\Pointers;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Admin
*/
class Upgrade {
const UPGRADED_OPTION = 'ea11y_upgraded';
const AJAX_ACTION = 'ea11y_upgrade';
const INTRODUCTION_NOTICE = 'ea11y_introduction_notice';
const CONFIRMATION_MODAL = 'ea11y_confirmation_modal';
/**
* @var true
*/
private bool $disable_introduction_modal = false;
/**
* upgrade
*/
public static function upgrade() : void {
update_option( self::UPGRADED_OPTION, true );
}
/**
* has_legacy_data
* used to check if the user has any legacy data stored
* @return bool
*/
public static function has_legacy_data() : bool {
$options = [
'pojo_a11y_focusable',
'pojo_a11y_skip_to_content_link',
'pojo_a11y_skip_to_content_link_element_id',
'pojo_a11y_remove_link_target',
'pojo_a11y_add_role_links',
'pojo_a11y_save',
'pojo_a11y_save_expiration',
'pojo_a11y_toolbar_title',
'pojo_a11y_toolbar_button_resize_font',
'pojo_a11y_toolbar_button_resize_font_add_title',
'pojo_a11y_toolbar_button_resize_font_less_title',
'pojo_a11y_toolbar_button_grayscale',
'pojo_a11y_toolbar_button_grayscale_title',
'pojo_a11y_toolbar_button_high_contrast',
'pojo_a11y_toolbar_button_high_contrast_title',
'pojo_a11y_toolbar_button_negative_contrast',
'pojo_a11y_toolbar_button_negative_contrast_title',
'pojo_a11y_toolbar_button_light_bg',
'pojo_a11y_toolbar_button_light_bg_title',
'pojo_a11y_toolbar_button_links_underline',
'pojo_a11y_toolbar_button_links_underline_title',
'pojo_a11y_toolbar_button_readable_font',
'pojo_a11y_toolbar_button_readable_font_title',
'pojo_a11y_toolbar_button_sitemap_title',
'pojo_a11y_toolbar_button_sitemap_link',
'pojo_a11y_toolbar_button_help_title',
'pojo_a11y_toolbar_button_help_link',
'pojo_a11y_toolbar_button_feedback_title',
'pojo_a11y_toolbar_button_feedback_link',
];
foreach ( $options as $option ) {
if ( get_option( $option, false ) ) {
return true;
}
}
return false;
}
/**
* is_upgraded
* @return bool
*/
public static function is_upgraded() : bool {
return (bool) get_option( self::UPGRADED_OPTION, false );
}
/**
* is_legacy_page
* @return bool
*/
public static function is_legacy_page() : bool {
return in_array( get_current_screen()->base, [ Settings::SETTINGS_PAGE, Settings::TOOLBAR_PAGE ] );
}
/**
* get_learn_more_link
*
* @param $campaign
*
* @return string
*/
public static function get_learn_more_link( $campaign ) : string {
return add_query_arg([
'utm_source' => 'acc-switch',
'utm_medium' => 'wp-dash',
'utm_campaign' => $campaign,
], 'https://go.elementor.com/acc-notice-switch-oc/' );
}
/**
* get_switch_now_link
* @return string
*/
public static function get_switch_now_link() : string {
return add_query_arg( [
'page' => 'accessibility-settings',
self::CONFIRMATION_MODAL => '1',
], admin_url() );
}
/**
* add_deprecated_notice_to_customizer_section_description
* @return string
*/
public function add_deprecated_notice_to_customizer_section_description(): string {
$content = sprintf(
'<p>%s</p><p><a href="%s">%s</a></p><p><a class="button" href="%s">%s</a></p>',
esc_html__( 'New! Switch to our updated accessibility widget for better features and control.', 'pojo-accessibility' ),
self::get_learn_more_link( 'acc-notice-switch-custom' ),
esc_html__( 'Learn More', 'pojo-accessibility' ),
self::get_switch_now_link(),
esc_html__( 'Switch Now', 'pojo-accessibility' )
);
return "<script>
jQuery( document ).ready( () => {
const api = wp.customize;
api.notifications.add( new api.Notification( 'deprecated', {
message: '" . $content . "',
type: 'info',
dismissible: false,
} ) )
} );</script>";
}
/**
* maybe_add_introduction_modal
*/
public function maybe_add_introduction_modal() {
if ( $this->disable_introduction_modal ) {
return;
}
// only show to admins on legacy pages and to admin users
if ( ! $this->is_legacy_page() || ! current_user_can( 'manage_options' ) ) {
return;
}
$dismissed = get_user_meta( get_current_user_id(), self::INTRODUCTION_NOTICE, true );
if ( $dismissed ) {
return;
}
add_thickbox();
wp_enqueue_script( 'jquery' );
add_action( 'admin_footer', function() {
?>
<div id="<?php echo esc_html( self::INTRODUCTION_NOTICE ); ?>" style="display:none;">
<div data-modal-slug="<?php echo esc_html( self::INTRODUCTION_NOTICE ); ?>" class="modal-content-wrap">
<div class="video-wrapper">
<iframe
width="480"
height="270"
src="https://www.youtube.com/embed/p-vVS5FXL_A?si=DL6RshUJot749xJ5&amp;controls=0"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen
></iframe>
</div>
<h2 class="intro-title"><?php esc_html_e( 'Switch to our new accessibility widget', 'pojo-accessibility' ); ?></h2>
<p class="intro-description typography-body1"><?php esc_html_e( 'Ally - Web Accessibility is our new and improved accessibility plugin. Get advanced customization, control, and customer support with one simple switch.', 'pojo-accessibility' ); ?></p>
<div>
<ul class="benefits typography-body1">
<li>
<?php esc_html_e( 'Personalize colors, styles, and layouts to match your brand', 'pojo-accessibility' ); ?>
</li>
<li>
<?php esc_html_e( 'Decide and manage which features you want displayed', 'pojo-accessibility' ); ?>
</li>
<li><?php esc_html_e( 'Create or add your Accessibility Statement instantly', 'pojo-accessibility' ); ?></li>
</ul>
</div>
<a class="intro-learn-more no-underline typography-body1" href="<?php echo esc_url( self::get_learn_more_link( 'acc-popup-switch-oc' ) ); ?>"><?php esc_html_e( 'Learn more about the changes', 'pojo-accessibility' ); ?></a>
<div class="footer-actions">
<a href="#" class="close-button no-underline"><?php esc_html_e( 'Not now', 'pojo-accessibility' ); ?></a>
<a href="<?php echo esc_url( self::get_switch_now_link() ); ?>" class="button button-primary switch-now"><?php esc_html_e( 'Switch to Ally', 'pojo-accessibility' ); ?></a>
</div>
</div>
</div>
<script>
const localStorageKey = '<?php echo esc_js( self::INTRODUCTION_NOTICE ); ?>';
const onLoad = ( callback ) => {
if ( document.readyState !== 'loading' ) {
callback();
} else if ( document.addEventListener ) {
document.addEventListener( 'DOMContentLoaded', () => callback() );
} else {
document.attachEvent( 'onreadystatechange', () => {
if ( document?.readyState === 'complete' ) {
callback();
}
});
}
};
onLoad(
() => setTimeout( () => {
const stored = window.localStorage.getItem( localStorageKey );
if ( stored && new Date().getTime() - parseInt( stored ) < 1000 * 60 * 60 * 24 * 7 ) {
return;
}
const $modal = document.querySelector( `[data-modal-slug="${localStorageKey}"]` );
tb_show( '', `/?TB_inline&inlineId=${localStorageKey}&height=650&width=480&modal=true` );
const closeButton = $modal.querySelector( 'a.close-button' );
const close = () => {
tb_remove();
closeButton.removeEventListener( 'click', close );
};
closeButton.addEventListener( 'click', (e) => {
e.preventDefault();
window.localStorage.setItem( localStorageKey, new Date().getTime().toString() );
close();
} );
}, 100 )
);
</script>
<style>
/* Basic components */
.typography-body1 {
font-family: Roboto, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
letter-spacing: 0.15px;
}
/* Modal CSS */
#TB_window {
border-radius: 4px;
}
.video-wrapper {
position: relative;
padding-bottom: 56.25%;
padding-top: 9px;
height: 0;
overflow: hidden;
}
.modal-content-wrap {
padding: 1px 9px;
}
.intro-title {
margin-top: 16px;
font-family: Roboto, sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 133.4%;
}
.intro-description{
padding: 0 !important;
margin-top: 24px;
margin-bottom: 24px;
color: #69727D;
}
.benefits {
margin: 24px 0;
}
.benefits li{
color: #69727D;
background-image: url(data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB2aWV3Qm94PSIwIDAgMjAgMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGlkPSJDaXJjbGVDaGVja0ZpbGxlZCI+CjxwYXRoIGlkPSJWZWN0b3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAgMS44NzVDOC45MzMwMSAxLjg3NSA3Ljg3NjQ3IDIuMDg1MTYgNi44OTA3IDIuNDkzNDhDNS45MDQ5MyAyLjkwMTggNS4wMDkyMyAzLjUwMDI4IDQuMjU0NzYgNC4yNTQ3NkMzLjUwMDI4IDUuMDA5MjMgMi45MDE4IDUuOTA0OTMgMi40OTM0OCA2Ljg5MDdDMi4wODUxNiA3Ljg3NjQ3IDEuODc1IDguOTMzMDEgMS44NzUgMTBDMS44NzUgMTEuMDY3IDIuMDg1MTYgMTIuMTIzNSAyLjQ5MzQ4IDEzLjEwOTNDMi45MDE4IDE0LjA5NTEgMy41MDAyOCAxNC45OTA4IDQuMjU0NzYgMTUuNzQ1MkM1LjAwOTIzIDE2LjQ5OTcgNS45MDQ5MyAxNy4wOTgyIDYuODkwNyAxNy41MDY1QzcuODc2NDcgMTcuOTE0OCA4LjkzMzAxIDE4LjEyNSAxMCAxOC4xMjVDMTEuMDY3IDE4LjEyNSAxMi4xMjM1IDE3LjkxNDggMTMuMTA5MyAxNy41MDY1QzE0LjA5NTEgMTcuMDk4MiAxNC45OTA4IDE2LjQ5OTcgMTUuNzQ1MiAxNS43NDUyQzE2LjQ5OTcgMTQuOTkwOCAxNy4wOTgyIDE0LjA5NTEgMTcuNTA2NSAxMy4xMDkzQzE3LjkxNDggMTIuMTIzNSAxOC4xMjUgMTEuMDY3IDE4LjEyNSAxMEMxOC4xMjUgOC45MzMwMSAxNy45MTQ4IDcuODc2NDcgMTcuNTA2NSA2Ljg5MDdDMTcuMDk4MiA1LjkwNDkzIDE2LjQ5OTcgNS4wMDkyMyAxNS43NDUyIDQuMjU0NzZDMTQuOTkwOCAzLjUwMDI4IDE0LjA5NTEgMi45MDE4IDEzLjEwOTMgMi40OTM0OEMxMi4xMjM1IDIuMDg1MTYgMTEuMDY3IDEuODc1IDEwIDEuODc1Wk0xMy41MzQ2IDguMzgwMjRDMTMuNzc4NyA4LjEzNjE2IDEzLjc3ODcgNy43NDA0MyAxMy41MzQ2IDcuNDk2MzVDMTMuMjkwNSA3LjI1MjI4IDEyLjg5NDggNy4yNTIyOCAxMi42NTA3IDcuNDk2MzVMOC45NjkyNSAxMS4xNzc4TDcuMzQ5NDkgOS41NTgwNUM3LjEwNTQyIDkuMzEzOTggNi43MDk2OSA5LjMxMzk4IDYuNDY1NjEgOS41NTgwNUM2LjIyMTUzIDkuODAyMTMgNi4yMjE1MyAxMC4xOTc5IDYuNDY1NjEgMTAuNDQxOUw4LjUyNzMxIDEyLjUwMzZDOC42NDQ1MiAxMi42MjA4IDguODAzNDkgMTIuNjg2NyA4Ljk2OTI1IDEyLjY4NjdDOS4xMzUwMSAxMi42ODY3IDkuMjkzOTggMTIuNjIwOCA5LjQxMTE5IDEyLjUwMzZMMTMuNTM0NiA4LjM4MDI0WiIgZmlsbD0iIzI1NjNFQiIvPgo8L2c+Cjwvc3ZnPgo=);
background-repeat: no-repeat;
padding: 0 0 0 25px;
margin-bottom: 8px;
}
li::marker{
font-size: 1.5em;
line-height: 0.1em;
}
.intro-learn-more{
color: #2563EB !important;
}
.intro-learn-more:hover{
color: #1D4ED8 !important;
}
.footer-actions {
display: flex;
gap: 20px;
justify-content: end;
align-items: center;
padding: 16px 0 1px 0;
margin-top: 32px;
}
.close-button {
font-family: Roboto, sans-serif;
font-size: 15px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.4px;
color: #515962;
}
.switch-now {
background: #2563EB !important;
border-radius: 4px;
font-family: Roboto, sans-serif;
font-size: 15px !important;
font-style: normal;
font-weight: 500;
letter-spacing: 0.4px;
padding: 3px 16px !important; /* Override WordPress CSS */
}
.switch-now:hover, .switch-now:active {
background: #1D4ED8 !important;
}
.no-underline {
text-decoration: none;
}
</style>
<?php
});
}
public function maybe_add_confirmation_modal() {
// only show to admins on legacy pages and to admin users
if ( ! $this->is_legacy_page() || ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! isset( $_GET[ self::CONFIRMATION_MODAL ] ) ) {
return;
}
$this->disable_introduction_modal = true;
add_thickbox();
wp_enqueue_script( 'jquery' );
add_action( 'admin_footer', function() {
?>
<div id="<?php echo esc_html( self::CONFIRMATION_MODAL ); ?>" style="display:none;">
<div data-modal-slug="<?php echo esc_html( self::CONFIRMATION_MODAL ); ?>" class="modal-content-wrap">
<div class="top-icons">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="49" viewBox="0 0 48 49" fill="none">
<path d="M21.3076 0.6497C15.8994 1.27144 10.9435 3.64535 7.06171 7.50766C3.50023 11.0497 1.1636 15.4584 0.29679 20.3192C-0.0989299 22.5424 -0.0989299 26.5743 0.29679 28.7975C2.04926 38.5757 9.92596 46.4511 19.7059 48.2033C21.9295 48.5989 25.962 48.5989 28.1856 48.2033C33.0473 47.3366 37.4567 45.0004 40.9994 41.4395C47.802 34.5815 49.8559 24.5207 46.2379 15.5337C42.2619 5.62361 31.9355 -0.556095 21.3076 0.6497ZM25.2648 12.2178C26.9419 13.0845 27.2246 15.2134 25.8301 16.5511C25.3779 16.9845 25.0575 17.1163 24.1719 17.1729C23.1355 17.2482 23.0224 17.2105 22.3817 16.6642C20.893 15.3453 21.0438 13.3106 22.6832 12.312C23.3993 11.8598 24.5111 11.8222 25.2648 12.2178ZM17.162 17.512C21.5149 17.8888 26.3577 17.8888 31.1441 17.4932C34.7809 17.1917 35.1013 17.1917 35.5347 17.4743C36.1377 17.87 36.1942 18.9062 35.6477 19.4526C35.2143 19.8859 34.9505 19.9424 31.389 20.3004C29.1089 20.5453 28.7132 20.6395 28.4306 20.9975C28.1102 21.3555 28.0914 22.0337 28.0914 28.7598C28.0914 36.0511 28.0914 36.1265 27.6768 36.5598C27.168 37.1062 26.1693 37.1439 25.6605 36.6352C25.246 36.2207 25.1706 35.7497 24.8879 32.3772C24.643 29.4192 24.5864 29.1555 24.2284 29.0236C23.6254 28.7786 23.437 29.3815 23.2108 32.3207C22.9282 35.6743 22.7586 36.541 22.2498 36.8047C21.6656 37.1062 20.893 37.012 20.422 36.5786L19.9885 36.183V28.7786C19.9885 22.0337 19.9697 21.3555 19.6682 20.9975C19.3667 20.6395 18.9521 20.5453 16.0314 20.2439C13.1106 19.9236 12.7148 19.8482 12.4133 19.4714C11.9799 18.9439 11.9799 18.0019 12.3945 17.5497C12.7714 17.1352 13.0163 17.1352 17.162 17.512Z" fill="black"/>
</svg>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="9" viewBox="0 0 37 9" fill="none">
<path d="M36.3536 4.85355C36.5488 4.65829 36.5488 4.34171 36.3536 4.14645L33.1716 0.964466C32.9763 0.769204 32.6597 0.769204 32.4645 0.964466C32.2692 1.15973 32.2692 1.47631 32.4645 1.67157L35.2929 4.5L32.4645 7.32843C32.2692 7.52369 32.2692 7.84027 32.4645 8.03553C32.6597 8.2308 32.9763 8.2308 33.1716 8.03553L36.3536 4.85355ZM0 5H36V4H0V5Z" fill="#69727D"/>
</svg>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<rect width="48" height="48" rx="24" fill="#FF7BE5"/>
<path d="M23.886 16.529a2.954 2.954 0 1 0 0-5.908 2.954 2.954 0 0 0 0 5.908Z" fill="#fff"/>
<path d="M28.223 17.35c-2.91.304-5.858.296-8.848 0l-6.585-1.035a1.717 1.717 0 0 0-1.773 1.716c0 .93.584 1.593 1.666 1.716l7.482.986-.615 15.28a1.57 1.57 0 0 0 3.137.118l.495-6.555a.79.79 0 0 1 1.579 0l.495 6.555a1.57 1.57 0 0 0 3.137-.117l-.547-13.458c0-.984.782-1.792 1.765-1.825l5.473-.984c1.09-.194 1.667-.787 1.667-1.716 0-.973-.803-1.749-1.773-1.719l-6.763 1.036.008.003Z" fill="#fff"/>
<path d="m16.889 21.437-3.132-1.53c6.826.773 7.493 1.142 10.64 1.21-3.251.178-6.101.555-7.508.318v.002Z" fill="#FF7BE5"/>
</svg>
</div>
</div>
<h2 class="confirmation-title"><?php esc_html_e( 'Confirm switching to Ally', 'pojo-accessibility' ); ?></h2>
<p class="confirmation-description"><?php esc_html_e( 'Youre about to switch from One click accessibility, which we no longer support, to Ally - Web Accessibility. Any previous settings will be removed, and this action cannot be undone.', 'pojo-accessibility' ); ?></p>
<div class="footer-actions">
<a href="#" class="close-button no-underline"><?php esc_html_e( 'Keep Current', 'pojo-accessibility' ); ?></a>
<a href="<?php echo esc_url( self::get_switch_now_link() ); ?>" class="button button-primary upgrade-now"><?php esc_html_e( 'Confirm and switch', 'pojo-accessibility' ); ?></a>
</div>
</div>
</div>
<script>
const modalID = '<?php echo esc_js( self::CONFIRMATION_MODAL ); ?>';
const onLoad = ( callback ) => {
if ( document.readyState !== 'loading' ) {
callback();
} else if ( document.addEventListener ) {
document.addEventListener( 'DOMContentLoaded', () => callback() );
} else {
document.attachEvent( 'onreadystatechange', () => {
if ( document?.readyState === 'complete' ) {
callback();
}
});
}
};
onLoad(
() => setTimeout( () => {
const modal = document.querySelector( `[data-modal-slug="${ modalID }"]` );
tb_show( '', `/?TB_inline&inlineId=${ modalID }&height=330&width=552&modal=true` );
const closeButton = modal.querySelector( 'a.close-button' );
const upgradeButton = modal.querySelector( 'a.upgrade-now' );
const close = () => {
tb_remove();
closeButton.removeEventListener( 'click', close );
};
closeButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
close();
} );
upgradeButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
fetch( ajaxurl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( {
action: '<?php echo esc_js( self::AJAX_ACTION ); ?>',
nonce: '<?php echo esc_js( wp_create_nonce( self::AJAX_ACTION ) ); ?>',
} ).toString(),
} ).then( ( response ) => {
if ( response.ok ) {
close();
location.href = '<?php echo esc_js( admin_url( 'admin.php?page=' . Config::ADMIN_PAGE ) ); ?>';
}
} );
} );
} ), 100 );
</script>
<style>
#TB_window {
border-radius: 4px;
}
.no-underline {
text-decoration: none;
}
.modal-content-wrap {
padding: 0 9px;
display: flex;
flex-direction: column;
align-items: center;
}
.confirmation-title {
font-family: Roboto, sans-serif !important;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 133.4%;
margin-block: 1px;
margin-bottom: 9px
}
.confirmation-description{
font-family: Roboto, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 500;
letter-spacing: 0.15px;
text-align: center;
color: #69727D;
}
.top-icons {
display: flex;
width: 150px;
justify-content: space-between;
align-items: center;
align-content: center;
row-gap: 1px;
flex-wrap: wrap;
margin-block: 25px;
}
.footer-actions {
display: flex;
gap: 20px;
justify-content: end;
align-items: center;
width: 100%;
margin-top: 16px;
}
.upgrade-now {
background: #2563EB !important;
border-radius: 4px;
font-family: Roboto, sans-serif;
font-size: 15px !important;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.4px;
padding: 3px 16px !important;
}
.upgrade-now:hover, .upgrade-now:active {
background: #1D4ED8 !important;
}
.close-button {
font-family: Roboto, sans-serif;
font-size: 15px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 160% */
letter-spacing: 0.4px;
color: #515962;
}
.close-button:hover, .close-button:active {
color: #1D4ED8;
}
</style>
<?php
}, 1000);
}
/**
* register_notices
*
* @param Notices $notice_manager
*/
public function register_notices( Notices $notice_manager ) {
$notices = [
'Sticky_Deprecated_Nag',
'Dismissible_Deprecated_Nag',
];
foreach ( $notices as $notice ) {
$class_name = 'EA11y\Modules\Legacy\Notices\\' . $notice;
$notice_manager->register_notice( new $class_name() );
}
}
/**
* handle_ajax_upgrade
*/
public function handle_ajax_upgrade() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
}
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), self::AJAX_ACTION ) ) {
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
}
self::upgrade();
// store upgrade campaign data
$campaign_data = [
'source' => 'one-click-upgrade',
'campaign' => 'a11y-upgrade',
'medium' => 'wp-dash',
];
set_transient( 'elementor_ea11y_campaign', $campaign_data, 30 * DAY_IN_SECONDS );
wp_send_json_success( [] );
}
/**
* maybe_render_pointer
*/
public function maybe_render_pointer() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( $this->is_legacy_page() ) {
return;
}
if ( Pointers::is_dismissed( self::AJAX_ACTION ) ) {
return;
}
wp_enqueue_script( 'wp-pointer' );
wp_enqueue_style( 'wp-pointer' );
$pointer_content = '<h3>' . esc_html__( 'Accessibility', 'pojo-accessibility' ) . '</h3>';
$pointer_content .= '<p>' . esc_html__( 'Our new, improved accessibility plugin is now available! Ally - Web Accessibility is packed with advanced styling options, improved feature controls, and so much more.', 'pojo-accessibility' ) . '</p>';
$pointer_content .= sprintf(
'<p><a class="button button-primary ea11y-pointer-settings-link" href="%s">%s</a></p>',
admin_url( 'admin.php?page=accessibility-settings' ),
esc_html__( 'View more details', 'pojo-accessibility' )
);
$allowed_tags = [
'h3' => [],
'p' => [],
'a' => [
'class' => [],
'href' => [],
],
];
?>
<script>
const onClose = () => {
return wp.ajax.post( 'ea11y_pointer_dismissed', {
data: {
pointer: '<?php echo esc_attr( self::AJAX_ACTION ); ?>',
},
nonce: '<?php echo esc_attr( wp_create_nonce( 'ea11y-pointer-dismissed' ) ); ?>',
} );
}
jQuery( document ).ready( function( $ ) {
$( '#<?php echo esc_html( Settings::SETTINGS_PAGE ); ?>' ).pointer( {
content: '<?php echo wp_kses( $pointer_content, $allowed_tags ); ?>',
pointerClass: 'ea11y-settings-pointer',
position: {
edge: <?php echo is_rtl() ? "'right'" : "'left'"; ?>,
align: 'center'
},
close: onClose
} ).pointer( 'open' );
$( '.ea11y-pointer-settings-link' ).first().on( 'click', function( e ) {
e.preventDefault();
$(this).attr( 'disabled', true );
onClose().promise().done(() => {
location = $(this).attr( 'href' );
});
})
} );
</script>
<style>
/* TODO: Update icon */
.ea11y-settings-pointer .wp-pointer-content h3::before {
content: '';
background-size: contain;
background-image: url("<?php echo esc_url( EA11Y_ASSETS_URL . 'images/logo.svg' ); ?>");
}
</style>
<?php
}
/**
* Upgrade constructor.
*/
public function __construct() {
// Upgrade Pointer
add_action( 'admin_footer', [ $this, 'maybe_render_pointer' ] );
// upgrade confirmation modal has to be before introduction modal
add_action( 'current_screen', [ $this, 'maybe_add_confirmation_modal' ], 9 );
// Introduction modal
add_action( 'current_screen', [ $this, 'maybe_add_introduction_modal' ] );
// Add customizer notification
add_filter( 'pojo_a11y_customizer_section_description', [ $this, 'add_deprecated_notice_to_customizer_section_description' ] );
// Register notices
add_action( 'ea11y_register_notices', [ $this, 'register_notices' ] );
// Upgrade
add_action( 'wp_ajax_' . self::AJAX_ACTION, [ $this, 'handle_ajax_upgrade' ] );
}
}

76
modules/legacy/module.php Normal file
View file

@ -0,0 +1,76 @@
<?php
namespace EA11y\Modules\Legacy;
use EA11y\Classes\Module_Base;
use EA11y\Modules\Legacy\Components\Customizer;
use EA11y\Modules\Legacy\Components\Settings;
use EA11y\Modules\Legacy\Components\Upgrade;
use EA11y\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends Module_Base {
public function get_name(): string {
return 'legacy';
}
public static function component_list() : array {
return [
'Frontend',
'Customizer',
'Settings',
'Admin',
'Upgrade',
];
}
public function backwards_compatibility() {
if ( false === get_option( POJO_A11Y_CUSTOMIZER_OPTIONS, false ) ) {
/**
* @var Customizer $customizer
*/
$customizer = $this->get_components()['Customizer'];
$customizer_fields = $customizer->get_customizer_fields();
$options = [];
$mods = get_theme_mods();
foreach ( $customizer_fields as $field ) {
if ( isset( $mods[ $field['id'] ] ) ) {
$options[ $field['id'] ] = $mods[ $field['id'] ];
} else {
$options[ $field['id'] ] = $field['std'];
}
}
update_option( POJO_A11Y_CUSTOMIZER_OPTIONS, $options );
}
}
public function add_elementor_support() {
$this->register_components( [ 'Elementor' ] );
}
public static function get_settings() {
/**
* @var Settings $settings
*/
return Plugin::instance()->modules_manager->get_modules( 'Legacy' )->get_component( 'Settings' );
}
public static function is_active(): bool {
if ( Upgrade::has_legacy_data() && ! Upgrade::is_upgraded() ) {
return true;
}
return false;
}
/**
* Module constructor.
*/
public function __construct() {
$this->register_components();
add_action( 'admin_init', [ $this, 'backwards_compatibility' ] );
add_action( 'elementor/init', [ $this, 'add_elementor_support' ] );
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace EA11y\Modules\Legacy\Notices;
use EA11y\Classes\Utils\Notice_Base;
use EA11y\Modules\Legacy\Components\Upgrade;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Dismissible_Deprecated_Nag
*/
class Dismissible_Deprecated_Nag extends Notice_Base {
public string $type = 'info';
public bool $is_dismissible = true;
public bool $per_user = true;
public $capability = 'manage_options';
public string $id = 'dismissible-deprecated-nag';
public function maybe_add_nag_deprecation_notice() {
if ( Upgrade::is_legacy_page() ) {
$this->conditions = false;
}
}
public function content(): string {
return sprintf( '<p>%s <a href="%s">%s</a></p><p><a class="button button-primary" href="%s">%s</a></p>',
esc_html__( 'Time to take your sites accessibility to the next level with Ally - Web Accessibility, our newest plugin packed with advanced widget customization, flexible feature controls, and a built-in statement generator. Want more details before switching?', 'pojo-accessibility' ),
esc_attr( Upgrade::get_learn_more_link( 'acc-notice-switch-dash' ) ),
esc_html__( 'Learn more', 'pojo-accessibility' ),
esc_attr( Upgrade::get_switch_now_link() ),
esc_html__( 'Switch To Ally - Web Accessibility', 'pojo-accessibility' )
);
}
public function __construct() {
add_action( 'current_screen', [ $this, 'maybe_add_nag_deprecation_notice' ] );
parent::__construct();
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace EA11y\Modules\Legacy\Notices;
use EA11y\Classes\Utils\Notice_Base;
use EA11y\Modules\Legacy\Components\Upgrade;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Class Sticky_Deprecated_Nag
*/
class Sticky_Deprecated_Nag extends Notice_Base {
public string $type = 'info';
public bool $is_dismissible = false;
public bool $per_user = true;
public $capability = 'manage_options';
public string $id = 'sticky-deprecated-nag';
public function content(): string {
return sprintf( '<h4>%s</h4><p>%s<a href="%s">%s</a></p><p><a class="button button-primary" href="%s">%s</a></p>',
__( 'New accessibility plugin available!', 'pojo-accessibility' ),
__( 'Your current plugin is no longer supported. Switch to Ally - Web Accessibility now to access more customization, control, and tools for a more inclusive site.', 'pojo-accessibility' ),
Upgrade::get_learn_more_link( 'acc-notice-switch-oc' ), // link to learn more
__( 'Learn More', 'pojo-accessibility' ),
Upgrade::get_switch_now_link(), // link to switch now
__( 'Switch to Ally', 'pojo-accessibility' )
);
}
public function maybe_add_nag_deprecation_notice() {
if ( ! Upgrade::is_legacy_page() ) {
$this->conditions = false;
}
}
public function __construct() {
add_action( 'current_screen', [ $this, 'maybe_add_nag_deprecation_notice' ] );
parent::__construct();
}
}

View file

@ -0,0 +1,52 @@
#wpbody-content {
padding-bottom: 0;
}
/* Hide wp notices */
.notice, .update-nag {
display: none;
}
body {
background: #fff;
}
.wrap {
margin-top: 0;
}
html:not([dir='rtl']) #ea11y-app {
margin-left: -20px;
background: white;
height: calc(100vh - 32px);
}
html[dir='rtl'] #ea11y-app {
margin-right: -20px;
background: white;
height: calc( 100vh - 32px );
}
#ea11y-app * {
box-sizing: border-box;
}
.ea11y-textfield .MuiInputBase-input {
height: 40px;
padding: 8px 12px;
box-shadow: none;
border-color: rgba(12, 13, 14, 0.23);
}
/* Widget Preview in Menu Settings */
#ea11y-widget-preview--container #ea11y-root {
display: flex;
justify-content: center;
}
@media (min-width: 480px) {
.ea11y-widget-container {
max-width: 100% !important;
}
}

View file

@ -0,0 +1,24 @@
import { SettingsProvider, NotificationsProvider } from '@ea11y/hooks';
import { StrictMode, Fragment, createRoot } from '@wordpress/element';
import App from './app';
import { PluginSettingsProvider } from './contexts/plugin-settings';
const rootNode = document.getElementById('ea11y-app');
// Can't use the settings hook in the global scope so accessing directly
const isDevelopment = window?.ea11ySettingsData?.isDevelopment;
const AppWrapper = Boolean(isDevelopment) ? StrictMode : Fragment;
const root = createRoot(rootNode);
root.render(
<AppWrapper>
<NotificationsProvider>
<SettingsProvider>
<PluginSettingsProvider>
<App />
</PluginSettingsProvider>
</SettingsProvider>
</NotificationsProvider>
</AppWrapper>,
);

View file

@ -0,0 +1,9 @@
class APIError extends Error {
constructor(message) {
super(message);
this.name = 'APIError';
}
}
export default APIError;

View file

@ -0,0 +1,149 @@
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import APIError from './exceptions/APIError';
const wpV2Prefix = '/wp/v2';
const v1Prefix = '/ea11y/v1';
class API {
static async request({ path, data, method = 'POST' }) {
try {
if ('GET' === method && !path.startsWith(wpV2Prefix)) {
path = addQueryArgs(path, { sb_time: new Date().getTime() });
}
const response = await apiFetch({
path,
method,
data,
});
if (path.startsWith(wpV2Prefix)) {
return response;
}
if (!response.success) {
throw new APIError(response.data.message);
}
return response.data;
} catch (e) {
if (e instanceof APIError) {
throw e;
} else {
throw new APIError(e.message);
}
}
}
static async initConnect(context = 'new') {
const data = {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
};
if ('update' === context) {
data.update_redirect_uri = true;
}
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/authorize`,
data,
});
}
static async clearSession() {
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/deactivate_and_disconnect`,
data: {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
clear_session: true,
},
});
}
static async deactivateAndDisconnect() {
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/deactivate_and_disconnect`,
data: {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
},
});
}
static async deactivate() {
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/deactivate`,
data: {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
},
});
}
static async disconnect() {
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/disconnect`,
data: {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
},
});
}
static async reconnect() {
return API.request({
method: 'POST',
path: `${v1Prefix}/connect/reconnect`,
data: {
wp_rest: window?.ea11ySettingsData?.wpRestNonce,
},
});
}
static async getSettings() {
return API.request({
method: 'GET',
path: `${wpV2Prefix}/settings`,
});
}
static async updateSettings(data) {
return API.request({
method: 'PUT',
path: `${wpV2Prefix}/settings`,
data,
});
}
static async addPage(data) {
return API.request({
method: 'POST',
path: `${wpV2Prefix}/pages`,
data,
});
}
/**
* @return {Promise<any>} {}
*/
static async getPluginSettings() {
return API.request({
method: 'GET',
path: `${v1Prefix}/settings/get-settings`,
});
}
/**
* Go to connect service
* @return {Promise<void>} redirect
*/
static async redirectToConnect() {
const link = await this.initConnect();
window.open(link, '_self').focus();
}
}
export default API;

View file

@ -0,0 +1,83 @@
import '../css/style.css';
import Box from '@elementor/ui/Box';
import DirectionProvider from '@elementor/ui/DirectionProvider';
import Grid from '@elementor/ui/Grid';
import { styled, ThemeProvider } from '@elementor/ui/styles';
import {
ConnectModal,
Notifications,
MenuItems,
PostConnectModal,
} from '@ea11y/components';
import {
useNotificationSettings,
useSettings,
useSavedSettings,
} from '@ea11y/hooks';
import { Sidebar } from '@ea11y/layouts';
import { mixpanelService } from '@ea11y/services';
import { useEffect } from '@wordpress/element';
import { usePluginSettingsContext } from './contexts/plugin-settings';
import PageContent from './page-content';
const StyledContainer = styled(Box)`
width: 100%;
display: flex;
flex-direction: column;
justify-content: start;
`;
const StyledGrid = styled(Grid)`
height: 100%;
display: flex;
flex-direction: row;
`;
const App = () => {
const { hasFinishedResolution, loading } = useSavedSettings();
const { isConnected, isRTL, closePostConnectModal } =
usePluginSettingsContext();
const { notificationMessage, notificationType } = useNotificationSettings();
const { selectedMenu } = useSettings();
useEffect(() => {
mixpanelService.init().then(() => {
mixpanelService.sendEvent('page_view', {
page: 'Button',
});
});
}, []);
const selectedParent = MenuItems[selectedMenu?.parent];
const selectedChild = selectedMenu?.child
? selectedParent?.children[selectedMenu?.child]
: null;
return (
<DirectionProvider rtl={isRTL}>
<ThemeProvider colorScheme="light">
{isConnected !== undefined && !isConnected && <ConnectModal />}
{isConnected && !closePostConnectModal && <PostConnectModal />}
<StyledGrid>
<Sidebar />
<StyledContainer>
<PageContent
// Looks the best if we have both checks
isLoading={!hasFinishedResolution || loading}
page={selectedChild ? selectedChild?.page : selectedParent?.page}
/>
</StyledContainer>
</StyledGrid>
<Notifications message={notificationMessage} type={notificationType} />
</ThemeProvider>
</DirectionProvider>
);
};
export default App;

View file

@ -0,0 +1,43 @@
import HelpIcon from '@elementor/icons/HelpIcon';
import AppBar from '@elementor/ui/AppBar';
import Link from '@elementor/ui/Link';
import Toolbar from '@elementor/ui/Toolbar';
import { styled } from '@elementor/ui/styles';
import { mixpanelService } from '@ea11y/services';
import { __ } from '@wordpress/i18n';
import { HELP_LINK } from '../../constants';
const StyledToolbar = styled(Toolbar)`
justify-content: end;
align-items: center;
flex-direction: row;
background-color: ${({ theme }) => theme.palette.background.default};
gap: 10px;
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
`;
const AdminTopBar = () => {
return (
<AppBar position="static">
<StyledToolbar variant="dense">
<Link
color="secondary"
underline="hover"
href={HELP_LINK}
target="_blank"
sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}
aria-label={__('Help', 'pojo-accessibility')}
onClick={() =>
mixpanelService.sendEvent('help_button_clicked', {
source: 'Header',
})
}
>
<HelpIcon />
</Link>
</StyledToolbar>
</AppBar>
);
};
export default AdminTopBar;

View file

@ -0,0 +1,100 @@
import Box from '@elementor/ui/Box';
import FormControl from '@elementor/ui/FormControl';
import FormControlLabel from '@elementor/ui/FormControlLabel';
import FormLabel from '@elementor/ui/FormLabel';
import Paper from '@elementor/ui/Paper';
import Radio from '@elementor/ui/Radio';
import RadioGroup from '@elementor/ui/RadioGroup';
import Tooltip from '@elementor/ui/Tooltip';
import Typography from '@elementor/ui/Typography';
import { useIconPosition } from '@ea11y/hooks';
import { mixpanelService } from '@ea11y/services';
import { __ } from '@wordpress/i18n';
const AlignmentMatrixControl = ({ mode }) => {
const { iconPosition, updateIconPosition } = useIconPosition();
const handleChange = (event) => {
updateIconPosition(mode, 'position', event.target.value);
mixpanelService.sendEvent('position_button_clicked', {
buttonData: {
mode,
value: event.target.value,
},
});
};
// Define options based on the mode
const options = [
{ value: 'top-left', label: __('Top Left', 'pojo-accessibility') },
...(mode === 'desktop'
? [{ value: 'top-center', label: __('Top Center', 'pojo-accessibility') }]
: []),
{ value: 'top-right', label: __('Top Right', 'pojo-accessibility') },
{ value: 'center-left', label: __('Center Left', 'pojo-accessibility') },
...(mode === 'desktop' ? [{ value: 'empty' }] : []),
{ value: 'center-right', label: __('Center Right', 'pojo-accessibility') },
{ value: 'bottom-left', label: __('Bottom Left', 'pojo-accessibility') },
...(mode === 'desktop'
? [
{
value: 'bottom-center',
label: __('Bottom Center', 'pojo-accessibility'),
},
]
: []),
{ value: 'bottom-right', label: __('Bottom Right', 'pojo-accessibility') },
];
return (
<FormControl>
<FormLabel id="alignment-matrix-control" color="secondary">
<Typography variant="subtitle2" marginBottom={3} color="text.primary">
{__('Default Position', 'pojo-accessibility')}
</Typography>
</FormLabel>
<Paper color="info" elevation={0}>
<Box display="flex" justifyContent="center" padding={4} width="100%">
<Paper color="secondary">
<RadioGroup
dir="ltr"
aria-labelledby="alignment-matrix-control"
value={iconPosition[mode].position}
onChange={handleChange}
name="alignment-matrix-control"
sx={{
display: 'grid',
gridTemplateColumns:
mode === 'desktop' ? 'repeat(3, 1fr)' : 'repeat(2, 1fr)',
gap: 1,
columnGap: mode === 'desktop' ? 6 : 1,
alignItems: 'center',
borderWidth: 5,
borderStyle: 'solid',
borderColor: 'secondary.main',
borderRadius: 1,
minWidth: mode === 'desktop' ? 'auto' : '100px',
}}
>
{options.map((option) =>
'empty' === option.value ? (
<Box key={option.value}></Box>
) : (
<Tooltip title={option.label} key={option.value}>
<FormControlLabel
sx={{ justifyContent: 'center', margin: 0 }}
value={option.value}
control={<Radio color="secondary" />}
/>
</Tooltip>
),
)}
</RadioGroup>
</Paper>
</Box>
</Paper>
</FormControl>
);
};
export default AlignmentMatrixControl;

View file

@ -0,0 +1,77 @@
import Box from '@elementor/ui/Box';
import Button from '@elementor/ui/Button';
import { styled } from '@elementor/ui/styles';
import { useSettings, useStorage, useToastNotification } from '@ea11y/hooks';
import { mixpanelService } from '@ea11y/services';
import { __ } from '@wordpress/i18n';
const StyledContainer = styled(Box)`
width: 100%;
display: flex;
justify-content: flex-end;
padding: ${({ theme }) => theme.spacing(2)};
border-top: 1px solid ${({ theme }) => theme.palette.divider};
`;
const BottomBar = () => {
const {
selectedMenu,
widgetMenuSettings,
iconDesign,
iconPosition,
hasChanges,
hasError,
setHasChanges,
} = useSettings();
const { save } = useStorage();
const { success, error } = useToastNotification();
const saveSettings = async () => {
let savedData = {};
if (selectedMenu.parent === 'capabilities') {
savedData = {
ea11y_widget_menu_settings: widgetMenuSettings,
};
} else if (selectedMenu.parent === 'design') {
savedData = {
ea11y_widget_icon_settings: {
style: iconDesign,
position: iconPosition,
},
};
}
try {
await save(savedData);
success(__('Settings saved!', 'pojo-accessibility'));
setHasChanges(false);
mixpanelService.sendEvent('save_button_clicked', {
savedData,
});
} catch (e) {
error(__('Failed to save settings!', 'pojo-accessibility'));
}
};
return (
<StyledContainer>
<Button
variant="contained"
color="info"
onClick={saveSettings}
disabled={
!hasChanges || Object.keys(hasError).some((key) => hasError[key])
}
>
{__('Save changes', 'pojo-accessibility')}
</Button>
</StyledContainer>
);
};
export default BottomBar;

View file

@ -0,0 +1,63 @@
import Box from '@elementor/ui/Box';
import FormControl from '@elementor/ui/FormControl';
import FormLabel from '@elementor/ui/FormLabel';
import Grid from '@elementor/ui/Grid';
import Typography from '@elementor/ui/Typography';
import { HexColorPicker, HexColorInput } from 'react-colorful';
import { useDebouncedCallback } from 'use-debounce';
import { useIconDesign } from '@ea11y/hooks';
import { mixpanelService } from '@ea11y/services';
import { __ } from '@wordpress/i18n';
import './style.css';
const ColorPicker = () => {
const { iconDesign, updateIconDesign } = useIconDesign();
const debounced = useDebouncedCallback((value) => {
mixpanelService.sendEvent('color_changed', {
color: value,
});
}, 1000);
return (
<FormControl fullWidth>
<FormLabel id="color-picker-label" color="secondary">
<Typography variant="subtitle2" marginBottom={1} color="text.primary">
{__('Color', 'pojo-accessibility')}
</Typography>
</FormLabel>
<Grid padding={1} border={1} borderColor="divider" borderRadius={1}>
<HexColorPicker
color={iconDesign.color}
onChange={(value) => {
updateIconDesign({ color: value });
debounced(value);
}}
className="widget-settings-color-picker"
/>
<Grid marginTop={2} display="flex">
<Box
padding={2}
sx={{ backgroundColor: iconDesign.color }}
borderRadius={1}
marginRight={1}
></Box>
<HexColorInput
color={iconDesign.color}
onChange={(value) => {
updateIconDesign({ color: value });
debounced(value);
}}
style={{
width: '100%',
border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '3px',
paddingLeft: '10px',
}}
/>
</Grid>
</Grid>
</FormControl>
);
};
export default ColorPicker;

Some files were not shown because too many files have changed in this diff Show more