iThemes Security (formerly Better WP Security) - Version 8.0.0

Version Description

  • Important: iThemes Security now requires WordPress 5.7 and PHP 7.0 or later.
  • New: iThemes Security gets a redesigned interface focused on making it easier to configure and find what you're looking for. Read More: https://ithemes.com/?p=65086.
  • New: Instantly search over everything in iThemes Security with a new instant search feature.
  • New: Security Tools have been grouped into their own page. "Identify Server IPs" and "Security Check Pro" can be run manually without using Debug Mode.
  • New: Relevant content from the Help Center, iThemes Blog, and iThemes YouTube channel is surfaced in a new Help area based on the current page. Click the "Help" button in the toolbar or the "Info" icon next to the page title to access it.
  • New: The settings UI is now fully responsive and works great across mobile, tablet, and desktop devices.
  • New: Two-Factor is now part of the core iThemes Security plugin.
  • Enhancement: Improved keyboard and screen reader support.
  • Enhancement: The Banned Users Card can add multiple bans at once.
  • Tweak: Add a new Global setting to control "Automatically Temporarily Authorize Hosts".
  • Tweak: When the Global setting "Hide Security Menu in Admin Bar" is enabled, notices will no longer be printed on non-iThemes Security pages. Instead, you can access the Message Center from the Settings or Dashbaord toolbars.
  • Tweak: The Database Backups module is no longer available if you have BackupBuddy installed. If this behavior isn't desired, enable the "ITSEC_ENABLE_BACKUPS" constant.
  • Tweak: The Geolocation API configuration used by Trusted Devices has been moved into it's own dedicated "Geolocation" module.
  • Tweak: Move "Have I Been Pwned" integration to the Core plugin.
  • Tweak: Reduce filename length and complexity for built CSS and JS files.
  • Removed: The following modules have been removed: 404 Detection, Away Mode, Change Content Directory, and Multisite Tweaks.
  • Removed: The following WordPress and System Tweaks have been removed: Remove Windows Live Writer Header, EditURI Header, Comment Spam, Mitigate Attachment File Traversal Attack, Protect Against Tabnapping, Filter Long URL Strings, Filter Non-English Characters, Filter Request Methods, Remove File Writing Permissions.
  • Removed: The "Backup Full Database" setting has been removed from the Backups module.
  • Removed: The "Require SSL", "Front End SSL Mode", and "SSL for Dashboard" settings have been removed from the SSL module.
  • Bug Fix: Fix fatal errors when using PHP 8.
  • Bug Fix: Fix infinite loop when restricting who can use App Passwords on multisite installs.
  • Bug Fix: Ensure the ITSEC_Setup class does not exist before trying to load it. Display schema errors on multisite in the Network Admin.
  • Bug Fix: Labels for Disable PHP Execution in Plugins and Themes were reversed.
  • Bug Fix: Add missing constants to the debug page.
  • Bug Fix: Remove deleted recipients when saving notifications.
  • Bug Fix: Correct Site Scan statuses for scans with no issues.
  • Dev Note: Modules are now based on a module.json configuration file. If you are registering custom iThemes Security module, you should update it to include a module.json file that adheres to the core/module-schema.json JSON Schema.
  • Dev Note: The Network Brute Force module had it's folder updated to "network-brute-force" from "ipcheck".
  • Dev Note: New Object Oriented API for creating Password Requirements.
  • Dev Note: New Settings and Modules REST API endpoints.
  • Dev Note: New RPC REST API namespace. There is no backward compatibility promise for these API endpoints.
Download this release

Release Info

Developer TimothyBlynJacobs
Plugin Icon 128x128 iThemes Security (formerly Better WP Security)
Version 8.0.0
Comparing to
See all releases

Code changes from version 7.9.1 to 8.0.0

Files changed (367) hide show
  1. better-wp-security.php +7 -7
  2. core/Exception/Invalid_Module.php +7 -0
  3. core/admin-pages/css/index.php +1 -2
  4. core/admin-pages/css/style.css +0 -615
  5. core/admin-pages/{js/lib → entries}/index.php +0 -0
  6. core/admin-pages/entries/settings.js +45 -0
  7. core/admin-pages/entries/settings/app.js +88 -0
  8. core/admin-pages/entries/settings/components/breadcrumbs/index.js +196 -0
  9. core/{modules/away-mode/css/images → admin-pages/entries/settings/components/breadcrumbs}/index.php +0 -0
  10. core/admin-pages/entries/settings/components/breadcrumbs/style.scss +99 -0
  11. core/admin-pages/entries/settings/components/error-renderer/index.js +48 -0
  12. core/{modules/away-mode/sync-verbs → admin-pages/entries/settings/components/error-renderer}/index.php +0 -0
  13. core/admin-pages/entries/settings/components/error-renderer/style.scss +8 -0
  14. core/admin-pages/entries/settings/components/help/index.js +67 -0
  15. core/{modules/ban-users/Module → admin-pages/entries/settings/components/help}/index.php +0 -0
  16. core/admin-pages/entries/settings/components/help/style.scss +23 -0
  17. core/admin-pages/entries/settings/components/index.js +20 -0
  18. core/{modules/core/entries/admin-notices/components/admin-bar → admin-pages/entries/settings/components}/index.php +0 -0
  19. core/admin-pages/entries/settings/components/logo/index.js +23 -0
  20. core/{modules/file-writing/js → admin-pages/entries/settings/components/logo}/index.php +0 -0
  21. core/admin-pages/entries/settings/components/main/index.js +84 -0
  22. core/{modules/global/css → admin-pages/entries/settings/components/main}/index.php +0 -0
  23. core/admin-pages/entries/settings/components/main/style.scss +141 -0
  24. core/admin-pages/entries/settings/components/navigation/index.js +146 -0
  25. core/{modules/hide-backend/css → admin-pages/entries/settings/components/navigation}/index.php +0 -0
  26. core/admin-pages/entries/settings/components/navigation/style.scss +76 -0
  27. core/admin-pages/entries/settings/components/page-header/index.js +60 -0
  28. core/{modules/malware/css → admin-pages/entries/settings/components/page-header}/index.php +0 -0
  29. core/admin-pages/entries/settings/components/page-header/style.scss +56 -0
  30. core/admin-pages/entries/settings/components/primary-form/index.js +108 -0
  31. core/{modules/malware/js → admin-pages/entries/settings/components/primary-form}/index.php +0 -0
  32. core/admin-pages/entries/settings/components/primary-form/style.scss +46 -0
  33. core/admin-pages/entries/settings/components/primary-schema-form/index.js +158 -0
  34. core/{modules/notification-center/css → admin-pages/entries/settings/components/primary-schema-form}/index.php +0 -0
  35. core/admin-pages/entries/settings/components/primary-schema-form/style.scss +115 -0
  36. core/admin-pages/entries/settings/components/selectable-card/index.js +78 -0
  37. core/{modules/password-requirements/css → admin-pages/entries/settings/components/selectable-card}/index.php +0 -0
  38. core/admin-pages/entries/settings/components/selectable-card/style.scss +130 -0
  39. core/admin-pages/entries/settings/components/sidebar/index.js +58 -0
  40. core/{modules/password-requirements/js → admin-pages/entries/settings/components/sidebar}/index.php +0 -0
  41. core/admin-pages/entries/settings/components/sidebar/style.scss +130 -0
  42. core/admin-pages/entries/settings/components/toolbar/index.js +91 -0
  43. core/{modules/pro → admin-pages/entries/settings/components/toolbar}/index.php +0 -0
  44. core/admin-pages/entries/settings/components/toolbar/style.scss +82 -0
  45. core/admin-pages/entries/settings/history.js +87 -0
  46. core/{modules/wordpress-tweaks/js/blankshield → admin-pages/entries/settings}/index.php +0 -0
  47. core/admin-pages/entries/settings/page-registration.js +261 -0
  48. core/admin-pages/entries/settings/pages/configure/index.js +514 -0
  49. core/{modules/wordpress-tweaks/js → admin-pages/entries/settings/pages/configure}/index.php +0 -0
  50. core/admin-pages/entries/settings/pages/configure/style.scss +26 -0
  51. core/admin-pages/entries/settings/pages/index.js +67 -0
  52. core/{packages/components/src/malware-scan-results → admin-pages/entries/settings/pages}/index.php +0 -0
  53. core/admin-pages/entries/settings/pages/modules/index.js +325 -0
  54. {dist/vendors/core → core/admin-pages/entries/settings/pages/modules}/index.php +0 -0
  55. core/admin-pages/entries/settings/pages/modules/style.scss +74 -0
  56. core/admin-pages/entries/settings/pages/onboard/index.js +150 -0
  57. core/admin-pages/entries/settings/pages/onboard/index.php +1 -0
  58. core/admin-pages/entries/settings/pages/onboard/style.scss +87 -0
  59. core/admin-pages/entries/settings/pages/onboard/welcome/index.js +55 -0
  60. core/admin-pages/entries/settings/pages/onboard/welcome/index.php +1 -0
  61. core/admin-pages/entries/settings/pages/onboard/welcome/style.scss +73 -0
  62. core/admin-pages/entries/settings/pages/secure-site/index.js +333 -0
  63. core/admin-pages/entries/settings/pages/secure-site/index.php +1 -0
  64. core/admin-pages/entries/settings/pages/secure-site/style.scss +41 -0
  65. core/admin-pages/entries/settings/pages/secure-site/tough-guy.svg +1 -0
  66. core/admin-pages/entries/settings/pages/settings/index.js +84 -0
  67. core/admin-pages/entries/settings/pages/settings/index.php +1 -0
  68. core/admin-pages/entries/settings/pages/settings/style.scss +36 -0
  69. core/admin-pages/entries/settings/pages/site-type/chooser/index.js +97 -0
  70. core/admin-pages/entries/settings/pages/site-type/chooser/index.php +1 -0
  71. core/admin-pages/entries/settings/pages/site-type/chooser/style.scss +22 -0
  72. core/admin-pages/entries/settings/pages/site-type/index.js +26 -0
  73. core/admin-pages/entries/settings/pages/site-type/index.php +1 -0
  74. core/admin-pages/entries/settings/pages/site-type/questions/index.js +102 -0
  75. core/admin-pages/entries/settings/pages/site-type/questions/index.php +1 -0
  76. core/admin-pages/entries/settings/pages/site-type/questions/question.js +90 -0
  77. core/admin-pages/entries/settings/pages/site-type/questions/questions/index.js +19 -0
  78. core/admin-pages/entries/settings/pages/site-type/questions/questions/index.php +1 -0
  79. core/admin-pages/entries/settings/pages/site-type/questions/questions/is-client.js +44 -0
  80. core/admin-pages/entries/settings/pages/site-type/questions/style.scss +13 -0
  81. core/admin-pages/entries/settings/pages/tools/index.js +265 -0
  82. core/admin-pages/entries/settings/pages/tools/index.php +1 -0
  83. core/admin-pages/entries/settings/pages/tools/style.scss +9 -0
  84. core/admin-pages/entries/settings/search.js +191 -0
  85. core/admin-pages/entries/settings/stores/controls.js +124 -0
  86. core/admin-pages/entries/settings/stores/index.js +2 -0
  87. core/admin-pages/entries/settings/stores/index.php +1 -0
  88. core/admin-pages/entries/settings/stores/onboard/actions.js +245 -0
  89. core/admin-pages/entries/settings/stores/onboard/index.js +26 -0
  90. core/admin-pages/entries/settings/stores/onboard/index.php +1 -0
  91. core/admin-pages/entries/settings/stores/onboard/reducers.js +126 -0
  92. core/admin-pages/entries/settings/stores/onboard/resolvers.js +50 -0
  93. core/admin-pages/entries/settings/stores/onboard/selectors.js +95 -0
  94. core/admin-pages/entries/settings/stores/tools/actions.js +71 -0
  95. core/admin-pages/entries/settings/stores/tools/index.js +25 -0
  96. core/admin-pages/entries/settings/stores/tools/index.php +1 -0
  97. core/admin-pages/entries/settings/stores/tools/reducers.js +76 -0
  98. core/admin-pages/entries/settings/stores/tools/resolvers.js +24 -0
  99. core/admin-pages/entries/settings/stores/tools/selectors.js +86 -0
  100. core/admin-pages/entries/settings/style.scss +242 -0
  101. core/admin-pages/entries/settings/utils.js +322 -0
  102. core/admin-pages/init.php +36 -41
  103. core/admin-pages/js/form-user-groups.js +0 -103
  104. core/admin-pages/js/index.php +1 -2
  105. core/admin-pages/js/lib/jquery.multiselect.css +0 -148
  106. core/admin-pages/js/lib/jquery.multiselect.js +0 -936
  107. core/admin-pages/js/settings.js +0 -994
  108. core/admin-pages/module-settings.php +0 -339
  109. core/admin-pages/page-dashboard.php +110 -0
  110. core/admin-pages/page-debug.php +3 -0
  111. core/admin-pages/page-logs.php +32 -141
  112. core/admin-pages/page-security-check.php +0 -4
  113. core/admin-pages/page-settings.php +65 -675
  114. core/admin-pages/sidebar-widget.php +17 -15
  115. core/container.php +49 -0
  116. core/core.php +140 -109
  117. core/css/index.php +0 -1
  118. core/css/ithemes.css +0 -637
  119. core/css/itsec_notice.css +0 -94
  120. core/deprecated/index.php +1 -0
  121. core/deprecated/module-settings.php +341 -0
  122. core/files.php +14 -5
  123. core/img/alert16.png +0 -0
  124. core/img/check16.png +0 -0
  125. core/img/flag16-blue.png +0 -0
  126. core/img/flag16-red.png +0 -0
  127. core/img/flag16-yellow.png +0 -0
  128. core/img/green-check16.png +0 -0
  129. core/img/index.php +1 -0
  130. core/img/mail/index.php +1 -1
  131. core/img/return-to-top.png +0 -0
  132. core/js/admin-dashboard-footer.js +0 -3
  133. core/js/admin-dashboard.js +0 -187
  134. core/js/admin-global-settings.js +0 -19
  135. core/js/admin-logs.js +0 -22
  136. core/js/admin-modal.js +0 -75
  137. core/js/index.php +0 -1
  138. core/js/itsec-notice.js +0 -16
  139. core/js/scrollTo.js +0 -7
  140. core/js/tracking.js +0 -154
  141. core/lib.php +178 -12
  142. core/lib/Config_Password_Requirement.php +105 -0
  143. core/lib/Config_Settings.php +85 -0
  144. core/lib/Config_Validator.php +81 -0
  145. core/lib/Legacy_Password_Requirement.php +131 -0
  146. core/lib/Module_Config.php +422 -0
  147. core/lib/Password_Requirement.php +113 -0
  148. core/lib/Result.php +108 -0
  149. core/lib/ban-hosts/REST.php +16 -2
  150. core/lib/class-itsec-lib-feature-flags.php +1 -1
  151. core/lib/class-itsec-lib-ip-detector.php +4 -0
  152. core/lib/class-itsec-lib-password-requirements.php +33 -16
  153. core/lib/class-itsec-lib-rest.php +38 -8
  154. core/lib/class-itsec-lib-user-activity.php +1 -1
  155. core/lib/class-itsec-scheduler.php +52 -3
  156. core/lib/form.php +2 -2
  157. core/lib/index.php +1 -1
  158. core/lib/rest/Modules_Controller.php +400 -0
  159. core/lib/rest/Settings_Controller.php +242 -0
  160. core/lib/rest/Site_Types_Controller.php +326 -0
  161. core/lib/rest/Tools_Controller.php +266 -0
  162. core/lib/rest/index.php +1 -0
  163. core/lib/schema.php +11 -0
  164. core/lib/settings.php +197 -13
  165. core/lib/site-types/Answer_Details.php +56 -0
  166. core/lib/site-types/Answer_Handler.php +210 -0
  167. core/lib/site-types/Answered_Question.php +98 -0
  168. core/lib/site-types/Controller.php +142 -0
  169. core/lib/site-types/Defaults.php +155 -0
  170. core/lib/site-types/Has_End_Users.php +13 -0
  171. core/lib/site-types/Has_Prerequisites.php +15 -0
  172. core/lib/site-types/Question.php +42 -0
  173. core/lib/site-types/Question/Client_Question_Pack.php +210 -0
  174. core/lib/site-types/Question/End_Users_Question_Pack.php +72 -0
  175. core/lib/site-types/Question/Login_Security_Question_Pack.php +167 -0
  176. core/lib/site-types/Question/index.php +1 -0
  177. core/lib/site-types/Questions_Provider.php +13 -0
  178. core/lib/site-types/Registry.php +48 -0
  179. core/lib/site-types/Responds.php +16 -0
  180. core/lib/site-types/Site_Type.php +47 -0
  181. core/lib/site-types/Templated_Question.php +38 -0
  182. core/lib/site-types/Templating_Site_Type.php +11 -0
  183. core/lib/site-types/Templating_Site_Type_Adapter.php +48 -0
  184. core/lib/site-types/Type/Blog.php +32 -0
  185. core/lib/site-types/Type/Brochure.php +32 -0
  186. core/lib/site-types/Type/Ecommerce.php +75 -0
  187. core/lib/site-types/Type/Network.php +75 -0
  188. core/lib/site-types/Type/Non_Profit.php +74 -0
  189. core/lib/site-types/Type/Portfolio.php +32 -0
  190. core/lib/site-types/Type/index.php +1 -0
  191. core/lib/site-types/index.php +1 -0
  192. core/lib/storage.php +8 -0
  193. core/lib/tools/Config_Tool.php +75 -0
  194. core/lib/tools/Tool.php +93 -0
  195. core/lib/tools/Tools_Registry.php +53 -0
  196. core/lib/tools/Tools_Runner.php +198 -0
  197. core/lib/tools/index.php +1 -0
  198. core/lib/validator.php +131 -29
  199. core/lockout.php +21 -50
  200. core/module-schema.json +454 -0
  201. core/modules.php +281 -38
  202. core/modules/404-detection/active.php +0 -5
  203. core/modules/404-detection/class-itsec-four-oh-four.php +0 -66
  204. core/modules/404-detection/index.php +0 -1
  205. core/modules/404-detection/js/admin-four-oh-four.js +0 -22
  206. core/modules/404-detection/js/index.php +0 -1
  207. core/modules/404-detection/labels.php +0 -5
  208. core/modules/404-detection/logs.php +0 -33
  209. core/modules/404-detection/settings-page.php +0 -64
  210. core/modules/404-detection/settings.php +0 -34
  211. core/modules/404-detection/setup.php +0 -104
  212. core/modules/404-detection/validator.php +0 -33
  213. core/modules/admin-user/active.php +109 -70
  214. core/modules/admin-user/index.php +1 -1
  215. core/modules/admin-user/js/admin-admin-user.js +0 -15
  216. core/modules/admin-user/js/index.php +0 -1
  217. core/modules/admin-user/module.json +35 -0
  218. core/modules/admin-user/settings-page.php +0 -57
  219. core/modules/admin-user/settings.php +0 -13
  220. core/modules/admin-user/validator.php +0 -53
  221. core/modules/away-mode/activate.php +0 -5
  222. core/modules/away-mode/active.php +0 -5
  223. core/modules/away-mode/class-itsec-away-mode.php +0 -139
  224. core/modules/away-mode/css/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  225. core/modules/away-mode/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  226. core/modules/away-mode/css/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  227. core/modules/away-mode/css/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  228. core/modules/away-mode/css/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  229. core/modules/away-mode/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  230. core/modules/away-mode/css/images/ui-icons_222222_256x240.png +0 -0
  231. core/modules/away-mode/css/images/ui-icons_2e83ff_256x240.png +0 -0
  232. core/modules/away-mode/css/images/ui-icons_454545_256x240.png +0 -0
  233. core/modules/away-mode/css/images/ui-icons_888888_256x240.png +0 -0
  234. core/modules/away-mode/css/images/ui-icons_cd0a0a_256x240.png +0 -0
  235. core/modules/away-mode/css/index.php +0 -1
  236. core/modules/away-mode/css/jquery-ui.min.css +0 -7
  237. core/modules/away-mode/css/jquery.datepicker.css +0 -210
  238. core/modules/away-mode/deactivate.php +0 -5
  239. core/modules/away-mode/index.php +0 -1
  240. core/modules/away-mode/js/admin-away-mode.js +0 -33
  241. core/modules/away-mode/js/index.php +0 -1
  242. core/modules/away-mode/js/settings-page.js +0 -25
  243. core/modules/away-mode/labels.php +0 -5
  244. core/modules/away-mode/logs.php +0 -18
  245. core/modules/away-mode/settings-page.php +0 -176
  246. core/modules/away-mode/settings.php +0 -27
  247. core/modules/away-mode/setup.php +0 -154
  248. core/modules/away-mode/sync-verbs/itsec-get-away-mode.php +0 -28
  249. core/modules/away-mode/sync-verbs/itsec-override-away-mode.php +0 -96
  250. core/modules/away-mode/utilities.php +0 -164
  251. core/modules/away-mode/validator.php +0 -135
  252. core/modules/backup/activate.php +0 -8
  253. core/modules/backup/cards/class-itsec-dashboard-card-database-backup.php +171 -0
  254. core/modules/backup/cards/index.php +1 -0
  255. core/modules/backup/class-itsec-backup.php +28 -45
  256. core/modules/backup/container.php +13 -0
  257. core/modules/backup/css/index.php +0 -1
  258. core/modules/backup/css/multi-select.css +0 -94
  259. core/modules/backup/css/settings-page.css +0 -110
  260. core/modules/backup/entries/dashboard.js +29 -0
  261. core/modules/backup/entries/dashboard/index.js +176 -0
  262. core/modules/backup/entries/dashboard/index.php +1 -0
  263. core/modules/backup/entries/dashboard/style.scss +176 -0
  264. core/modules/backup/entries/index.php +1 -0
  265. core/modules/backup/img/index.php +0 -1
  266. core/modules/backup/img/switch.png +0 -0
  267. core/modules/backup/index.php +1 -1
  268. core/modules/backup/js/index.php +0 -1
  269. core/modules/backup/js/jquery.multi-select.js +0 -552
  270. core/modules/backup/js/settings-page.js +0 -58
  271. core/modules/backup/module.json +198 -0
  272. core/modules/backup/settings-page.php +0 -198
  273. core/modules/backup/settings.php +56 -28
  274. core/modules/backup/setup.php +70 -87
  275. core/modules/backup/validator.php +0 -32
  276. core/modules/ban-users/Database_Repository.php +1 -1
  277. core/modules/ban-users/Module/Validator.php +0 -154
  278. core/modules/ban-users/REST.php +119 -0
  279. core/modules/ban-users/container.php +30 -5
  280. core/modules/ban-users/index.php +1 -1
  281. core/modules/ban-users/js/admin-ban_users.js +0 -17
  282. core/modules/ban-users/js/index.php +0 -1
  283. core/modules/ban-users/module.json +90 -0
  284. core/modules/ban-users/settings-page.php +0 -89
  285. core/modules/ban-users/settings.php +12 -13
  286. core/modules/brute-force/index.php +1 -1
  287. core/modules/brute-force/js/admin-brute-force.js +0 -17
  288. core/modules/brute-force/js/index.php +0 -1
  289. core/modules/brute-force/module.json +52 -0
  290. core/modules/brute-force/settings-page.php +0 -66
  291. core/modules/brute-force/settings.php +0 -18
  292. core/modules/brute-force/setup.php +42 -78
  293. core/modules/brute-force/validator.php +0 -17
  294. core/modules/content-directory/index.php +1 -1
  295. core/modules/content-directory/module.json +8 -0
  296. core/modules/content-directory/settings-page.php +0 -125
  297. core/modules/core/class-itsec-admin-notices.php +0 -8
  298. core/modules/core/class-itsec-core-active.php +48 -14
  299. core/modules/core/class-itsec-core-admin.php +0 -18
  300. core/modules/core/class-itsec-rest-actor-types-controller.php +1 -1
  301. core/modules/core/class-itsec-rest-actors-controller.php +1 -1
  302. core/modules/core/entries/admin-notices-dashboard-admin-bar.js +10 -2
  303. core/modules/core/entries/admin-notices.js +3 -1
  304. core/modules/core/entries/admin-notices/components/admin-bar/index.js +0 -77
  305. core/modules/core/entries/admin-notices/components/admin-bar/style.scss +0 -56
  306. core/modules/core/entries/admin-notices/components/notice-actions/index.js +39 -16
  307. core/modules/core/entries/admin-notices/components/notice-actions/style.scss +3 -5
  308. core/modules/core/entries/admin-notices/components/notice-list/index.js +4 -1
  309. core/modules/core/entries/admin-notices/components/notice/index.js +45 -28
  310. core/modules/core/entries/admin-notices/components/notice/style.scss +88 -69
  311. core/modules/core/entries/admin-notices/components/panel/index.js +72 -27
  312. core/modules/core/entries/admin-notices/components/panel/style.scss +22 -7
  313. core/modules/core/entries/admin-notices/components/toolbar-button/index.js +66 -0
  314. core/modules/core/entries/admin-notices/components/toolbar-button/index.php +1 -0
  315. core/modules/core/entries/admin-notices/components/toolbar-button/style.scss +39 -0
  316. core/modules/core/entries/admin-notices/components/toolbar/index.js +17 -29
  317. core/modules/core/entries/admin-notices/components/toolbar/style.scss +0 -1
  318. core/modules/core/entries/admin-notices/store/controls.js +17 -4
  319. core/modules/core/entries/admin-notices/store/reducers.js +12 -6
  320. core/modules/core/entries/admin-notices/store/resolvers.js +14 -4
  321. core/modules/core/entries/admin-notices/store/selectors.js +10 -2
  322. core/modules/core/entries/admin-notices/utils.js +0 -19
  323. core/modules/core/entries/settings.js +21 -0
  324. core/modules/core/module.json +20 -0
  325. core/modules/core/notices.php +10 -8
  326. core/modules/core/sidebar-widget-backupbuddy-cross-promo.php +0 -20
  327. core/modules/core/sidebar-widget-mail-list-signup.php +0 -61
  328. core/modules/core/sidebar-widget-pro-upsell.php +0 -25
  329. core/modules/core/sidebar-widget-support.php +0 -20
  330. core/modules/core/sidebar-widget-sync-cross-promo.php +0 -27
  331. core/modules/dashboard/active.php +5 -0
  332. core/modules/dashboard/cards/abstract-class-itsec-dashboard-card.php +92 -0
  333. core/modules/dashboard/cards/class-itsec-dashboard-card-active-lockouts.php +316 -0
  334. core/modules/dashboard/cards/class-itsec-dashboard-card-banned-users.php +26 -0
  335. core/modules/dashboard/cards/class-itsec-dashboard-card-line-graph.php +126 -0
  336. core/modules/dashboard/cards/class-itsec-dashboard-card-pie-chart.php +153 -0
  337. core/modules/dashboard/cards/index.php +1 -0
  338. core/modules/dashboard/class-itsec-dashboard-rest.php +162 -0
  339. core/modules/dashboard/class-itsec-dashboard-util.php +917 -0
  340. core/modules/dashboard/class-itsec-dashboard.php +403 -0
  341. core/modules/dashboard/container.php +48 -0
  342. core/modules/dashboard/deactivate.php +3 -0
  343. core/modules/dashboard/entries/api.js +35 -0
  344. core/modules/dashboard/entries/dashboard.js +40 -0
  345. core/modules/dashboard/entries/dashboard/app.js +105 -0
  346. core/modules/dashboard/entries/dashboard/cards/active-lockouts/Detail.js +152 -0
  347. core/modules/dashboard/entries/dashboard/cards/active-lockouts/index.js +189 -0
  348. core/modules/dashboard/entries/dashboard/cards/active-lockouts/index.php +1 -0
  349. core/modules/dashboard/entries/dashboard/cards/active-lockouts/lockout-controller.js +86 -0
  350. core/modules/dashboard/entries/dashboard/cards/active-lockouts/style.scss +128 -0
  351. core/modules/dashboard/entries/dashboard/cards/banned-users-list/add-new.js +106 -0
  352. core/modules/dashboard/entries/dashboard/cards/banned-users-list/index.js +143 -0
  353. core/modules/dashboard/entries/dashboard/cards/banned-users-list/index.php +1 -0
  354. core/modules/dashboard/entries/dashboard/cards/banned-users-list/list.js +176 -0
  355. core/modules/dashboard/entries/dashboard/cards/banned-users-list/search.js +111 -0
  356. core/modules/dashboard/entries/dashboard/cards/banned-users-list/style.scss +257 -0
  357. core/modules/dashboard/entries/dashboard/cards/index.js +114 -0
  358. core/modules/dashboard/entries/dashboard/cards/index.php +1 -0
  359. core/modules/dashboard/entries/dashboard/cards/renderers/index.js +2 -0
  360. core/modules/dashboard/entries/dashboard/cards/renderers/index.php +1 -0
  361. core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/index.js +105 -0
  362. core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/index.php +1 -0
  363. core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/style.scss +14 -0
  364. core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/index.js +155 -0
  365. core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/index.php +1 -0
  366. core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/style.scss +49 -0
  367. core/modules/dashboard/entries/dashboard/components/add-card/index.js +31 -0
better-wp-security.php CHANGED
@@ -6,17 +6,17 @@
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
- * Version: 7.9.1
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
13
- * Requires PHP: 5.6
14
- * Requires at least: 5.4
15
  */
16
 
17
- if ( version_compare( phpversion(), '5.6.0', '<' ) ) {
18
  function itsec_free_minimum_php_version_notice() {
19
- echo '<div class="notice notice-error"><p>' . esc_html__( 'iThemes Security requires PHP 5.6 or higher.', 'better-wp-security' ) . '</p></div>';
20
  }
21
 
22
  add_action( 'admin_notices', 'itsec_free_minimum_php_version_notice' );
@@ -24,9 +24,9 @@ if ( version_compare( phpversion(), '5.6.0', '<' ) ) {
24
  return;
25
  }
26
 
27
- if ( version_compare( $GLOBALS['wp_version'], '5.4.0', '<' ) ) {
28
  function itsec_minimum_wp_version_notice() {
29
- echo '<div class="notice notice-error"><p>' . esc_html__( 'iThemes Security Pro requires WordPress 5.4 or later.', 'better-wp-security' ) . '</p></div>';
30
  }
31
 
32
  add_action( 'admin_notices', 'itsec_minimum_wp_version_notice' );
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
+ * Version: 8.0.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
13
+ * Requires PHP: 7.0
14
+ * Requires at least: 5.7
15
  */
16
 
17
+ if ( version_compare( phpversion(), '7.0.0', '<' ) ) {
18
  function itsec_free_minimum_php_version_notice() {
19
+ echo '<div class="notice notice-error"><p>' . esc_html__( 'iThemes Security requires PHP 7.0 or higher.', 'better-wp-security' ) . '</p></div>';
20
  }
21
 
22
  add_action( 'admin_notices', 'itsec_free_minimum_php_version_notice' );
24
  return;
25
  }
26
 
27
+ if ( version_compare( $GLOBALS['wp_version'], '5.7.0', '<' ) ) {
28
  function itsec_minimum_wp_version_notice() {
29
+ echo '<div class="notice notice-error"><p>' . esc_html__( 'iThemes Security Pro requires WordPress 5.7 or later.', 'better-wp-security' ) . '</p></div>';
30
  }
31
 
32
  add_action( 'admin_notices', 'itsec_minimum_wp_version_notice' );
core/Exception/Invalid_Module.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Exception;
4
+
5
+ class Invalid_Module extends \Exception implements Exception {
6
+
7
+ }
core/admin-pages/css/index.php CHANGED
@@ -1,2 +1 @@
1
- <?php
2
- // Silence is golden.
1
+ <?php // Silence is golden.
 
core/admin-pages/css/style.css CHANGED
@@ -1,25 +1,6 @@
1
  /*
2
  * Override Core styling
3
  */
4
- #poststuff #post-body.columns-2 {
5
- margin-right: 320px;
6
- }
7
- #post-body.columns-2 #postbox-container-1,
8
- #post-body.columns-2 #postbox-container-2 {
9
- border-top: 1px solid #ddd;
10
- padding-top: 20px;
11
- }
12
- #post-body.columns-2 #postbox-container-1 {
13
- box-sizing: border-box;
14
- margin-right: -300px;
15
- padding-left: 20px;
16
- width: 300px;
17
- }
18
- #post-body.columns-2 #postbox-container-2 {
19
- border-right: 1px solid #ddd;
20
- box-sizing: border-box;
21
- padding-right: 20px;
22
- }
23
 
24
  .wrap h1 img,
25
  .wrap h1 a {
@@ -56,57 +37,6 @@
56
  display: none;
57
  }
58
 
59
-
60
- /*
61
- * Module settings container
62
- */
63
-
64
- /* List Styling */
65
-
66
- .list .itsec-module-settings-container {
67
- clear: both;
68
- background: #fff;
69
- padding: 2em 2em 4em 2em;
70
- -moz-box-shadow: inset 0 8px 8px -8px #ddd,
71
- inset 0 -8px 8px -8px #ddd;
72
- -webkit-box-shadow: inset 0 8px 8px -8px #ddd,
73
- inset 0 -8px 8px -8px #ddd;
74
- box-shadow: inset 0 8px 8px -8px #ddd,
75
- inset 0 -8px 8px -8px #ddd;
76
- }
77
- .list .itsec-modal-navigation,
78
- .list .itsec-modal-content-footer {
79
- display: none;
80
- }
81
- .list .itsec-modal-header {
82
- display: none;
83
- }
84
-
85
- .itsec-pro-label {
86
- background: #ffcd08;
87
- box-shadow: 1px 1px 1px rgba(0,0,0,0.2);
88
- font-size: 13px;
89
- color: #5e5018;
90
- text-transform: uppercase;
91
- }
92
- .list .itsec-pro-label {
93
- display: inline;
94
- float: left;
95
- font-size: 10px;
96
- margin: .35em 0 0 1em;
97
- padding: 1px 3px;
98
- position: relative;
99
- }
100
- .grid .itsec-pro-label {
101
- padding: 3px 6px;
102
- position: absolute;
103
- right: -4px;
104
- top: 5px;
105
- }
106
-
107
-
108
-
109
-
110
  /* Grid Styling */
111
 
112
  .itsec-modal-background {
@@ -157,38 +87,6 @@
157
  min-height: 14em;
158
  }
159
 
160
- /* Override max width for user security check */
161
- #itsec-module-card-user-security-check .itsec-module-settings-content {
162
- max-width: 100%;
163
- position: relative;
164
- }
165
- @media screen and (max-width: 782px) {
166
- #itsec-module-card-user-security-check p.search-box {
167
- width: 100%;
168
- }
169
- #itsec-user-table {
170
- padding-bottom: 100px;
171
- }
172
- }
173
-
174
- /* Correct font size of user table nav in user security check */
175
- #itsec-module-card-user-security-check .tablenav {
176
- font-size: 12px;
177
- }
178
- .itsec-module-pro-upsell {
179
- opacity: .5;
180
- }
181
-
182
- .itsec-pro-upsell {
183
- display: block;
184
- height: 100%;
185
- left: 0;
186
- position: absolute;
187
- top: 0;
188
- width: 100%;
189
- text-decoration: none;
190
- }
191
-
192
  @media ( max-width: 960px ) {
193
  .grid .itsec-module-settings-container {
194
  margin-left: 36px;
@@ -208,8 +106,6 @@
208
  }
209
  }
210
 
211
-
212
-
213
  /*
214
  * Grid Settings Modal
215
  */
@@ -354,127 +250,13 @@ body.itsec-modal-open {
354
  content: '';
355
  width: 100%;
356
  }
357
- .itsec-module-card,
358
- .itsec-module-card-filler {
359
- font-size: 16px;
360
- width: 100%;
361
- max-width: 32%;
362
- text-align: left;
363
- margin-bottom: 2%;
364
- vertical-align: top;
365
- }
366
 
367
- .itsec-module-card-content {
368
- font-size: 16px;
369
- position: relative;
370
- background: #fff;
371
- border-left: 3px solid #afafaf;
372
- box-shadow: 1px 1px 0px rgba(0,0,0,.1);
373
- padding: 1em;
374
- }
375
- #poststuff .itsec-module-card h2 {
376
- font-size: 16px;
377
- margin: 0;
378
- padding: 0;
379
- }
380
- .itsec-module-card-content .module-description {
381
- display: block;
382
- }
383
- .itsec-module-type-enabled .itsec-module-card-content {
384
- background: #f7fcfe;
385
- border-left-color: #00a0d2;
386
- box-shadow: 1px 1px 0px rgba(0,0,0,.1);
387
- }
388
- .itsec-module-type-enabled.itsec-module-status--warning .itsec-module-card-content {
389
- background: #fff8e5;
390
- border-left-color: #ffb900;
391
- }
392
- .itsec-module-card-content .module-actions {
393
- float: none;
394
- margin: 1em 0 0 0;
395
- }
396
- .itsec-module-type-disabled .itsec-settings-module-settings,
397
- .itsec-module-type-disabled .itsec-module-settings-save,
398
- .itsec-module-type-disabled .itsec-list-content-footer {
399
- display: none;
400
- }
401
- .itsec-module-type-disabled .itsec-module-settings-container {
402
- padding: 2em;
403
- }
404
  .itsec-module-settings-content pre{
405
  background: #f1f1f1;
406
  overflow:scroll;
407
  padding: 1em;
408
  }
409
 
410
- /*
411
- * List View
412
- */
413
- .list .itsec-module-card {
414
- max-width: 100%;
415
- padding: 0;
416
- float: none;
417
- margin-bottom: 1px;
418
- }
419
- .list .itsec-module-card-content {
420
- cursor: pointer;
421
- padding: .5em .5em .5em .75em;
422
- }
423
- .list .itsec-module-card-content:after {
424
- content: "";
425
- display: table;
426
- clear: both;
427
- }
428
- .list .itsec-module-card:hover .itsec-module-card-content {
429
- background: #fafafa;
430
- }
431
- .list .itsec-module-card.enabled:hover .itsec-module-card-content {
432
- background: #ebf7fc;
433
- }
434
- .list .module-description,
435
- .list .itsec-module-card-filler {
436
- display: none;
437
- }
438
- #poststuff .list .itsec-module-card h2 {
439
- float: left;
440
- line-height: 1.8;
441
- font-size: 14px;
442
- }
443
- .list .itsec-module-card-content .module-actions {
444
- float: right;
445
- margin-top: 0;
446
- }
447
- .list textarea {
448
- max-width: 100%;
449
- width: 500px;
450
- min-height: 14em;
451
- }
452
-
453
- .list .itsec-list-content-footer {
454
- display: flex;
455
- align-items: center;
456
- justify-content: space-between;
457
- }
458
-
459
- @media ( max-width: 782px ) {
460
- #poststuff .list .itsec-module-card h2 {
461
- line-height: 2.6;
462
- }
463
- }
464
-
465
- @media ( max-width: 600px ) {
466
- #poststuff .list .itsec-module-card h2 {
467
- float: none;
468
- }
469
- .list .itsec-module-card-content .module-actions {
470
- float:none;
471
- }
472
- }
473
-
474
- .itsec-module-documentation {
475
- font-size: 13px;
476
- }
477
-
478
  /*
479
  * Success Error Messages
480
  */
@@ -575,201 +357,6 @@ body.itsec-modal-open {
575
  background-color: #FFCD08;
576
  }
577
 
578
-
579
- /* Extra styling */
580
- .current-date-time {
581
- display: inline-block;
582
- font-size: 1.25em;
583
- background: #EFEFEF;
584
- padding: .75em 1em;
585
- }
586
-
587
- .itsec-module-cards textarea.textarea-small {
588
- height: 6em;
589
- min-height: 6em;
590
- width: 25em;
591
- max-width: 100%;
592
- }
593
-
594
- .itsec-warning-message {
595
- display: block;
596
- clear: both;
597
- font-size: 1.125em;
598
- margin: 1.5em 0;
599
- padding: 1em;
600
- border-left: 4px solid #ca8383;
601
- background: #ffd4d4;
602
- color: #9c3e3e;
603
- }
604
-
605
- .itsec-notice-message {
606
- display: block;
607
- clear: both;
608
- font-size: 1.125em;
609
- margin: 1.5em 0;
610
- padding: 1em;
611
- border-left: 4px solid #e8e4a7;
612
- background: #fff9ec;
613
- }
614
-
615
- .notice.itsec-is-dismissible {
616
- padding-right: 38px;
617
- position: relative;
618
- }
619
-
620
- .itsec-notification-center-link {
621
- float: right;
622
- text-decoration: none;
623
- font-style: italic;
624
- font-weight: normal;
625
- font-size: .8rem;
626
- }
627
-
628
- .itsec-notification-center-link::after {
629
- content: "\f345";
630
- font-family: Dashicons;
631
- font-size: .8rem;
632
- vertical-align: sub;
633
- }
634
- /*
635
- * Security Check
636
- */
637
-
638
- .itsec-security-check-list {
639
- font-size: 13px;
640
- margin: 0 0 2em 1em;
641
- }
642
-
643
- .itsec-security-check-list p {
644
- margin: 0;
645
- }
646
-
647
- .itsec-security-check-container {
648
- padding: 0 10px 0 50px;
649
- border: 1px solid #ddd;
650
- border-left-width: 4px;
651
- margin: 0 0 8px;
652
- position: relative;
653
- }
654
-
655
- .itsec-security-check-container::before {
656
- font-family: 'Dashicons';
657
- font-size: 1.75em;
658
- position: absolute;
659
- left: 7px;
660
- top: 50%;
661
- margin: -9px 0 0 0;
662
- }
663
-
664
- .itsec-security-check-container-action-taken::before,
665
- .itsec-security-check-container-complete::before,
666
- .itsec-security-check-container-confirmation::before {
667
- content: "\f147";
668
- color: #46b450;
669
- }
670
-
671
- .itsec-security-check-container-incomplete::before,
672
- .itsec-security-check-container-call-to-action::before {
673
- content: "\f534";
674
- color: #f2dd28;
675
- }
676
-
677
- .itsec-security-check-container-error::before {
678
- content: "\f534";
679
- color: #dc3232;
680
- }
681
-
682
- .itsec-security-check-container-action-taken,
683
- .itsec-security-check-container-complete,
684
- .itsec-security-check-container-confirmation {
685
- border-left-color: #46b450;
686
- }
687
-
688
- .itsec-security-check-container-incomplete,
689
- .itsec-security-check-container-call-to-action {
690
- border-left-color: #f2dd28;
691
- }
692
-
693
- .itsec-security-check-container-error {
694
- border-left-color: #dc3232;
695
- }
696
-
697
- /**
698
- * User Security Check
699
- */
700
- .itsec-two-factor .dashicons {
701
- cursor: default;
702
- }
703
- .itsec-two-factor .dashicons.not-configured {
704
- color: #F56E28;
705
- }
706
- .itsec-two-factor .dashicons.dashicons-unlock {
707
- color: #dc3232;
708
- }
709
- .itsec-password-age {
710
- display:inline-block;
711
- }
712
- .itsec-password-strength {
713
- background: #ddd;
714
- border: 1px solid #aaa;
715
- border-radius: 3px;
716
- color: #23282d;
717
- display: inline-block;
718
- font-size: 11px;
719
- line-height: 1.5;
720
- padding: 0px 4px;
721
- }
722
- .itsec-password-strength.short {
723
- background: #f1adad;
724
- border-color: #e35b5b;
725
- }
726
- .itsec-password-strength.bad {
727
- background-color: #fbc5a9;
728
- border-color: #f78b53;
729
- }
730
- .itsec-password-strength.good {
731
- background-color: #ffe399;
732
- border-color: #ffc733;
733
- }
734
- .itsec-password-strength.strong {
735
- background-color: #c1e1b9;
736
- border-color: #83c373;
737
- }
738
-
739
- /**
740
- * Styling for output created by ITSEC_Settings_Page::show_details_toggle()
741
- */
742
- .itsec-details-toggle-container {
743
- }
744
- .itsec-details-toggle-container .itsec-details-toggle-details-hidde {
745
- display: none;
746
- }
747
- .itsec-details-toggle-container {
748
- font-size: 13px;
749
- }
750
- .itsec-details-toggle-container .itsec-details-toggle-details :last-child {
751
- margin-bottom: 0;
752
- }
753
- .itsec-details-toggle-container .itsec-details-toggle-details ul li {
754
- margin: 1em 0;
755
- }
756
-
757
- /**
758
- * Styling for email contacts lists
759
- */
760
- ul.itsec-settings-contacts {
761
- list-style: none;
762
- margin: 1em 0;
763
- padding-left: 0;
764
- }
765
- ul.itsec-settings-contacts + ul.itsec-settings-contacts {
766
- padding-top: 1em;
767
- border-top: 1px solid #ddd;
768
- }
769
- ul.itsec-settings-contacts li input[type="checkbox"] {
770
- margin-top: 0;
771
- }
772
-
773
  /**
774
  * Styling for the logs page
775
  */
@@ -921,205 +508,3 @@ body.security_page_itsec-logs .itsec-module-settings-content-main .form-table .w
921
  .itsec-settings-module-settings .ms-options-wrap > button > span {
922
  display: inline;
923
  }
924
-
925
- /***************************************
926
- Tooltip
927
- ****************************************/
928
- .tooltip {
929
- position: relative;
930
- top: -6px;
931
- display: inline-block;
932
- background: #0080C9;
933
- padding: 0px 4px;
934
- border-radius: 50px;
935
- font-size: 0.7em;
936
- margin: -10px 0;
937
- cursor: pointer;
938
- color: #FFFFFF;
939
- text-align: left;
940
- }
941
-
942
- .tooltip-trigger-only {
943
- position: relative;
944
- font-size: 0.7em;
945
- cursor: pointer;
946
- color: #FFFFFF;
947
- text-align: left;
948
- }
949
-
950
- .tooltip:hover .info,
951
- .tooltip-trigger-only:hover .info,
952
- .tooltip:focus .info,
953
- .tooltip-trigger-only:focus .info {
954
- visibility: visible;
955
- opacity: 1;
956
- transform: translate3d(0, 0, 0);
957
- }
958
-
959
- .tooltip .info,
960
- .tooltip-trigger-only .info {
961
- box-sizing: border-box;
962
- position: absolute;
963
- top: 18px;
964
- left: -8px;
965
- display: block;
966
- background: #0080C9;
967
- border: 1px solid #D6D6D6;
968
- width: 350px;
969
- font-size: 1.45em;
970
- font-weight: 300;
971
- line-height: 1.5;
972
- cursor: text;
973
- visibility: hidden;
974
- opacity: 0;
975
- transform: translate3d(0, -20px, 0);
976
- transition: all .5s ease-out;
977
- z-index: 2;
978
- }
979
-
980
- .tooltip .info:before,
981
- .tooltip-trigger-only .info:before {
982
- position: absolute;
983
- content: '';
984
- width: 100%;
985
- height: 14px;
986
- bottom: -14px;
987
- left: 0;
988
- }
989
-
990
- .tooltip .info:after,
991
- .tooltip-trigger-only .info:after {
992
- position: absolute;
993
- content: '';
994
- width: 10px;
995
- height: 10px;
996
- transform: rotate(45deg);
997
- top: -5px;
998
- left: 13px;
999
- margin-left: -5px;
1000
- background: #0080C9;
1001
- }
1002
-
1003
- .tooltip .text,
1004
- .tooltip-trigger-only .text {
1005
- display: block;
1006
- padding: 2em;
1007
- color: #FFFFFF;
1008
- }
1009
-
1010
- .tooltip .info .text a,
1011
- .tooltip-trigger-only .info .text a {
1012
- color: white;
1013
- }
1014
-
1015
- .tooltip .info .text a:hover,
1016
- .tooltip-trigger-only .info .text a:hover {
1017
- color: #000000;
1018
- }
1019
-
1020
- .included-with-bb .tooltip span {
1021
- border-bottom: none;
1022
- }
1023
-
1024
- .tooltip .tooltip-container .info,
1025
- .tooltip-trigger-only .tooltip-container .info {
1026
- border-bottom: 1px solid #D6D6D6;
1027
- }
1028
-
1029
- /***************************************
1030
- Tooltip Responsive Styles
1031
- ****************************************/
1032
- @media ( min-width: 1042px ) and ( max-width: 1342px ) {
1033
- .tooltip .info,
1034
- .tooltip-trigger-only .info {
1035
- width: 250px;
1036
- left: -58px;
1037
- }
1038
-
1039
- .tooltip .info:after,
1040
- .tooltip-trigger-only .info:after {
1041
- left: 63px;
1042
- }
1043
- }
1044
-
1045
- @media ( min-width: 783px ) and ( max-width: 1041px ) {
1046
- .tooltip .info,
1047
- .tooltip-trigger-only .info {
1048
- width: 200px;
1049
- left: -130px;
1050
- }
1051
-
1052
- .tooltip .info:after,
1053
- .tooltip-trigger-only .info:after {
1054
- left: 136px;
1055
- }
1056
- }
1057
-
1058
- @media ( max-width: 740px ) {
1059
- .tooltip .info,
1060
- .tooltip-trigger-only .info {
1061
- width: 250px;
1062
- }
1063
- }
1064
-
1065
- @media ( max-width: 646px ) {
1066
- .tooltip .info,
1067
- .tooltip-trigger-only .info {
1068
- left: -130px;
1069
- }
1070
-
1071
- .tooltip .info:after,
1072
- .tooltip-trigger-only .info:after {
1073
- left: 136px;
1074
- }
1075
- }
1076
-
1077
- @media ( min-width: 450px ) and ( max-width: 520px ) {
1078
- .tooltip .info,
1079
- .tooltip-trigger-only .info {
1080
- left: -230px;
1081
- }
1082
-
1083
- .tooltip .info:after,
1084
- .tooltip-trigger-only .info:after {
1085
- left: 236px;
1086
- }
1087
- }
1088
-
1089
- @media ( max-width: 483px ) {
1090
- #itsec-version-management-scan_for_old_wordpress_sites ~ .tooltip .info {
1091
- left: -8px;
1092
- }
1093
-
1094
- #itsec-version-management-scan_for_old_wordpress_sites ~ .tooltip .info:after {
1095
- left: 14px;
1096
- }
1097
- }
1098
-
1099
- @media ( max-width: 449px ) {
1100
- .tooltip .info,
1101
- .tooltip-trigger-only .info {
1102
- left: -58px;
1103
- }
1104
-
1105
- .tooltip .info:after,
1106
- .tooltip-trigger-only .info:after {
1107
- left: 63px;
1108
- }
1109
-
1110
- .itsec-settings-module-settings .form-table td p {
1111
- max-width: 275px;
1112
- }
1113
- }
1114
-
1115
- @media ( max-width: 336px ) {
1116
- .tooltip .info,
1117
- .tooltip-trigger-only .info {
1118
- left: -100px;
1119
- }
1120
-
1121
- .tooltip .info:after,
1122
- .tooltip-trigger-only .info:after {
1123
- left: 106px;
1124
- }
1125
- }
1
  /*
2
  * Override Core styling
3
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  .wrap h1 img,
6
  .wrap h1 a {
37
  display: none;
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  /* Grid Styling */
41
 
42
  .itsec-modal-background {
87
  min-height: 14em;
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  @media ( max-width: 960px ) {
91
  .grid .itsec-module-settings-container {
92
  margin-left: 36px;
106
  }
107
  }
108
 
 
 
109
  /*
110
  * Grid Settings Modal
111
  */
250
  content: '';
251
  width: 100%;
252
  }
 
 
 
 
 
 
 
 
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  .itsec-module-settings-content pre{
255
  background: #f1f1f1;
256
  overflow:scroll;
257
  padding: 1em;
258
  }
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  /*
261
  * Success Error Messages
262
  */
357
  background-color: #FFCD08;
358
  }
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  /**
361
  * Styling for the logs page
362
  */
508
  .itsec-settings-module-settings .ms-options-wrap > button > span {
509
  display: inline;
510
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/{js/lib → entries}/index.php RENAMED
File without changes
core/admin-pages/entries/settings.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { setLocaleData } from '@wordpress/i18n';
5
+ import { render } from '@wordpress/element';
6
+ import domReady from '@wordpress/dom-ready';
7
+
8
+ // Silence warnings until JS i18n is stable.
9
+ setLocaleData( { '': {} }, 'better-wp-security' );
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { createHistory } from './settings/history';
15
+ import App from './settings/app.js';
16
+
17
+ domReady( () => {
18
+ const history = createHistory( document.location, { page: 'itsec' } );
19
+ const containerEl = document.getElementById( 'itsec-settings-root' );
20
+ const serverType = containerEl.dataset.serverType;
21
+ const installType = containerEl.dataset.installType;
22
+ const onboardComplete = containerEl.dataset.onboard === '1';
23
+
24
+ return render(
25
+ <App
26
+ history={ history }
27
+ serverType={ serverType }
28
+ installType={ installType }
29
+ onboardComplete={ onboardComplete }
30
+ />,
31
+ containerEl
32
+ );
33
+ } );
34
+
35
+ export * from './settings/components';
36
+ export { ToolFill } from './settings/pages/tools';
37
+ export {
38
+ Page,
39
+ ChildPages,
40
+ useNavigation,
41
+ useCurrentPage,
42
+ } from './settings/page-registration';
43
+ export { useNavigateTo, useConfigContext } from './settings/utils';
44
+ export { STORE_NAME as ONBOARD_STORE_NAME } from './settings/stores/onboard';
45
+ export { STORE_NAME as TOOLS_STORE_NAME } from './settings/stores/tools';
core/admin-pages/entries/settings/app.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Router, Switch, Route, Redirect } from 'react-router-dom';
5
+ import { QueryParamProvider } from 'use-query-params';
6
+ import { ErrorBoundary } from 'react-error-boundary';
7
+
8
+ /**
9
+ * WordPress components
10
+ */
11
+ import {
12
+ SlotFillProvider,
13
+ Popover,
14
+ Flex,
15
+ FlexBlock,
16
+ } from '@wordpress/components';
17
+ import { PluginArea } from '@wordpress/plugins';
18
+
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ import PageRegistration from './page-registration';
23
+ import Pages, { Onboard, Settings } from './pages';
24
+ import { ConfigContext } from './utils';
25
+ import { Main, Sidebar, ErrorRenderer } from './components';
26
+ import useSearchProviders from './search';
27
+ import './stores';
28
+ import './style.scss';
29
+
30
+ export default function App( {
31
+ history,
32
+ serverType,
33
+ installType,
34
+ onboardComplete,
35
+ } ) {
36
+ useSearchProviders();
37
+ const redirect = onboardComplete ? '/settings' : '/onboard';
38
+
39
+ return (
40
+ <div className="itsec-settings">
41
+ <ConfigContext.Provider
42
+ value={ { serverType, installType, onboardComplete } }
43
+ >
44
+ <Router history={ history }>
45
+ <QueryParamProvider ReactRouterRoute={ Route }>
46
+ <SlotFillProvider>
47
+ <ErrorBoundary
48
+ FallbackComponent={ GlobalErrorBoundary }
49
+ >
50
+ <PageRegistration>
51
+ <Pages />
52
+ <PluginArea />
53
+ <Popover.Slot />
54
+ <Switch>
55
+ <Route
56
+ path="/:root(settings)"
57
+ component={ Settings }
58
+ />
59
+ <Route
60
+ path="/:root(onboard)"
61
+ component={ Onboard }
62
+ />
63
+
64
+ <Route path="/">
65
+ <Redirect to={ redirect } />
66
+ <Sidebar />
67
+ <Main />
68
+ </Route>
69
+ </Switch>
70
+ </PageRegistration>
71
+ </ErrorBoundary>
72
+ </SlotFillProvider>
73
+ </QueryParamProvider>
74
+ </Router>
75
+ </ConfigContext.Provider>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function GlobalErrorBoundary( props ) {
81
+ return (
82
+ <Flex>
83
+ <FlexBlock>
84
+ <ErrorRenderer { ...props } />
85
+ </FlexBlock>
86
+ </Flex>
87
+ );
88
+ }
core/admin-pages/entries/settings/components/breadcrumbs/index.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Link, useLocation, useRouteMatch } from 'react-router-dom';
5
+ import { createPath } from 'history';
6
+ import { identity } from 'lodash';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import { Dropdown, Button, NavigableMenu } from '@wordpress/components';
12
+ import { DOWN } from '@wordpress/keycodes';
13
+ import { useFocusOnMount } from '@wordpress/compose';
14
+ import { __ } from '@wordpress/i18n';
15
+ import { useMemo } from '@wordpress/element';
16
+
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import {
21
+ useCurrentChildPages,
22
+ useCurrentPage,
23
+ usePages,
24
+ } from '../../page-registration';
25
+ import './style.scss';
26
+
27
+ export default function Breadcrumbs( { title, trail, match } ) {
28
+ const localTrail = useBreadcrumbTrail( title, match );
29
+ trail = trail || localTrail;
30
+
31
+ if ( trail.length <= 1 ) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <nav
37
+ className="itsec-breadcrumbs"
38
+ aria-label={ __( 'Breadcrumbs', 'better-wp-security' ) }
39
+ >
40
+ <ul className="itsec-breadcrumbs">
41
+ { trail
42
+ .map( ( crumb, i ) => {
43
+ const current = i === trail.length - 1 ? 'page' : null;
44
+
45
+ if ( crumb.to ) {
46
+ return (
47
+ <li key={ i }>
48
+ <Link
49
+ to={ crumb.to }
50
+ aria-current={ current }
51
+ >
52
+ { crumb.title }
53
+ </Link>
54
+ </li>
55
+ );
56
+ }
57
+
58
+ if ( crumb.childPages ) {
59
+ return (
60
+ <li key={ i }>
61
+ <Menu
62
+ selected={ crumb.selected }
63
+ childPages={ crumb.childPages }
64
+ aria-current={ current }
65
+ />
66
+ </li>
67
+ );
68
+ }
69
+
70
+ return null;
71
+ } )
72
+ .filter( identity ) }
73
+ </ul>
74
+ </nav>
75
+ );
76
+ }
77
+
78
+ export function useBreadcrumbTrail( title, match ) {
79
+ const localMatch = useRouteMatch();
80
+ const location = useLocation();
81
+ const {
82
+ url,
83
+ params: { root, page: currentPageId, child },
84
+ } = match || localMatch;
85
+
86
+ const pages = usePages( root );
87
+ let childPages = useCurrentChildPages();
88
+
89
+ if ( useCurrentPage()?.id !== currentPageId ) {
90
+ childPages = [];
91
+ }
92
+
93
+ const currentPage = pages.find( ( page ) => page.id === currentPageId );
94
+ const currentChildPage = childPages.find( ( page ) => page.id === child );
95
+
96
+ return useMemo( () => {
97
+ const crumbs = [];
98
+ crumbs.push( {
99
+ title: currentPage.title,
100
+ to: `/${ root }/${ currentPage.id }`,
101
+ } );
102
+
103
+ if ( currentChildPage && childPages.length ) {
104
+ crumbs.push( {
105
+ title: currentChildPage.title,
106
+ selected: currentChildPage,
107
+ childPages,
108
+ } );
109
+ }
110
+
111
+ if (
112
+ title &&
113
+ title !== currentChildPage?.title &&
114
+ title !== currentPage.title
115
+ ) {
116
+ crumbs.push( {
117
+ title,
118
+ to: location.pathname.startsWith( url )
119
+ ? createPath( location )
120
+ : url,
121
+ } );
122
+ }
123
+
124
+ return crumbs;
125
+ }, [ title, root, currentPage, currentChildPage, location ] );
126
+ }
127
+
128
+ export function useHelpBreadcrumbTrail( title ) {
129
+ const { url } = useRouteMatch();
130
+ const trail = useBreadcrumbTrail( __( 'Help', 'better-wp-security' ) );
131
+
132
+ return useMemo( () => {
133
+ const withModule = [ ...trail ];
134
+
135
+ if ( title ) {
136
+ withModule.splice( withModule.length - 1, 0, {
137
+ to: url,
138
+ title,
139
+ } );
140
+ }
141
+
142
+ return withModule;
143
+ }, [ trail, title ] );
144
+ }
145
+
146
+ function Menu( { childPages, selected, 'aria-current': ariaCurrent } ) {
147
+ const focusRef = useFocusOnMount();
148
+
149
+ return (
150
+ <Dropdown
151
+ className="itsec-breadcrumbs__menu"
152
+ popoverProps={ {
153
+ position: 'bottom center',
154
+ className: 'itsec-breadcrumbs__menu-popover',
155
+ focusOnMount: 'container',
156
+ } }
157
+ renderToggle={ ( { isOpen, onToggle } ) => {
158
+ const openOnArrowDown = ( event ) => {
159
+ if ( ! isOpen && event.keyCode === DOWN ) {
160
+ event.preventDefault();
161
+ event.stopPropagation();
162
+ onToggle();
163
+ }
164
+ };
165
+
166
+ return (
167
+ <Button
168
+ aria-haspopup
169
+ aria-expanded={ isOpen }
170
+ onClick={ onToggle }
171
+ onKeyDown={ openOnArrowDown }
172
+ text={ selected.title }
173
+ icon={ isOpen ? 'arrow-up' : 'arrow-down' }
174
+ iconPosition="right"
175
+ />
176
+ );
177
+ } }
178
+ renderContent={ () => (
179
+ <NavigableMenu role="menu">
180
+ { childPages.map( ( { id, to, title } ) => (
181
+ <Link
182
+ key={ to }
183
+ to={ to }
184
+ ref={ selected.id === id ? focusRef : null }
185
+ aria-current={
186
+ selected.id === id ? ariaCurrent : null
187
+ }
188
+ >
189
+ { title }
190
+ </Link>
191
+ ) ) }
192
+ </NavigableMenu>
193
+ ) }
194
+ />
195
+ );
196
+ }
core/{modules/away-mode/css/images → admin-pages/entries/settings/components/breadcrumbs}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/breadcrumbs/style.scss ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-breadcrumbs {
2
+ ul {
3
+ display: flex;
4
+ align-items: center;
5
+ margin: 0;
6
+
7
+ li {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ margin: 0;
11
+ min-height: 26px;
12
+ color: $dark-text;
13
+ font-size: .75rem;
14
+
15
+ a {
16
+ color: $dark-text;
17
+ text-decoration: none;
18
+
19
+ &:hover {
20
+ color: $main-blue;
21
+ }
22
+ }
23
+
24
+ &:after {
25
+ @include dashicon('\f345', .75rem);
26
+ margin: 0 .25rem;
27
+ color: $medium-text;
28
+ }
29
+
30
+ &:last-child {
31
+ color: $medium-text;
32
+
33
+ a {
34
+ color: $medium-text;
35
+
36
+ &:hover {
37
+ color: $main-blue;
38
+ }
39
+ }
40
+
41
+ &:after {
42
+ content: '';
43
+ }
44
+ }
45
+ }
46
+
47
+ .itsec-breadcrumbs__menu {
48
+ margin-left: 2px;
49
+
50
+ .components-button.has-icon {
51
+ color: $main-blue;
52
+ padding: 3px;
53
+ height: auto;
54
+
55
+ .dashicon {
56
+ margin-left: 2px;
57
+ margin-right: -5px;
58
+ }
59
+
60
+ &:hover {
61
+ color: $dark-text;
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ .itsec-breadcrumbs__menu-popover {
69
+ .components-popover__content > div {
70
+ padding: .5rem;
71
+ }
72
+
73
+ a {
74
+ display: block;
75
+ width: max-content;
76
+ color: $medium-text;
77
+ border-left: 3px solid transparent;
78
+ text-decoration: none;
79
+ padding: .125rem calc(.25rem + 3px) .125rem .25rem;
80
+ margin: .5rem 0;
81
+ font-weight: 500;
82
+
83
+ &:first-child {
84
+ margin-top: 0;
85
+ }
86
+
87
+ &:last-child {
88
+ margin-bottom: 0;
89
+ }
90
+
91
+ &:hover,
92
+ &:focus {
93
+ color: $dark-text;
94
+ border-left-color: $main-blue;
95
+ outline: none;
96
+ box-shadow: none;
97
+ }
98
+ }
99
+ }
core/admin-pages/entries/settings/components/error-renderer/index.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useLocation } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import {
10
+ Button,
11
+ Card,
12
+ CardFooter,
13
+ CardBody,
14
+ ClipboardButton,
15
+ } from '@wordpress/components';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { FlexSpacer } from '@ithemes/security-components';
22
+ import { Crash as Icon } from '@ithemes/security-style-guide';
23
+ import './style.scss';
24
+
25
+ export default function ErrorRenderer( { error } ) {
26
+ const { pathname } = useLocation();
27
+
28
+ return (
29
+ <Card className="itsec-error-renderer">
30
+ <CardBody>
31
+ <Icon />
32
+ </CardBody>
33
+ <CardFooter isShady>
34
+ { __( 'An unexpected error occurred.', 'better-wp-security' ) }
35
+ <FlexSpacer />
36
+ <Button isSecondary onClick={ () => window.location.reload() }>
37
+ { __( 'Refresh', 'better-wp-security' ) }
38
+ </Button>
39
+ <ClipboardButton
40
+ isPrimary
41
+ text={ `Page: ${ pathname }\nError: ${ error.stack }` }
42
+ >
43
+ { __( 'Copy Error', 'better-wp-security' ) }
44
+ </ClipboardButton>
45
+ </CardFooter>
46
+ </Card>
47
+ );
48
+ }
core/{modules/away-mode/sync-verbs → admin-pages/entries/settings/components/error-renderer}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/error-renderer/style.scss ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ .itsec-error-renderer {
2
+ svg {
3
+ max-width: 35rem;
4
+ padding: 1rem;
5
+ margin: 0 auto;
6
+ display: block;
7
+ }
8
+ }
core/admin-pages/entries/settings/components/help/index.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useLocation, Link } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { createSlotFill, ToolbarButton } from '@wordpress/components';
10
+ import { __ } from '@wordpress/i18n';
11
+ import { useState, useEffect } from '@wordpress/element';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { withNavigate } from '@ithemes/security-hocs';
17
+ import { ToolbarFill } from '../toolbar';
18
+ import './style.scss';
19
+
20
+ export default function Help() {
21
+ const [ hasHelp, setHasHelp ] = useState( false );
22
+ const location = useLocation();
23
+ const isVisible = location.hash === '#help';
24
+ const to = { ...location, hash: isVisible ? '' : '#help' };
25
+
26
+ return (
27
+ <>
28
+ <ToolbarFill>
29
+ <Link
30
+ component={ withNavigate( ToolbarButton ) }
31
+ icon="editor-help"
32
+ to={ to }
33
+ disabled={ ! hasHelp }
34
+ className="itsec-help-toggle-link"
35
+ text={
36
+ isVisible
37
+ ? __( 'Exit Help', 'better-wp-security' )
38
+ : __( 'Help', 'better-wp-security' )
39
+ }
40
+ />
41
+ </ToolbarFill>
42
+ <HelpSlot>
43
+ { ( fills ) => (
44
+ <HelpContent
45
+ fills={ fills }
46
+ isVisible={ isVisible }
47
+ setHasHelp={ setHasHelp }
48
+ />
49
+ ) }
50
+ </HelpSlot>
51
+ </>
52
+ );
53
+ }
54
+
55
+ function HelpContent( { fills, isVisible, setHasHelp } ) {
56
+ useEffect( () => setHasHelp( fills.length > 0 ), [ fills ] );
57
+
58
+ if ( ! isVisible ) {
59
+ return null;
60
+ }
61
+
62
+ return fills;
63
+ }
64
+
65
+ const { Slot: HelpSlot, Fill: HelpFill } = createSlotFill( 'Help' );
66
+
67
+ export { HelpFill };
core/{modules/ban-users/Module → admin-pages/entries/settings/components/help}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/help/style.scss ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-help-header {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ }
6
+
7
+ .itsec-help-header__title {
8
+ color: $main-blue;
9
+ display: block;
10
+ font-size: 1.6rem;
11
+ font-weight: 600;
12
+ text-transform: uppercase;
13
+ }
14
+
15
+ .itsec-help-header__back-link.components-button {
16
+ color: $main-blue;
17
+ height: auto;
18
+ margin: 0;
19
+
20
+ &:hover {
21
+ color: $dark-text;
22
+ }
23
+ }
core/admin-pages/entries/settings/components/index.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export { default as Sidebar } from './sidebar';
2
+ export { default as Main, AsideFill } from './main';
3
+ export { default as Toolbar, ToolbarFill } from './toolbar';
4
+ export { default as Navigation, NavigationFill } from './navigation';
5
+ export {
6
+ default as Breadcrumbs,
7
+ useBreadcrumbTrail,
8
+ useHelpBreadcrumbTrail,
9
+ } from './breadcrumbs';
10
+ export { default as PageHeader } from './page-header';
11
+ export { default as SelectableCard } from './selectable-card';
12
+ export { default as PrimaryForm, PrimaryFormSection } from './primary-form';
13
+ export {
14
+ default as PrimarySchemaForm,
15
+ PrimarySchemaFormInputs,
16
+ PrimarySchemaFormActions,
17
+ } from './primary-schema-form';
18
+ export { default as Help, HelpFill } from './help';
19
+ export { default as ErrorRenderer } from './error-renderer';
20
+ export { default as Logo } from './logo';
core/{modules/core/entries/admin-notices/components/admin-bar → admin-pages/entries/settings/components}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/logo/index.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import {
5
+ LogoFreeColor,
6
+ LogoFreeWhite,
7
+ LogoProColor,
8
+ LogoProWhite,
9
+ } from '@ithemes/security-style-guide';
10
+ import { useConfigContext } from '../../utils';
11
+
12
+ export default function Logo( { style, className } ) {
13
+ const { installType } = useConfigContext();
14
+ let Component;
15
+
16
+ if ( installType === 'pro' ) {
17
+ Component = style === 'white' ? LogoProWhite : LogoProColor;
18
+ } else {
19
+ Component = style === 'white' ? LogoFreeWhite : LogoFreeColor;
20
+ }
21
+
22
+ return <Component className={ className } />;
23
+ }
core/{modules/file-writing/js → admin-pages/entries/settings/components/logo}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/main/index.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useLocation } from 'react-router-dom';
5
+ import { ErrorBoundary } from 'react-error-boundary';
6
+ import classnames from 'classnames';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import { useEffect, useRef, useState } from '@wordpress/element';
12
+ import { createSlotFill } from '@wordpress/components';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { Toolbar, Help, ErrorRenderer } from '../';
18
+ import { useCurrentPage } from '../../page-registration';
19
+ import './style.scss';
20
+
21
+ export default function Main( { children } ) {
22
+ const page = useCurrentPage();
23
+
24
+ // Focus handling
25
+ const ref = useRef();
26
+ const location = useLocation();
27
+ const { hash } = location;
28
+ useEffect( () => {
29
+ ref.current?.focus();
30
+ ref.current?.scrollIntoView();
31
+ }, [ location ] );
32
+
33
+ // Aside handling
34
+ const [ hasAside, setHasAside ] = useState( false );
35
+
36
+ return (
37
+ <div className="itsec-settings-main" ref={ ref } tabIndex={ -1 }>
38
+ <Toolbar />
39
+ <div
40
+ className={ classnames( 'itsec-settings-main__wrapper', {
41
+ 'itsec-settings-main__wrapper--has-aside': hasAside,
42
+ } ) }
43
+ >
44
+ <main
45
+ aria-labelledby="itsec-page-header"
46
+ className={ classnames( 'itsec-settings-main__content', {
47
+ [ `itsec-page--${ page?.id }` ]: !! page,
48
+ } ) }
49
+ >
50
+ <ErrorBoundary FallbackComponent={ ErrorRenderer }>
51
+ <Help />
52
+ { hash === '#help' ? (
53
+ <div hidden>{ children }</div>
54
+ ) : (
55
+ children
56
+ ) }
57
+ </ErrorBoundary>
58
+ </main>
59
+ <AsideSlot>
60
+ { ( fills ) => (
61
+ <AsideContent
62
+ fills={ fills }
63
+ setHasAside={ setHasAside }
64
+ />
65
+ ) }
66
+ </AsideSlot>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function AsideContent( { fills, setHasAside } ) {
73
+ useEffect( () => setHasAside( fills.length > 0 ), [ fills ] );
74
+
75
+ if ( fills.length ) {
76
+ return <aside className="itsec-aside">{ fills }</aside>;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ const { Slot: AsideSlot, Fill: AsideFill } = createSlotFill( 'Aside' );
83
+
84
+ export { AsideFill };
core/{modules/global/css → admin-pages/entries/settings/components/main}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/main/style.scss ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings-main {
2
+ flex-grow: 1;
3
+ background: $background-blue;
4
+ z-index: 1;
5
+ overflow: auto;
6
+ }
7
+
8
+ .itsec-settings-main__wrapper {
9
+ padding: .5rem .5rem 2rem .5rem;
10
+ margin-top: .25rem;
11
+
12
+ @include break-mobile {
13
+ padding: 1rem;
14
+ }
15
+
16
+ @include break-small {
17
+ padding: 2rem;
18
+ margin-top: calc(40px + 1rem);
19
+ }
20
+
21
+ @include break-min-width($break-medium + 1) {
22
+ padding: 3rem;
23
+ margin-top: 0;
24
+ }
25
+ }
26
+
27
+ .itsec-settings-main__content {
28
+ max-width: 70rem;
29
+ margin: 0 auto;
30
+ position: relative;
31
+
32
+ &:focus {
33
+ outline: none;
34
+ }
35
+ }
36
+
37
+ .itsec-settings-main__content > header,
38
+ .itsec-settings-main__content > .components-disabled > header {
39
+ & :first-child {
40
+ margin-top: 0;
41
+ }
42
+
43
+ & :last-child {
44
+ margin-bottom: 0;
45
+ }
46
+ }
47
+
48
+ .itsec-settings-main__content > *,
49
+ .itsec-settings-main__content > .components-disabled > * {
50
+ margin-bottom: 1rem;
51
+
52
+ @include break-small {
53
+ margin-bottom: 2rem;
54
+ }
55
+
56
+ &:empty {
57
+ display: none;
58
+ }
59
+
60
+ &:last-child {
61
+ margin-bottom: 0;
62
+ }
63
+
64
+ &.itsec-message-list:not(:first-child):not(:last-child) {
65
+ margin-top: -.5rem;
66
+
67
+ @include break-small {
68
+ margin-top: -1rem;
69
+ }
70
+ }
71
+ }
72
+
73
+ .itsec-settings-main__wrapper.itsec-settings-main__wrapper--has-aside {
74
+ @mixin wide {
75
+ @include break-min-width(1599px) {
76
+ body:not(.folded) & {
77
+ @content
78
+ }
79
+ }
80
+
81
+ @include break-min-width(1475px) {
82
+ body.folded & {
83
+ @content
84
+ }
85
+ }
86
+ }
87
+
88
+ display: flex;
89
+ justify-content: flex-start;
90
+ flex-direction: column;
91
+ align-items: center;
92
+
93
+ @include break-small {
94
+ align-items: stretch;
95
+ }
96
+
97
+ @include wide {
98
+ flex-direction: row;
99
+ justify-content: center;
100
+ }
101
+
102
+ .itsec-settings-main__content {
103
+ margin: 0;
104
+ flex-grow: 2;
105
+ flex-shrink: 1;
106
+ }
107
+
108
+ .itsec-aside {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ justify-content: center;
112
+ align-items: flex-start;
113
+ flex-grow: 1;
114
+ min-width: 15rem;
115
+ margin-left: -1rem;
116
+
117
+ @include break-small {
118
+ justify-content: stretch;
119
+ }
120
+
121
+ @include wide {
122
+ align-content: flex-start;;
123
+ margin-top: -1rem;
124
+ margin-left: 0;
125
+ width: min-content;
126
+ max-width: 18rem;
127
+ }
128
+
129
+ & > * {
130
+ flex-basis: 15rem;
131
+ flex-grow: 1;
132
+ flex-shrink: 1;
133
+
134
+ min-width: 15rem;
135
+ max-width: 18rem;
136
+
137
+ margin-top: 1rem;
138
+ margin-left: 1rem;
139
+ }
140
+ }
141
+ }
core/admin-pages/entries/settings/components/navigation/index.js ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import { Link, NavLink, useParams } from 'react-router-dom';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { createSlotFill, Dashicon } from '@wordpress/components';
11
+ import { Children } from '@wordpress/element';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { useCurrentChildPages, usePages } from '../../page-registration';
17
+ import { useChildPath } from '../../utils';
18
+ import './style.scss';
19
+
20
+ export default function Navigation( {
21
+ guided = false,
22
+ allowBack = false,
23
+ allowForward = false,
24
+ children,
25
+ } ) {
26
+ const pages = usePages();
27
+ const { root, page: active } = useParams();
28
+ const childPath = useChildPath();
29
+
30
+ const activeIndex = pages.findIndex( ( item ) => item.id === active );
31
+
32
+ return (
33
+ <nav>
34
+ <ul
35
+ className={ classnames( 'itsec-nav', {
36
+ 'itsec-nav--guided': guided,
37
+ } ) }
38
+ >
39
+ { pages.map( ( item, i ) => {
40
+ if ( false === item.priority ) {
41
+ return null;
42
+ }
43
+
44
+ let asLink = ! guided;
45
+
46
+ if ( allowBack && i <= activeIndex ) {
47
+ asLink = true;
48
+ } else if ( allowForward && i >= activeIndex ) {
49
+ asLink = true;
50
+ }
51
+
52
+ let isActive = active === item.id;
53
+
54
+ if (
55
+ isActive &&
56
+ item.ignore &&
57
+ item.ignore.find( ( path ) =>
58
+ childPath.startsWith( path )
59
+ )
60
+ ) {
61
+ isActive = false;
62
+ }
63
+
64
+ const icon = guided
65
+ ? 'yes-alt'
66
+ : item.icon || 'admin-generic';
67
+
68
+ return (
69
+ <li
70
+ key={ item.id }
71
+ className={ classnames( 'itsec-nav__item', {
72
+ 'itsec-nav__item--active': isActive,
73
+ 'itsec-nav__item--completed':
74
+ guided && i < activeIndex,
75
+ } ) }
76
+ >
77
+ <span className="itsec-nav__item-title">
78
+ { ! asLink ? (
79
+ <>
80
+ <Dashicon icon={ icon } />
81
+ <span className="itsec-nav__item-title-text">
82
+ { item.title }
83
+ </span>
84
+ </>
85
+ ) : (
86
+ <Link to={ `/${ root }/${ item.id }` }>
87
+ <Dashicon icon={ icon } />
88
+ <span className="itsec-nav__item-title-text">
89
+ { item.title }
90
+ </span>
91
+ </Link>
92
+ ) }
93
+ </span>
94
+
95
+ { ! isActive ? null : (
96
+ <>
97
+ { children }
98
+ <ChildPages item={ item } />
99
+ </>
100
+ ) }
101
+ </li>
102
+ );
103
+ } ) }
104
+ </ul>
105
+ </nav>
106
+ );
107
+ }
108
+
109
+ function ChildPages( { item } ) {
110
+ const childPages = useCurrentChildPages();
111
+
112
+ return (
113
+ <NavigationSlot fillProps={ { item } }>
114
+ { ( fills ) => {
115
+ if (
116
+ Children.count( fills ) === 0 &&
117
+ childPages.length === 0
118
+ ) {
119
+ return null;
120
+ }
121
+
122
+ return (
123
+ <ul className="itsec-nav__children">
124
+ { childPages.map( ( { to, title, ...rest } ) => (
125
+ <li key={ to }>
126
+ <NavLink to={ to } { ...rest }>
127
+ { title }
128
+ </NavLink>
129
+ </li>
130
+ ) ) }
131
+
132
+ { Children.map( fills, ( child, j ) => (
133
+ <li key={ j }>{ child }</li>
134
+ ) ) }
135
+ </ul>
136
+ );
137
+ } }
138
+ </NavigationSlot>
139
+ );
140
+ }
141
+
142
+ const { Fill: NavigationFill, Slot: NavigationSlot } = createSlotFill(
143
+ 'Navigation'
144
+ );
145
+
146
+ export { NavigationFill };
core/{modules/hide-backend/css → admin-pages/entries/settings/components/navigation}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/navigation/style.scss ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-nav {
2
+ margin: 0;
3
+ --icon-size: 30px;
4
+
5
+ @include break-xlarge {
6
+ &.itsec-nav--guided {
7
+ --icon-size: 40px;
8
+ }
9
+ }
10
+
11
+ .itsec-nav__item {
12
+ margin: 1.25rem 0;
13
+ }
14
+
15
+ .itsec-nav__item-title {
16
+ display: flex;
17
+ align-items: center;
18
+ margin: 0;
19
+ font-weight: bold;
20
+ text-transform: uppercase;
21
+ font-size: 1.25rem;
22
+ line-height: 1;
23
+ color: $medium-text;
24
+ }
25
+
26
+ .itsec-nav__item-title a {
27
+ color: $medium-text;
28
+ text-decoration: none;
29
+ display: flex;
30
+ align-items: center;
31
+ }
32
+
33
+ .itsec-nav__item--completed .itsec-nav__item-title,
34
+ .itsec-nav__item--completed .itsec-nav__item-title a {
35
+ color: $light-text;
36
+ }
37
+
38
+ .itsec-nav__item--active .itsec-nav__item-title,
39
+ .itsec-nav__item--active .itsec-nav__item-title a,
40
+ .itsec-nav__item-title a:hover {
41
+ color: $main-blue;
42
+ }
43
+
44
+ .itsec-nav__item-title .dashicon {
45
+ margin-right: .5rem;
46
+ width: var(--icon-size);
47
+ height: var(--icon-size);
48
+ font-size: var(--icon-size);
49
+ }
50
+
51
+ .itsec-nav__children {
52
+ // Width of icon and margin.
53
+ margin-left: calc(30px + .5rem);
54
+ margin-top: 1rem;
55
+ }
56
+
57
+ &.itsec-nav--guided .itsec-nav__children {
58
+ // Width of icon and margin.
59
+ margin-left: calc(40px + .5rem);
60
+ margin-top: .5rem;
61
+ }
62
+
63
+ .itsec-nav__children a {
64
+ color: $medium-text;
65
+ text-decoration: none;
66
+ font-weight: 500;
67
+ border-left: 3px solid transparent;
68
+ padding-left: 5px;
69
+ }
70
+
71
+ .itsec-nav__children a.active,
72
+ .itsec-nav__children a:hover {
73
+ color: $dark-text;
74
+ border-left-color: $main-blue;
75
+ }
76
+ }
core/admin-pages/entries/settings/components/page-header/index.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import { useLocation } from 'react-router-dom';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { HelpPopover, Markup } from '@ithemes/security-components';
11
+ import { Breadcrumbs } from '../';
12
+ import './style.scss';
13
+
14
+ export default function PageHeader( {
15
+ title,
16
+ subtitle,
17
+ description,
18
+ help,
19
+ align = 'left',
20
+ breadcrumbs = true,
21
+ children,
22
+ } ) {
23
+ const location = useLocation();
24
+
25
+ return (
26
+ <header
27
+ className={ classnames(
28
+ 'itsec-page-header',
29
+ `itsec-page-header--align-${ align }`,
30
+ {
31
+ 'itsec-page-header--has-actions': !! children,
32
+ 'itsec-page-header--has-help': !! help,
33
+ }
34
+ ) }
35
+ >
36
+ { breadcrumbs === true && <Breadcrumbs title={ title } /> }
37
+ { breadcrumbs }
38
+
39
+ <div className="itsec-page-header__text">
40
+ <h1 id="itsec-page-header">
41
+ { title }
42
+ { help && (
43
+ <HelpPopover
44
+ help={ help }
45
+ to={ { ...location, hash: '#help' } }
46
+ />
47
+ ) }
48
+ </h1>
49
+ { subtitle && <h2>{ subtitle }</h2> }
50
+ { description && (
51
+ <Markup content={ description } tagName="p" />
52
+ ) }
53
+ </div>
54
+
55
+ { children && (
56
+ <div className="itsec-page-header__actions">{ children }</div>
57
+ ) }
58
+ </header>
59
+ );
60
+ }
core/{modules/malware/css → admin-pages/entries/settings/components/page-header}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/page-header/style.scss ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-page-header--align-center {
2
+ text-align: center;
3
+ }
4
+
5
+ .itsec-page-header--align-right {
6
+ text-align: right;
7
+ }
8
+
9
+ .itsec-page-header {
10
+ display: grid;
11
+ grid-template-areas: "breadcrumbs breadcrumbs" "text actions";
12
+ grid-template-columns: auto auto;
13
+ gap: .5rem 1rem;
14
+ align-items: center;
15
+ }
16
+
17
+ .itsec-page-header .itsec-breadcrumbs {
18
+ grid-area: breadcrumbs;
19
+ }
20
+
21
+ .itsec-page-header__text {
22
+ grid-row-start: text;
23
+ grid-row-end: text;
24
+ grid-column-start: text;
25
+ grid-column-end: actions-end;
26
+ }
27
+
28
+ .itsec-page-header--has-actions .itsec-page-header__text {
29
+ grid-area: text;
30
+ }
31
+
32
+ .itsec-page-header__actions {
33
+ grid-area: actions;
34
+ justify-self: end;
35
+ }
36
+
37
+ .itsec-page-header--has-help h1 {
38
+ display: flex;
39
+ align-items: center;
40
+
41
+ .components-button {
42
+ margin: 0 0 0 .5rem;
43
+ padding: 0;
44
+ min-width: auto;
45
+ color: $main-blue;
46
+
47
+ .dashicon {
48
+ margin: 0;
49
+ padding: 0;
50
+ }
51
+
52
+ &:hover {
53
+ color: $dark-text;
54
+ }
55
+ }
56
+ }
core/admin-pages/entries/settings/components/primary-form/index.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Card, CardBody, Button, Flex, FlexItem } from '@wordpress/components';
10
+ import { __ } from '@wordpress/i18n';
11
+ import { useInstanceId } from '@wordpress/compose';
12
+ import { Children, isValidElement } from '@wordpress/element';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { FlexSpacer, ErrorList } from '@ithemes/security-components';
18
+ import './style.scss';
19
+
20
+ export default function PrimaryForm( {
21
+ onSubmit = () => {},
22
+ saveLabel = __( 'Save', 'better-wp-security' ),
23
+ isSaving,
24
+ saveDisabled = false,
25
+ onCancel,
26
+ cancelHref,
27
+ cancelLabel = __( 'Cancel', 'better-wp-security' ),
28
+ id,
29
+ children,
30
+ buttons,
31
+ errors,
32
+ apiError,
33
+ } ) {
34
+ const generatedId = useInstanceId( PrimaryForm, 'itsec-primary-form' );
35
+ id = id || generatedId;
36
+
37
+ return (
38
+ <>
39
+ <Card className="itsec-primary-form">
40
+ <CardBody>
41
+ <ErrorList errors={ errors } apiError={ apiError } />
42
+ <form
43
+ id={ id }
44
+ onSubmit={ ( e ) => {
45
+ e.preventDefault();
46
+ onSubmit();
47
+ } }
48
+ >
49
+ { children }
50
+ </form>
51
+ </CardBody>
52
+ </Card>
53
+
54
+ <Flex>
55
+ { ( onCancel || cancelHref ) && (
56
+ <FlexItem>
57
+ <Button
58
+ isTertiary
59
+ onClick={ onCancel }
60
+ href={ cancelHref }
61
+ >
62
+ { cancelLabel }
63
+ </Button>
64
+ </FlexItem>
65
+ ) }
66
+
67
+ <FlexSpacer />
68
+
69
+ { buttons &&
70
+ buttons.map( ( button, i ) => (
71
+ <FlexItem key={ i }>{ button }</FlexItem>
72
+ ) ) }
73
+
74
+ <FlexItem>
75
+ <Button
76
+ isPrimary
77
+ isBusy={ isSaving }
78
+ disabled={ saveDisabled }
79
+ form={ id }
80
+ type="submit"
81
+ >
82
+ { saveLabel }
83
+ </Button>
84
+ </FlexItem>
85
+ </Flex>
86
+ </>
87
+ );
88
+ }
89
+
90
+ export function PrimaryFormSection( { heading, className, children } ) {
91
+ if ( ! Children.toArray( children ).some( isValidElement ) ) {
92
+ return null;
93
+ }
94
+
95
+ return (
96
+ <div
97
+ className={ classnames( 'itsec-primary-form__section', className ) }
98
+ >
99
+ { heading && (
100
+ <h3 className="itsec-primary-form__section-title">
101
+ { heading }
102
+ </h3>
103
+ ) }
104
+
105
+ { children }
106
+ </div>
107
+ );
108
+ }
core/{modules/malware/js → admin-pages/entries/settings/components/primary-form}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/primary-form/style.scss ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings {
2
+ .itsec-primary-form {
3
+ .components-base-control__label,
4
+ .components-input-control__label,
5
+ label,
6
+ caption,
7
+ legend {
8
+ color: $dark-text;
9
+ }
10
+ }
11
+
12
+ .itsec-primary-form__section {
13
+ display: grid;
14
+ grid-template-columns: [label fields] minmax(0, 1fr);
15
+ grid-template-rows: auto;
16
+ grid-gap: .5rem 0;
17
+
18
+ @include break-small {
19
+ grid-template-columns: [label] 10rem [fields] minmax(0, 1fr);
20
+ }
21
+
22
+ .itsec-primary-form__section-title,
23
+ .itsec-primary-form__section-title label {
24
+ color: $dark-text;
25
+ grid-column: label;
26
+ font-size: 1rem;
27
+ padding-right: 1rem;
28
+ }
29
+
30
+ &:not(:first-child) {
31
+ border-top: 1px solid $border-color;
32
+ margin-top: 1rem;
33
+ padding-top: 1rem;
34
+
35
+ & + * {
36
+ border-top: 1px solid $border-color;
37
+ margin-top: 1rem;
38
+ padding-top: 1rem;
39
+ }
40
+ }
41
+
42
+ > *:not(.itsec-primary-form__section-title) {
43
+ grid-column: fields;
44
+ }
45
+ }
46
+ }
core/admin-pages/entries/settings/components/primary-schema-form/index.js ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { withTheme } from '@rjsf/core';
5
+ import { Link } from 'react-router-dom';
6
+ import classnames from 'classnames';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import { Card, CardBody, Button, Flex, FlexItem } from '@wordpress/components';
12
+ import { __ } from '@wordpress/i18n';
13
+ import { useInstanceId } from '@wordpress/compose';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import Theme from '@ithemes/security-rjsf-theme';
19
+ import { ErrorList, FlexSpacer } from '@ithemes/security-components';
20
+ import { withNavigate } from '@ithemes/security-hocs';
21
+ import './style.scss';
22
+
23
+ const SchemaForm = withTheme( Theme );
24
+
25
+ export default function PrimarySchemaForm( {
26
+ saveLabel,
27
+ isSaving,
28
+ saveDisabled,
29
+ cancelLabel,
30
+ onCancel,
31
+ cancelRoute,
32
+ undoLabel,
33
+ undoDisabled,
34
+ onUndo,
35
+ children,
36
+ errors,
37
+ apiError,
38
+ schemaError,
39
+ ...rest
40
+ } ) {
41
+ let id = useInstanceId( PrimarySchemaForm, 'itsec-schema-form' );
42
+ id = rest.id || id;
43
+
44
+ return (
45
+ <>
46
+ <Card>
47
+ <CardBody>
48
+ <ErrorList
49
+ errors={ errors }
50
+ apiError={ apiError }
51
+ schemaError={ schemaError }
52
+ />
53
+ <PrimarySchemaFormInputs { ...rest } id={ id } />
54
+ </CardBody>
55
+ </Card>
56
+
57
+ <PrimarySchemaFormActions
58
+ id={ id }
59
+ saveLabel={ saveLabel }
60
+ cancelLabel={ cancelLabel }
61
+ isSaving={ isSaving }
62
+ saveDisabled={ saveDisabled }
63
+ onCancel={ onCancel }
64
+ cancelRoute={ cancelRoute }
65
+ undoLabel={ undoLabel }
66
+ undoDisabled={ undoDisabled }
67
+ onUndo={ onUndo }
68
+ >
69
+ { children }
70
+ </PrimarySchemaFormActions>
71
+ </>
72
+ );
73
+ }
74
+
75
+ export function PrimarySchemaFormInputs( { className, ...rest } ) {
76
+ return (
77
+ <SchemaForm
78
+ { ...rest }
79
+ className={ classnames(
80
+ 'itsec-primary-schema-form',
81
+ 'rjsf',
82
+ className
83
+ ) }
84
+ additionalMetaSchemas={ [
85
+ require( 'ajv/lib/refs/json-schema-draft-04.json' ),
86
+ ] }
87
+ >
88
+ <></>
89
+ </SchemaForm>
90
+ );
91
+ }
92
+
93
+ export function PrimarySchemaFormActions( {
94
+ id,
95
+ saveLabel = __( 'Save', 'better-wp-security' ),
96
+ isSaving,
97
+ saveDisabled,
98
+ cancelLabel = __( 'Cancel', 'better-wp-security' ),
99
+ onCancel,
100
+ cancelRoute,
101
+ undoLabel = __( 'Undo Changes', 'better-wp-security' ),
102
+ undoDisabled,
103
+ onUndo,
104
+ children,
105
+ } ) {
106
+ return (
107
+ <Flex>
108
+ { onCancel && (
109
+ <FlexItem>
110
+ <Button isTertiary type="button" onClick={ onCancel }>
111
+ { cancelLabel }
112
+ </Button>
113
+ </FlexItem>
114
+ ) }
115
+
116
+ { cancelRoute && (
117
+ <FlexItem>
118
+ <Link
119
+ component={ withNavigate( Button ) }
120
+ isTertiary
121
+ type="button"
122
+ to={ cancelRoute }
123
+ >
124
+ { cancelLabel }
125
+ </Link>
126
+ </FlexItem>
127
+ ) }
128
+
129
+ <FlexSpacer />
130
+
131
+ { children }
132
+
133
+ { onUndo && (
134
+ <FlexItem>
135
+ <Button
136
+ isSecondary
137
+ disabled={ undoDisabled }
138
+ onClick={ onUndo }
139
+ >
140
+ { undoLabel }
141
+ </Button>
142
+ </FlexItem>
143
+ ) }
144
+
145
+ <FlexItem>
146
+ <Button
147
+ isPrimary
148
+ isBusy={ isSaving }
149
+ disabled={ isSaving || saveDisabled }
150
+ form={ id }
151
+ type="submit"
152
+ >
153
+ { saveLabel }
154
+ </Button>
155
+ </FlexItem>
156
+ </Flex>
157
+ );
158
+ }
core/{modules/notification-center/css → admin-pages/entries/settings/components/primary-schema-form}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/primary-schema-form/style.scss ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings .itsec-primary-schema-form.rjsf {
2
+ .itsec-rjsf-object-fieldset {
3
+ display: grid;
4
+ grid-template-columns: [label fields] minmax(0, 1fr);
5
+ grid-template-rows: auto;
6
+ grid-gap: .5rem 0;
7
+
8
+ @include break-small {
9
+ grid-template-columns: [label] 10rem [fields] minmax(0, 1fr);
10
+ }
11
+ }
12
+
13
+ .itsec-rjsf-section-description {
14
+ margin-top: 0;
15
+ grid-column: fields;
16
+ }
17
+
18
+ > .field-object > .itsec-rjsf-object-fieldset {
19
+ & > .itsec-rjsf-title-field,
20
+ & > .itsec-rjsf-section-title {
21
+ grid-column: label;
22
+ font-size: 1rem;
23
+ padding-right: 1rem;
24
+ margin-bottom: 0;
25
+
26
+ &:not(:first-child) {
27
+ border-top: 1px solid $border-color;
28
+ margin-top: .5rem;
29
+ padding-top: 1rem;
30
+
31
+ @include break-small {
32
+ & + * {
33
+ border-top: 1px solid $border-color;
34
+ margin-top: .5rem;
35
+ padding-top: 1rem;
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ & > .form-group:not(.field-object) {
42
+ grid-column: label / fields-end;
43
+ }
44
+
45
+ & > .itsec-rjsf-section-title ~ .form-group {
46
+ grid-column: fields;
47
+ }
48
+
49
+ & > .field-object {
50
+ grid-column: label / fields-end;
51
+ }
52
+
53
+ & > .field-description {
54
+ margin-top: 0;
55
+ }
56
+ }
57
+
58
+ .itsec-rjsf-object-fieldset > * > .itsec-rjsf-object-fieldset {
59
+ & > .itsec-rjsf-title-field {
60
+ grid-column: label;
61
+ font-size: 1rem;
62
+ padding-right: 1rem;
63
+ }
64
+
65
+ & > .form-group {
66
+ grid-column: fields;
67
+ }
68
+
69
+ & > .field-description {
70
+ grid-column: fields;
71
+ margin-top: 0;
72
+ }
73
+ }
74
+
75
+ .components-base-control__label,
76
+ .components-input-control__label,
77
+ label,
78
+ caption,
79
+ legend {
80
+ color: $dark-text;
81
+ }
82
+
83
+ .itsec-highlighted-search-result {
84
+ .components-base-control__label,
85
+ .components-input-control__label,
86
+ label,
87
+ caption {
88
+ border-bottom: 3px solid $main-blue;
89
+ padding-bottom: 3px;
90
+ margin-bottom: 6px;
91
+ }
92
+
93
+ .components-base-control__field {
94
+ margin-bottom: 12px;
95
+ }
96
+ }
97
+
98
+ .field-object:not(:first-child):not(:empty) {
99
+ border-top: 1px solid $border-color;
100
+ margin-top: 1rem;
101
+ padding-top: 1rem;
102
+ }
103
+
104
+ .field-object:empty {
105
+ display: none;
106
+ }
107
+
108
+ .itsec-rjsf-title-field + .field-object:not(:first-child),
109
+ .itsec-rjsf-section-description + .field-object:not(:first-child) {
110
+ border-top: none;
111
+ margin-top: 0;
112
+ padding-top: 0;
113
+ }
114
+ }
115
+
core/admin-pages/entries/settings/components/selectable-card/index.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Link } from 'react-router-dom';
5
+ import classnames from 'classnames';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { Card, CardBody, Icon } from '@wordpress/components';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import './style.scss';
16
+
17
+ export default function SelectableCard( {
18
+ to,
19
+ onClick,
20
+ title,
21
+ description,
22
+ icon,
23
+ fillIcon,
24
+ recommended,
25
+ direction = 'horizontal',
26
+ className: userClassName,
27
+ } ) {
28
+ const className = classnames(
29
+ 'itsec-selectable-card',
30
+ `itsec-selectable-card--${ direction }`,
31
+ userClassName,
32
+ {
33
+ 'itsec-selectable-card--fill-icon': fillIcon,
34
+ 'itsec-selectable-card--recommended': recommended,
35
+ }
36
+ );
37
+
38
+ const card = (
39
+ <Card>
40
+ <CardBody>
41
+ <div className="itsec-selectable-card__content">
42
+ <Icon icon={ icon } />
43
+ <div className="itsec-selectable-card__text">
44
+ <h4 className="itsec-selectable-card__title">
45
+ { title }
46
+ </h4>
47
+ <p className="itsec-selectable-card__description">
48
+ { description }
49
+ </p>
50
+ </div>
51
+ </div>
52
+ </CardBody>
53
+ </Card>
54
+ );
55
+
56
+ if ( to ) {
57
+ return (
58
+ <Link to={ to } className={ className }>
59
+ { card }
60
+ </Link>
61
+ );
62
+ }
63
+
64
+ if ( onClick ) {
65
+ return (
66
+ <button
67
+ aria-label={ title }
68
+ type="button"
69
+ onClick={ onClick }
70
+ className={ className }
71
+ >
72
+ { card }
73
+ </button>
74
+ );
75
+ }
76
+
77
+ return card;
78
+ }
core/{modules/password-requirements/css → admin-pages/entries/settings/components/selectable-card}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/selectable-card/style.scss ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-selectable-card {
2
+ .components-card {
3
+ position: relative;
4
+ }
5
+
6
+ &:hover,
7
+ &:focus {
8
+ .components-card {
9
+ box-shadow: 0 0 0 2px $main-blue;
10
+ background-color: $focused-blue;
11
+ }
12
+
13
+ .dashicon {
14
+ color: $main-blue;
15
+ }
16
+
17
+ .itsec-selectable-card__content::after {
18
+ visibility: visible;
19
+ }
20
+ }
21
+
22
+ .components-card__body {
23
+ height: 100%;
24
+ }
25
+
26
+ .itsec-selectable-card__content {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: stretch;
30
+ height: 100%;
31
+ }
32
+
33
+ .itsec-selectable-card__content::after {
34
+ @include dashicon("\f344");
35
+ }
36
+ }
37
+
38
+ a.itsec-selectable-card {
39
+ text-decoration: none;
40
+ color: $main-blue;
41
+
42
+ &:focus {
43
+ box-shadow: none;
44
+ outline: none;
45
+ }
46
+ }
47
+
48
+ button.itsec-selectable-card {
49
+ text-decoration: none;
50
+ margin: 0;
51
+ border: 0;
52
+ padding: 0;
53
+ cursor: pointer;
54
+ -webkit-appearance: none;
55
+ background: none;
56
+ color: $main-blue;
57
+
58
+ &:hover, &:focus {
59
+ color: $main-blue;
60
+ }
61
+ }
62
+
63
+ .itsec-selectable-card .dashicon {
64
+ --icon-size: 55px;
65
+
66
+ width: var(--icon-size);
67
+ height: var(--icon-size);
68
+ font-size: var(--icon-size);
69
+ line-height: 1;
70
+ }
71
+
72
+ .itsec-selectable-card.itsec-selectable-card--fill-icon .dashicon {
73
+ --icon-size: calc(55px - (8px * 2));
74
+ padding: 8px;
75
+ border-radius: 8px;
76
+ background: $main-blue;
77
+ color: $white;
78
+ box-sizing: content-box;
79
+ }
80
+
81
+ .itsec-selectable-card--recommended .components-card::before {
82
+ position: absolute;
83
+ right: 8px;
84
+ top: 8px;
85
+ @include recommended-icon;
86
+ }
87
+
88
+ .itsec-selectable-card--recommended.itsec-selectable-card--vertical .components-card::before {
89
+ --itsec-recommended-icon-size: 30px;
90
+ }
91
+
92
+ .itsec-selectable-card .itsec-selectable-card__title {
93
+ color: $dark-text;
94
+ margin-top: 0;
95
+ }
96
+
97
+ .itsec-selectable-card__description {
98
+ color: $medium-text;
99
+ margin: .5rem 0 0 0;
100
+ }
101
+
102
+ .itsec-selectable-card--horizontal .itsec-selectable-card__content {
103
+ .dashicon {
104
+ margin-right: 8px;
105
+ }
106
+
107
+ .itsec-selectable-card__text {
108
+ height: 100%;
109
+ flex-direction: column;
110
+ padding-right: 8px;
111
+ margin-right: auto;
112
+ }
113
+ }
114
+
115
+ .itsec-selectable-card--vertical .itsec-selectable-card__content {
116
+ flex-direction: column;
117
+ text-align: center;
118
+
119
+ .dashicon {
120
+ margin-bottom: 12px;
121
+
122
+ &:last-child {
123
+ margin-bottom: 0;
124
+ }
125
+ }
126
+
127
+ &::after {
128
+ margin-top: 12px;
129
+ }
130
+ }
core/admin-pages/entries/settings/components/sidebar/index.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Link, useParams } from 'react-router-dom';
5
+ import classnames from 'classnames';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { Button } from '@wordpress/components';
11
+ import { __ } from '@wordpress/i18n';
12
+ import { useState } from '@wordpress/element';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { useFocusOutside } from '@ithemes/security-hocs';
18
+ import { Logo } from '../';
19
+ import './style.scss';
20
+
21
+ export default function Sidebar( { className, logo = 'color', children } ) {
22
+ const { root } = useParams();
23
+ const [ expanded, setExpanded ] = useState( false );
24
+
25
+ return (
26
+ <div
27
+ { ...useFocusOutside( () => expanded && setExpanded( false ) ) }
28
+ tabIndex={ -1 }
29
+ className={ classnames(
30
+ 'itsec-settings-sidebar',
31
+ className,
32
+ `itsec-settings-sidebar--root-${ root }`,
33
+ {
34
+ 'itsec-settings-sidebar--expanded': expanded,
35
+ }
36
+ ) }
37
+ >
38
+ <div className="itsec-settings-sidebar__inner">
39
+ <Link to="/" className="itsec-settings-sidebar__logo">
40
+ <Logo style={ logo } />
41
+ </Link>
42
+ <Button
43
+ icon="menu-alt2"
44
+ label={ __( 'Toggle Sidebar', 'better-wp-security' ) }
45
+ className="itsec-settings-sidebar__toggle"
46
+ showTooltip={ false }
47
+ isPressed={ expanded }
48
+ onClick={ ( e ) => {
49
+ e.currentTarget.focus();
50
+ setExpanded( ! expanded );
51
+ } }
52
+ aria-expanded={ expanded }
53
+ />
54
+ { children }
55
+ </div>
56
+ </div>
57
+ );
58
+ }
core/{modules/password-requirements/js → admin-pages/entries/settings/components/sidebar}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/sidebar/style.scss ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings-sidebar {
2
+ width: 14rem;
3
+ flex-shrink: 0;
4
+ overflow: auto;
5
+ background: white;
6
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
7
+ z-index: 2;
8
+ padding: 1rem;
9
+ position: absolute;
10
+ top: 0;
11
+ left: 0;
12
+ bottom: 0;
13
+
14
+ @include break-large {
15
+ position: relative;
16
+ }
17
+
18
+ @include break-xlarge {
19
+ width: 16rem;
20
+ }
21
+
22
+ @include break-wide {
23
+ width: 18rem;
24
+ }
25
+
26
+ .itsec-settings-sidebar__inner {
27
+ display: flex;
28
+ flex-direction: column;
29
+ max-height: calc(100vh - 46px - 2rem);
30
+ height: 100%;
31
+
32
+ @include break-small {
33
+ max-height: none;
34
+ }
35
+ }
36
+ }
37
+
38
+ .itsec-settings-sidebar h1 {
39
+ margin-top: 0;
40
+ }
41
+
42
+ .itsec-settings-sidebar__toggle.components-button.has-icon {
43
+ align-self: flex-start;
44
+ padding: 0;
45
+ --icon-size: 30px;
46
+
47
+ min-width: var(--icon-size);
48
+ width: var(--icon-size); // Nav icon size.
49
+ min-height: var(--icon-size);
50
+ height: var(--icon-size);
51
+
52
+ @include break-large {
53
+ display: none;
54
+ }
55
+
56
+ .dashicon {
57
+ // Fix for WordPress 5.7 styling
58
+ margin-right: 2px;
59
+ }
60
+ }
61
+
62
+ @media (max-width: $break-large - 1) {
63
+ .itsec-settings-sidebar__logo {
64
+ display: none;
65
+ }
66
+
67
+ .itsec-settings-sidebar:not(.itsec-settings-sidebar--expanded) {
68
+ transition: width 200ms;
69
+ // Icon size plus padding
70
+ width: calc(30px + 2rem);
71
+
72
+ .itsec-nav {
73
+ .itsec-nav__item-title {
74
+ .itsec-nav__item-title-text {
75
+ display: none;
76
+ }
77
+
78
+ .dashicon {
79
+ margin-right: 0;
80
+ }
81
+ }
82
+
83
+ .itsec-nav__children {
84
+ display: none;
85
+ }
86
+ }
87
+
88
+ .itsec-settings-advanced-nav {
89
+ align-self: initial;
90
+ justify-content: flex-start;
91
+ flex-direction: column;
92
+
93
+ li:not(:last-child) {
94
+ border-right: none;
95
+ margin-right: 0;
96
+ padding-right: 0;
97
+ }
98
+
99
+ a {
100
+ &::before {
101
+ margin-right: 0;
102
+ }
103
+
104
+ span {
105
+ display: none;
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ @media (max-width: $break-medium) {
113
+ .itsec-settings-sidebar {
114
+ .itsec-settings-sidebar__toggle.components-button.has-icon {
115
+ --icon-size: 20px;
116
+ }
117
+
118
+ &:not(.itsec-settings-sidebar--expanded) {
119
+ box-shadow: none;
120
+ transition: none;
121
+ width: calc(20px + 2rem);
122
+ height: calc(40px + 1rem);
123
+ clip-path: inset(0 0 -6px 0);
124
+
125
+ .itsec-settings-sidebar__inner > :not(.itsec-settings-sidebar__toggle) {
126
+ display: none;
127
+ }
128
+ }
129
+ }
130
+ }
core/admin-pages/entries/settings/components/toolbar/index.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useParams } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress components.
8
+ */
9
+ import {
10
+ createSlotFill,
11
+ Popover,
12
+ Toolbar,
13
+ ToolbarButton,
14
+ } from '@wordpress/components';
15
+ import { __ } from '@wordpress/i18n';
16
+ import { useMediaQuery, useFocusOnMount } from '@wordpress/compose';
17
+ import { useState } from '@wordpress/element';
18
+
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ import { Search } from '@ithemes/security-search';
23
+ import { useGlobalNavigationUrl } from '@ithemes/security-utils';
24
+ import './style.scss';
25
+
26
+ const { Slot: ToolbarSlot, Fill: ToolbarFill } = createSlotFill( 'Toolbar' );
27
+
28
+ export { ToolbarFill };
29
+
30
+ export default function () {
31
+ const { root } = useParams();
32
+ const dashboardUrl = useGlobalNavigationUrl( 'dashboard' );
33
+ const isSmall = useMediaQuery( '(max-width: 600px)' );
34
+ const [ isSearchOpen, setIsSearchOpen ] = useState( false );
35
+ const focusSearchRef = useFocusOnMount();
36
+
37
+ return (
38
+ <div
39
+ role="region"
40
+ aria-label={ __( 'Toolbar', 'better-wp-security' ) }
41
+ className="itsec-settings-toolbar"
42
+ >
43
+ { root !== 'onboard' && ! isSmall && <Search /> }
44
+
45
+ <Toolbar label={ __( 'Toolbar Actions', 'better-wp-security' ) }>
46
+ { root !== 'onboard' && (
47
+ <>
48
+ { isSmall && (
49
+ <>
50
+ <ToolbarButton
51
+ icon="search"
52
+ text={ __( 'Search', 'better-wp-security' ) }
53
+ aria-expanded={ isSearchOpen }
54
+ onClick={ () =>
55
+ setIsSearchOpen( ! isSearchOpen )
56
+ }
57
+ />
58
+ { isSearchOpen && (
59
+ <Popover
60
+ className="itsec-settings-search__popover"
61
+ expandOnMobile
62
+ headerTitle={ __( 'Search', 'better-wp-security' ) }
63
+ focusOnMount="container"
64
+ onClose={ () =>
65
+ setIsSearchOpen( false )
66
+ }
67
+ onFocusOutside={ () => {} }
68
+ >
69
+ <Search
70
+ showResults
71
+ ref={ focusSearchRef }
72
+ onPick={ () =>
73
+ setIsSearchOpen( false )
74
+ }
75
+ />
76
+ </Popover>
77
+ ) }
78
+ </>
79
+ ) }
80
+ <ToolbarButton
81
+ icon="layout"
82
+ href={ dashboardUrl }
83
+ text={ __( 'Dashboard', 'better-wp-security' ) }
84
+ />
85
+ </>
86
+ ) }
87
+ <ToolbarSlot />
88
+ </Toolbar>
89
+ </div>
90
+ );
91
+ }
core/{modules/pro → admin-pages/entries/settings/components/toolbar}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/components/toolbar/style.scss ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings-toolbar {
2
+ background: white;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: flex-end;
6
+ padding: .5rem 1.5rem;
7
+ min-height: calc(30px + 1rem);
8
+
9
+ .components-accessible-toolbar {
10
+ border: none;
11
+ max-width: 100%;
12
+
13
+ a.components-button:focus {
14
+ outline: none;
15
+ box-shadow: none;
16
+ }
17
+
18
+ .components-button.has-icon::before,
19
+ .components-toolbar .components-button.has-icon::before {
20
+ left: 6px;
21
+ right: 2px;
22
+ }
23
+ }
24
+
25
+ .components-button {
26
+ height: auto;
27
+ font-size: 1rem;
28
+ padding-top: 0;
29
+ padding-bottom: 0;
30
+ }
31
+
32
+ .itsec-search {
33
+ margin-right: auto;
34
+ flex-grow: 1;
35
+ }
36
+
37
+ @media screen and (max-width: $break-medium) {
38
+ .itsec-settings-toolbar {
39
+ min-height: calc(40px + 1rem);
40
+ }
41
+ }
42
+
43
+ @media screen and (max-width: $break-mobile) {
44
+ padding-top: 1rem;
45
+ padding-bottom: 1rem;
46
+
47
+ .components-accessible-toolbar {
48
+ width: 100%;
49
+ display: grid;
50
+ grid-template-columns: 1fr 1fr;
51
+ justify-items: start;
52
+ grid-gap: 1rem;
53
+ }
54
+ }
55
+ }
56
+
57
+ .itsec-settings-search__popover.components-popover.is-expanded .components-popover__content {
58
+ padding: 1rem;
59
+ overflow-y: scroll;
60
+
61
+ .components-base-control__field {
62
+ max-width: none;
63
+ }
64
+
65
+ .itsec-search__results {
66
+ position: relative;
67
+ padding-top: 0;
68
+ margin-bottom: 1rem;
69
+ width: 100%;
70
+ max-width: none;
71
+ box-shadow: none;
72
+ max-height: none;
73
+
74
+ :first-child {
75
+ margin-top: 0;
76
+
77
+ :first-child {
78
+ border-top: none;
79
+ }
80
+ }
81
+ }
82
+ }
core/admin-pages/entries/settings/history.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { createBrowserHistory, parsePath } from 'history';
5
+ import { parse, stringify } from 'query-string';
6
+ import { omit } from 'lodash';
7
+
8
+ /**
9
+ * Recreate `history` to coerce React Router into accepting path arguments found in query
10
+ * parameter `path`, allowing a url hash to be avoided. Since hash portions of the url are
11
+ * not sent server side, full route information can be detected by the server.
12
+ *
13
+ * `<Router />` and `<Switch />` components use `history.location()` to match a url with a route.
14
+ * Since they don't parse query arguments, recreate `get location` to return a `pathname` with the
15
+ * query path argument's value.
16
+ *
17
+ * @param {Location} initialLocation The initial document.location.
18
+ * @param {Object} fixedQuery The query parameters that should be fixed.
19
+ *
20
+ * @return {Object} React-router history object with `get location` modified.
21
+ */
22
+ export function createHistory( initialLocation, fixedQuery ) {
23
+ const browserHistory = createBrowserHistory();
24
+
25
+ const getNextTo = ( to ) => {
26
+ const parsed = typeof to === 'string' ? parsePath( to ) : to;
27
+ const search = parse( parsed.search?.substring( 1 ) ) || {};
28
+
29
+ return {
30
+ ...parsed,
31
+ pathname: initialLocation.pathname,
32
+ search:
33
+ '?' +
34
+ stringify( {
35
+ ...search,
36
+ path: parsed.pathname,
37
+ ...fixedQuery,
38
+ } ),
39
+ };
40
+ };
41
+
42
+ return {
43
+ get length() {
44
+ return browserHistory.length;
45
+ },
46
+ get action() {
47
+ return browserHistory.action;
48
+ },
49
+ get location() {
50
+ const query = parse(
51
+ browserHistory.location.search.substring( 1 )
52
+ );
53
+ const pathname = query.path || '/';
54
+
55
+ return {
56
+ ...browserHistory.location,
57
+ pathname,
58
+ search:
59
+ '?' +
60
+ stringify(
61
+ omit( query, [ 'path', Object.keys( fixedQuery ) ] )
62
+ ),
63
+ };
64
+ },
65
+ createHref: ( location ) => {
66
+ return browserHistory.createHref( getNextTo( location ) );
67
+ },
68
+ push: ( to, state ) => {
69
+ browserHistory.push( getNextTo( to ), state );
70
+ },
71
+ replace: ( to, state ) => {
72
+ browserHistory.replace( getNextTo( to ), state );
73
+ },
74
+ go: ( ...args ) => browserHistory.go.apply( browserHistory, args ),
75
+ goBack: ( ...args ) =>
76
+ browserHistory.goBack.apply( browserHistory, args ),
77
+ goForward: ( ...args ) =>
78
+ browserHistory.goForward.apply( browserHistory, args ),
79
+ block: ( ...args ) =>
80
+ browserHistory.block.apply( browserHistory, args ),
81
+ listen( listener ) {
82
+ return browserHistory.listen( () => {
83
+ listener( this.location, this.action );
84
+ } );
85
+ },
86
+ };
87
+ }
core/{modules/wordpress-tweaks/js/blankshield → admin-pages/entries/settings}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/page-registration.js ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { sortBy, omit, flatMap } from 'lodash';
5
+ import { useHistory, useParams } from 'react-router-dom';
6
+ import { createLocation } from 'history';
7
+ import useDeepCompareEffect from 'use-deep-compare-effect';
8
+
9
+ /**
10
+ * WordPress dependencies
11
+ */
12
+ import {
13
+ createContext,
14
+ useCallback,
15
+ useContext,
16
+ useEffect,
17
+ useState,
18
+ useMemo,
19
+ } from '@wordpress/element';
20
+ import { useInstanceId } from '@wordpress/compose';
21
+
22
+ const Context = createContext( {
23
+ pages: [],
24
+ childPages: {},
25
+ addPage: () => {},
26
+ removePage: () => {},
27
+ addChildPages: () => {},
28
+ removeChildPages: () => {},
29
+ } );
30
+ Context.displayName = 'PageRegistration';
31
+
32
+ export { Context };
33
+
34
+ export default function PageRegistration( { children } ) {
35
+ const [ pages, setPages ] = useState( [] );
36
+ const [ childPages, setChildPages ] = useState( {} );
37
+
38
+ const addPage = useCallback( ( page ) => {
39
+ setPages( ( latestPages ) => {
40
+ const i = latestPages.findIndex(
41
+ ( maybe ) => maybe.id === page.id
42
+ );
43
+ let next;
44
+
45
+ if ( i === -1 ) {
46
+ next = [ ...latestPages, page ];
47
+ } else {
48
+ next = [ ...latestPages ];
49
+ next[ i ] = page;
50
+ }
51
+
52
+ return sortBy( next, 'priority' );
53
+ } );
54
+ }, [] );
55
+
56
+ const removePage = useCallback( ( id ) => {
57
+ setPages( ( latestPages ) =>
58
+ latestPages.filter( ( page ) => page.id !== id )
59
+ );
60
+ }, [] );
61
+
62
+ const addChildPages = useCallback( ( id, newChildPages ) => {
63
+ setChildPages( ( latestPages ) => ( {
64
+ ...latestPages,
65
+ [ id ]: newChildPages,
66
+ } ) );
67
+ } );
68
+ const removeChildPages = useCallback( ( id ) => {
69
+ setChildPages( ( latestPages ) => omit( latestPages, id ) );
70
+ } );
71
+
72
+ return (
73
+ <Context.Provider
74
+ value={ {
75
+ pages,
76
+ childPages,
77
+ addPage,
78
+ removePage,
79
+ addChildPages,
80
+ removeChildPages,
81
+ } }
82
+ >
83
+ { children }
84
+ </Context.Provider>
85
+ );
86
+ }
87
+
88
+ export function Page( {
89
+ id,
90
+ title,
91
+ icon,
92
+ roots = [ 'settings' ],
93
+ priority = 90,
94
+ ignore,
95
+ children,
96
+ } ) {
97
+ const context = useContext( Context );
98
+
99
+ useEffect( () => {
100
+ context.addPage( {
101
+ id,
102
+ title,
103
+ icon,
104
+ roots,
105
+ priority,
106
+ ignore,
107
+ render: children,
108
+ } );
109
+
110
+ return () => {
111
+ context.removePage( id );
112
+ };
113
+ }, [ id, title ] );
114
+
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Register child pages.
120
+ *
121
+ * @param {Object} props Props.
122
+ * @param {Array<{title, id, to}>} props.pages The pages to register.
123
+ * @return {null} No component rendered.
124
+ */
125
+ export function ChildPages( props ) {
126
+ const { pages } = props;
127
+ const context = useContext( Context );
128
+ const id = useInstanceId( ChildPages, '' );
129
+
130
+ useDeepCompareEffect( () => {
131
+ context.addChildPages( id, pages );
132
+ return () => {
133
+ context.removeChildPages( id );
134
+ };
135
+ }, [ pages ] );
136
+
137
+ return null;
138
+ }
139
+
140
+ export function usePages( root ) {
141
+ const { root: matchedRoot } = useParams();
142
+ const { pages } = useContext( Context );
143
+
144
+ return pages.filter( ( page ) =>
145
+ page.roots.includes( root || matchedRoot )
146
+ );
147
+ }
148
+
149
+ export function useCurrentPage() {
150
+ const pages = usePages();
151
+ const { page: currentPage } = useParams();
152
+
153
+ return pages.find( ( page ) => page.id === currentPage );
154
+ }
155
+
156
+ export function useCurrentChildPages() {
157
+ const { childPages } = useContext( Context );
158
+
159
+ return useMemo( () => flatMap( childPages ) );
160
+ }
161
+
162
+ export function useNextPage( currentPage ) {
163
+ const pages = usePages();
164
+
165
+ if ( ! pages.length ) {
166
+ return undefined;
167
+ }
168
+
169
+ if ( ! currentPage ) {
170
+ return pages[ 0 ];
171
+ }
172
+
173
+ const index = pages.findIndex( ( page ) => page.id === currentPage );
174
+
175
+ return pages[ index + 1 ]?.id;
176
+ }
177
+
178
+ /**
179
+ * Gets navigation helpers based on the current page.
180
+ *
181
+ * @param {Array<string>} [tabs] Ordered list of tab ids.
182
+ * @return {Object} An object with slugs and goto functions for the previous and next pages.
183
+ */
184
+ export function useNavigation( tabs ) {
185
+ const {
186
+ root: base,
187
+ page: currentPage,
188
+ child: currentChildPage,
189
+ tab: currentTab,
190
+ } = useParams();
191
+ const nextPage = useNextPage( currentPage );
192
+ const childPages = useCurrentChildPages().map(
193
+ ( childPage ) => childPage.id
194
+ );
195
+ const history = useHistory();
196
+
197
+ let previous, next;
198
+
199
+ if ( tabs ) {
200
+ let prevTab, nextTab;
201
+
202
+ for ( let i = 0; i < tabs.length; i++ ) {
203
+ if ( tabs[ i ] === currentTab ) {
204
+ prevTab = tabs[ i - 1 ];
205
+ nextTab = tabs[ i + 1 ];
206
+ break;
207
+ }
208
+ }
209
+
210
+ previous =
211
+ prevTab &&
212
+ `/${ base }/${ currentPage }/${ currentChildPage }/${ prevTab }`;
213
+ next =
214
+ nextTab &&
215
+ `/${ base }/${ currentPage }/${ currentChildPage }/${ nextTab }`;
216
+ }
217
+
218
+ if ( ( ! previous || ! next ) && childPages ) {
219
+ let prevChild, nextChild;
220
+
221
+ for ( let i = 0; i < childPages.length; i++ ) {
222
+ if ( childPages[ i ] === currentChildPage ) {
223
+ prevChild = childPages[ i - 1 ];
224
+ nextChild = childPages[ i + 1 ];
225
+ break;
226
+ }
227
+ }
228
+
229
+ previous =
230
+ previous ||
231
+ ( prevChild && `/${ base }/${ currentPage }/${ prevChild }` );
232
+ next =
233
+ next ||
234
+ ( nextChild && `/${ base }/${ currentPage }/${ nextChild }` );
235
+ }
236
+
237
+ if ( ! next && nextPage ) {
238
+ next = `/${ base }/${ nextPage }`;
239
+ }
240
+
241
+ return {
242
+ previous,
243
+ goPrevious() {
244
+ if ( previous ) {
245
+ history.push( createLocation( previous ) );
246
+ }
247
+ },
248
+ next,
249
+ goNext() {
250
+ if ( next ) {
251
+ history.push( createLocation( next ) );
252
+ }
253
+ },
254
+ nextPage: nextPage && `/${ base }/${ nextPage }`,
255
+ goNextPage() {
256
+ if ( nextPage ) {
257
+ history.push( createLocation( `/${ base }/${ nextPage }` ) );
258
+ }
259
+ },
260
+ };
261
+ }
core/admin-pages/entries/settings/pages/configure/index.js ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ Redirect,
6
+ Route,
7
+ Switch,
8
+ useParams,
9
+ useRouteMatch,
10
+ useLocation,
11
+ generatePath,
12
+ Link,
13
+ } from 'react-router-dom';
14
+ import { isEmpty, every, cloneDeep, size } from 'lodash';
15
+
16
+ /**
17
+ * WordPress dependencies
18
+ */
19
+ import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
20
+ import { __, sprintf } from '@wordpress/i18n';
21
+ import { useMemo, useState } from '@wordpress/element';
22
+ import { Card, CardHeader, CardBody, Button } from '@wordpress/components';
23
+ import { useInstanceId } from '@wordpress/compose';
24
+
25
+ /**
26
+ * Internal dependencies
27
+ */
28
+ import {
29
+ ControlledTabPanel,
30
+ ErrorList,
31
+ HelpList,
32
+ } from '@ithemes/security-components';
33
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
34
+ import {
35
+ PageHeader,
36
+ PrimarySchemaFormInputs,
37
+ PrimarySchemaFormActions,
38
+ HelpFill,
39
+ SelectableCard,
40
+ Breadcrumbs,
41
+ useHelpBreadcrumbTrail,
42
+ } from '../../components';
43
+ import {
44
+ useConfigContext,
45
+ useNavigateTo,
46
+ useSettingsForm,
47
+ makeConditionalSettingsSchema,
48
+ getModuleTypes,
49
+ appendClassNameAtPath,
50
+ validateModuleRequirements,
51
+ } from '../../utils';
52
+ import { useNavigation, ChildPages } from '../../page-registration';
53
+ import './style.scss';
54
+ import { withNavigate } from '@ithemes/security-hocs';
55
+
56
+ function useTypes() {
57
+ const { root } = useParams();
58
+ const { serverType, installType } = useConfigContext();
59
+ const registry = useRegistry();
60
+ const { editedModules, activeModules } = useSelect( ( select ) => ( {
61
+ editedModules: select( MODULES_STORE_NAME ).getEditedModules(),
62
+ activeModules: select( MODULES_STORE_NAME ).getActiveModules(),
63
+ } ) );
64
+
65
+ const getModules = () =>
66
+ editedModules.filter( ( module ) => {
67
+ if ( module.status.selected !== 'active' ) {
68
+ return false;
69
+ }
70
+
71
+ if ( ! module.settings?.interactive.length ) {
72
+ return false;
73
+ }
74
+
75
+ if ( root === 'onboard' && ! module.settings?.onboard.length ) {
76
+ return false;
77
+ }
78
+
79
+ if ( validateModuleRequirements( module, 'run' ).hasErrors() ) {
80
+ return false;
81
+ }
82
+
83
+ if ( module.settings?.conditional ) {
84
+ const schema = makeConditionalSettingsSchema( module, {
85
+ serverType,
86
+ installType,
87
+ activeModules,
88
+ registry,
89
+ settings: registry
90
+ .select( MODULES_STORE_NAME )
91
+ .getEditedSettings( module.id ),
92
+ } );
93
+
94
+ if ( isEmpty( schema.properties ) ) {
95
+ return false;
96
+ }
97
+
98
+ const allEmpty = every(
99
+ schema.properties,
100
+ ( propSchema ) =>
101
+ propSchema.type === 'object' &&
102
+ isEmpty( propSchema.properties )
103
+ );
104
+
105
+ if ( allEmpty ) {
106
+ return false;
107
+ }
108
+
109
+ if (
110
+ root === 'onboard' &&
111
+ ! module.settings.onboard.some(
112
+ ( setting ) => !! schema.properties[ setting ]
113
+ )
114
+ ) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ return true;
120
+ } );
121
+
122
+ const modules = getModules();
123
+ const types = getModuleTypes()
124
+ .map( ( type ) => ( {
125
+ ...type,
126
+ modules: modules.filter( ( module ) => module.type === type.slug ),
127
+ } ) )
128
+ .filter( ( type ) => type.modules.length > 0 );
129
+
130
+ return { types, modules };
131
+ }
132
+
133
+ export default function Configure() {
134
+ const {
135
+ url,
136
+ path,
137
+ isExact,
138
+ params: { root },
139
+ } = useRouteMatch();
140
+ const { types, modules } = useTypes();
141
+ const recommended = modules.filter(
142
+ ( module ) => module.type === 'recommended'
143
+ );
144
+ const recommendedIds = recommended
145
+ .map( ( module ) => module.id )
146
+ .join( '|' );
147
+ const nav = [
148
+ ...recommended.map( ( module ) => ( {
149
+ slug: module.id,
150
+ label: module.title,
151
+ } ) ),
152
+ ...types,
153
+ ];
154
+
155
+ return (
156
+ <>
157
+ { ! isExact && (
158
+ <ChildPages
159
+ pages={ nav
160
+ .filter( ( { slug } ) => slug !== 'advanced' )
161
+ .map( ( { slug, label } ) => ( {
162
+ title: label,
163
+ to: `${ url }/${ slug }`,
164
+ id: slug,
165
+ } ) ) }
166
+ />
167
+ ) }
168
+
169
+ <Switch>
170
+ <Route
171
+ path={ `${ path }/:child(${ recommendedIds })` }
172
+ render={ ( { match } ) => {
173
+ const module = modules.find(
174
+ ( maybe ) => maybe.id === match.params.child
175
+ );
176
+
177
+ if ( ! module ) {
178
+ return null;
179
+ }
180
+
181
+ return <ModulePage module={ module } />;
182
+ } }
183
+ />
184
+
185
+ <Route
186
+ path={ [ `${ path }/:child/:tab`, `${ path }/:child` ] }
187
+ render={ ( { match } ) => {
188
+ const activeType = types.find(
189
+ ( type ) => type.slug === match.params.child
190
+ );
191
+
192
+ if ( ! activeType ) {
193
+ return null;
194
+ }
195
+
196
+ return <TabPanel modules={ activeType.modules } />;
197
+ } }
198
+ />
199
+
200
+ <Route path={ path }>
201
+ { nav.length > 0 &&
202
+ ( root === 'onboard' ? (
203
+ <Intro to={ `${ url }/${ nav[ 0 ].slug }` } />
204
+ ) : (
205
+ <Redirect to={ `${ url }/${ nav[ 0 ].slug }` } />
206
+ ) ) }
207
+ </Route>
208
+ </Switch>
209
+ </>
210
+ );
211
+ }
212
+
213
+ function Intro( { to } ) {
214
+ return (
215
+ <>
216
+ <PageHeader
217
+ title={ __( 'Configure', 'better-wp-security' ) }
218
+ subtitle={ __(
219
+ 'Based on the Security Features you’ve enabled while settings up iThemes Security, we’ve selected the most important settings for you to configure.',
220
+ 'better-wp-security'
221
+ ) }
222
+ />
223
+
224
+ <div className="itsec-configure-intro">
225
+ <SelectableCard
226
+ title={ __( 'Recommended', 'better-wp-security' ) }
227
+ description={ __( 'Configure Site', 'better-wp-security' ) }
228
+ icon="star-filled"
229
+ fillIcon
230
+ to={ to }
231
+ direction="vertical"
232
+ />
233
+ </div>
234
+ </>
235
+ );
236
+ }
237
+
238
+ function TabPanel( { modules } ) {
239
+ const { url, path } = useRouteMatch();
240
+ const { child: type, tab: moduleId, root, ...params } = useParams();
241
+ const navigateTo = useNavigateTo();
242
+ const tabs = useMemo(
243
+ () =>
244
+ modules.map( ( module ) => ( {
245
+ name: module.id,
246
+ title: module.title,
247
+ module,
248
+ } ) ),
249
+ [ type, modules ]
250
+ );
251
+
252
+ const activeModule = modules.find( ( module ) => module.id === moduleId );
253
+
254
+ if ( ! activeModule ) {
255
+ const first = modules.find( ( module ) => module.type === type );
256
+
257
+ return <Redirect to={ first ? `${ url }/${ first.id }` : url } />;
258
+ }
259
+
260
+ const onSelect = ( selected ) =>
261
+ navigateTo(
262
+ generatePath( path, {
263
+ ...params,
264
+ root,
265
+ child: type,
266
+ tab: selected,
267
+ } )
268
+ );
269
+
270
+ return (
271
+ <ModulePage
272
+ module={ activeModule }
273
+ tabs={ tabs }
274
+ onSelect={ onSelect }
275
+ />
276
+ );
277
+ }
278
+
279
+ function ModulePage( { module, tabs, onSelect } ) {
280
+ const { root } = useParams();
281
+
282
+ const Concrete =
283
+ root === 'onboard' ? ConfigureModuleOnboard : ConfigureModuleSettings;
284
+
285
+ return (
286
+ <>
287
+ <PageHeader
288
+ title={ module.title }
289
+ subtitle={ module.description }
290
+ help={ module.help }
291
+ breadcrumbs={ module.type !== 'advanced' }
292
+ />
293
+ <Concrete tabs={ tabs } module={ module } onSelect={ onSelect } />
294
+ </>
295
+ );
296
+ }
297
+
298
+ function ConfigureModuleOnboard( { tabs, module, onSelect } ) {
299
+ const { previous, goNext } = useNavigation(
300
+ tabs?.map( ( tab ) => tab.name )
301
+ );
302
+
303
+ if ( ! module ) {
304
+ return null;
305
+ }
306
+
307
+ return (
308
+ <ConfigureModule
309
+ tabs={ tabs }
310
+ module={ module }
311
+ onSelect={ onSelect }
312
+ onSave={ goNext }
313
+ saveLabel={ __( 'Next', 'better-wp-security' ) }
314
+ saveDisabled={ false }
315
+ cancelLabel={ __( 'Back', 'better-wp-security' ) }
316
+ cancelRoute={ previous }
317
+ filterFields={ ( _, setting ) =>
318
+ module.settings.onboard.includes( setting )
319
+ }
320
+ />
321
+ );
322
+ }
323
+
324
+ function ConfigureModuleSettings( { tabs, module, onSelect } ) {
325
+ const { saveSettings } = useDispatch( MODULES_STORE_NAME );
326
+
327
+ if ( ! module ) {
328
+ return null;
329
+ }
330
+
331
+ const onSave = () => saveSettings( module.id );
332
+
333
+ return (
334
+ <ConfigureModule
335
+ tabs={ tabs }
336
+ module={ module }
337
+ onSelect={ onSelect }
338
+ onSave={ onSave }
339
+ />
340
+ );
341
+ }
342
+
343
+ function ConfigureModule( {
344
+ tabs,
345
+ module,
346
+ onSelect,
347
+ onSave,
348
+ saveDisabled,
349
+ filterFields,
350
+ ...rest
351
+ } ) {
352
+ const id = useInstanceId(
353
+ ConfigureModule,
354
+ `itsec-configure-${ module.id }`
355
+ );
356
+ const { hash } = useLocation();
357
+
358
+ const { isSaving, isDirty, apiError } = useSelect(
359
+ ( select ) => ( {
360
+ isSaving: select( MODULES_STORE_NAME ).isSavingSettings(
361
+ module.id
362
+ ),
363
+ isDirty: select( MODULES_STORE_NAME ).areSettingsDirty( module.id ),
364
+ apiError: select( MODULES_STORE_NAME ).getError( module.id ),
365
+ } ),
366
+ [ module.id ]
367
+ );
368
+ const { resetSettingEdits } = useDispatch( MODULES_STORE_NAME );
369
+ const {
370
+ schema,
371
+ uiSchema: uiSchemaRaw,
372
+ formData,
373
+ setFormData,
374
+ } = useSettingsForm( module, filterFields );
375
+ const uiSchema = useMemo( () => {
376
+ if ( ! hash ) {
377
+ return uiSchemaRaw;
378
+ }
379
+
380
+ return appendClassNameAtPath(
381
+ uiSchemaRaw ? cloneDeep( uiSchemaRaw ) : {},
382
+ [ hash.substr( 1 ), 'classNames' ],
383
+ 'itsec-highlighted-search-result'
384
+ );
385
+ }, [ uiSchemaRaw, hash ] );
386
+
387
+ const [ schemaError, setSchemaError ] = useState( [] );
388
+ const formContext = useMemo(
389
+ () => ( {
390
+ module: module.id,
391
+ disableInlineErrors: true,
392
+ } ),
393
+ [ module.id ]
394
+ );
395
+
396
+ const onSubmit = ( e ) => {
397
+ setSchemaError( [] );
398
+ onSave( e );
399
+ };
400
+
401
+ if ( ! module ) {
402
+ return null;
403
+ }
404
+
405
+ const renderModule = () => (
406
+ <>
407
+ <ModuleLinks module={ module } />
408
+ <CardBody>
409
+ <ErrorList apiError={ apiError } schemaError={ schemaError } />
410
+ <PrimarySchemaFormInputs
411
+ id={ id }
412
+ onSubmit={ onSubmit }
413
+ schema={ schema }
414
+ uiSchema={ uiSchema }
415
+ formData={ formData }
416
+ onChange={ setFormData }
417
+ idPrefix={ `itsec_${ module.id }` }
418
+ formContext={ formContext }
419
+ onError={ setSchemaError }
420
+ showErrorList={ false }
421
+ />
422
+ </CardBody>
423
+ </>
424
+ );
425
+
426
+ return (
427
+ <>
428
+ <HelpPage module={ module } />
429
+ <Card>
430
+ { tabs ? (
431
+ <ControlledTabPanel
432
+ tabs={ tabs }
433
+ selected={ module.id }
434
+ onSelect={ onSelect }
435
+ isStyled
436
+ >
437
+ { renderModule }
438
+ </ControlledTabPanel>
439
+ ) : (
440
+ renderModule()
441
+ ) }
442
+ </Card>
443
+ <PrimarySchemaFormActions
444
+ id={ id }
445
+ isSaving={ isSaving }
446
+ saveDisabled={
447
+ saveDisabled === undefined ? ! isDirty : saveDisabled
448
+ }
449
+ undoDisabled={ ! isDirty }
450
+ onUndo={ () => resetSettingEdits( module.id ) }
451
+ { ...rest }
452
+ />
453
+ </>
454
+ );
455
+ }
456
+
457
+ function HelpPage( { module } ) {
458
+ return (
459
+ <HelpFill>
460
+ <PageHeader
461
+ title={ module.title }
462
+ description={ module.help }
463
+ breadcrumbs={
464
+ <Breadcrumbs
465
+ trail={ useHelpBreadcrumbTrail( module.title ) }
466
+ />
467
+ }
468
+ />
469
+ <HelpList topic={ module.id } />
470
+ </HelpFill>
471
+ );
472
+ }
473
+
474
+ function ModuleLinks( { module } ) {
475
+ const links = [];
476
+
477
+ if (
478
+ ! isEmpty( module.user_groups ) ||
479
+ module.id === 'password-requirements'
480
+ ) {
481
+ const text =
482
+ module.id === 'password-requirements'
483
+ ? __( 'User Groups', 'better-wp-security' )
484
+ : sprintf(
485
+ /* translators: 1. The number of user groups. */
486
+ __( 'User Groups (%d)', 'better-wp-security' ),
487
+ size( module.user_groups )
488
+ );
489
+
490
+ links.push(
491
+ <Link
492
+ to={ `/settings/user-groups?module=${ module.id }` }
493
+ component={ withNavigate( Button ) }
494
+ isLink
495
+ text={ text }
496
+ icon="groups"
497
+ />
498
+ );
499
+ }
500
+
501
+ if ( ! links.length ) {
502
+ return null;
503
+ }
504
+
505
+ return (
506
+ <CardHeader className="itsec-configure-module-links">
507
+ <ul>
508
+ { links.map( ( link, i ) => (
509
+ <li key={ i }>{ link }</li>
510
+ ) ) }
511
+ </ul>
512
+ </CardHeader>
513
+ );
514
+ }
core/{modules/wordpress-tweaks/js → admin-pages/entries/settings/pages/configure}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/pages/configure/style.scss ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-configure-intro {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fit, minmax(min-content, 250px));
4
+ grid-gap: 1rem;
5
+ }
6
+
7
+ .itsec-configure-module-links {
8
+ padding-top: 4px !important;
9
+ padding-bottom: 4px !important;
10
+
11
+ ul {
12
+ margin: 0;
13
+ }
14
+
15
+ li {
16
+ margin: 0;
17
+ }
18
+
19
+ .components-button.is-link {
20
+ text-decoration: none;
21
+
22
+ &:first-child {
23
+ padding-left: 0;
24
+ }
25
+ }
26
+ }
core/admin-pages/entries/settings/pages/index.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { Page } from '../page-registration';
10
+ import SiteType from './site-type';
11
+ import Modules from './modules';
12
+ import Configure from './configure';
13
+ import Tools from './tools';
14
+ import SecureSite from './secure-site';
15
+
16
+ export default function Pages() {
17
+ return (
18
+ <>
19
+ <Page
20
+ id="site-type"
21
+ title={ __( 'Site Type', 'better-wp-security' ) }
22
+ priority={ 0 }
23
+ roots={ [ 'onboard' ] }
24
+ >
25
+ { () => <SiteType /> }
26
+ </Page>
27
+ <Page
28
+ id="modules"
29
+ title={ __( 'Features', 'better-wp-security' ) }
30
+ icon="shield"
31
+ priority={ 5 }
32
+ roots={ [ 'onboard', 'settings' ] }
33
+ >
34
+ { () => <Modules /> }
35
+ </Page>
36
+ <Page
37
+ id="configure"
38
+ title={ __( 'Configure', 'better-wp-security' ) }
39
+ icon="admin-settings"
40
+ priority={ 15 }
41
+ roots={ [ 'onboard', 'settings' ] }
42
+ ignore={ [ '/advanced/' ] }
43
+ >
44
+ { () => <Configure /> }
45
+ </Page>
46
+ <Page
47
+ id="secure-site"
48
+ title={ __( 'Secure Site', 'better-wp-security' ) }
49
+ priority={ 100 }
50
+ roots={ [ 'onboard' ] }
51
+ >
52
+ { () => <SecureSite /> }
53
+ </Page>
54
+ <Page
55
+ id="tools"
56
+ title={ __( 'Tools', 'better-wp-security' ) }
57
+ priority={ false }
58
+ roots={ [ 'settings' ] }
59
+ >
60
+ { () => <Tools /> }
61
+ </Page>
62
+ </>
63
+ );
64
+ }
65
+
66
+ export { default as Onboard } from './onboard';
67
+ export { default as Settings } from './settings';
core/{packages/components/src/malware-scan-results → admin-pages/entries/settings/pages}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/pages/modules/index.js ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ useParams,
6
+ useLocation,
7
+ Link,
8
+ Route,
9
+ Switch,
10
+ useRouteMatch,
11
+ Redirect,
12
+ } from 'react-router-dom';
13
+ import { isEmpty, size } from 'lodash';
14
+ import classnames from 'classnames';
15
+
16
+ /**
17
+ * WordPress dependencies
18
+ */
19
+ import { useSelect, useDispatch } from '@wordpress/data';
20
+ import { useState } from '@wordpress/element';
21
+ import {
22
+ FormToggle,
23
+ Card,
24
+ CardBody,
25
+ Flex,
26
+ FlexItem,
27
+ Tooltip,
28
+ Button,
29
+ VisuallyHidden,
30
+ } from '@wordpress/components';
31
+ import { __, sprintf } from '@wordpress/i18n';
32
+
33
+ /**
34
+ * Internal dependencies
35
+ */
36
+ import {
37
+ ErrorList,
38
+ FlexSpacer,
39
+ ControlledTabPanel,
40
+ HelpList,
41
+ Markup,
42
+ } from '@ithemes/security-components';
43
+ import { withNavigate } from '@ithemes/security-hocs';
44
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
45
+ import {
46
+ Breadcrumbs,
47
+ HelpFill,
48
+ PageHeader,
49
+ useHelpBreadcrumbTrail,
50
+ } from '../../components';
51
+ import { useNavigation, ChildPages } from '../../page-registration';
52
+ import {
53
+ validateModuleRequirements,
54
+ getModuleTypes,
55
+ useNavigateTo,
56
+ } from '../../utils';
57
+ import './style.scss';
58
+
59
+ export default function Modules() {
60
+ const { url, path } = useRouteMatch();
61
+ const { root } = useParams();
62
+ const modules = useSelect( ( select ) =>
63
+ select( MODULES_STORE_NAME ).getEditedModules()
64
+ );
65
+ const tabs = getModuleTypes()
66
+ .map( ( { slug, label } ) => ( {
67
+ name: slug,
68
+ title: label,
69
+ modules: modules.filter(
70
+ ( module ) =>
71
+ module.type === slug &&
72
+ module.status.default !== 'always-active' &&
73
+ ( root !== 'onboard' || module.onboard ) &&
74
+ ( module.status.selected === 'active' ||
75
+ ! validateModuleRequirements(
76
+ module,
77
+ 'activate'
78
+ ).hasErrors() )
79
+ ),
80
+ } ) )
81
+ .filter(
82
+ ( tab ) =>
83
+ tab.modules.length > 0 &&
84
+ ( root !== 'onboard' || tab.name !== 'advanced' )
85
+ );
86
+ const help = __(
87
+ 'Features is the home base of iThemes Security. Enabling a security feature will unlock the related User Group, Configure, and Notification settings. Disabling a security feature will hide the related options throughout the plugin.',
88
+ 'better-wp-security'
89
+ );
90
+
91
+ return (
92
+ <>
93
+ <ChildPages
94
+ pages={ tabs.map( ( tab ) => ( {
95
+ title: tab.title,
96
+ to: `${ url }/${ tab.name }`,
97
+ id: tab.name,
98
+ replace: true,
99
+ } ) ) }
100
+ />
101
+ { tabs.length > 0 && (
102
+ <Switch>
103
+ <Route path={ `${ path }/:child` }>
104
+ <PageHeader
105
+ title={ __( 'Features', 'better-wp-security' ) }
106
+ subtitle={ __(
107
+ 'Choose the security features you‘d like to enable.',
108
+ 'better-wp-security'
109
+ ) }
110
+ help={ help }
111
+ />
112
+ <ModuleTabPanel base={ url } tabs={ tabs } />
113
+ { root === 'onboard' && <Navigation /> }
114
+ <HelpPage help={ help } />
115
+ </Route>
116
+ <Redirect to={ `${ url }/${ tabs[ 0 ].name }` } />
117
+ </Switch>
118
+ ) }
119
+ </>
120
+ );
121
+ }
122
+
123
+ function ModuleTabPanel( { base, tabs } ) {
124
+ const { child } = useParams();
125
+ const navigateTo = useNavigateTo();
126
+ const onSelect = ( selected ) => {
127
+ navigateTo( `${ base }/${ selected }`, 'replace' );
128
+ };
129
+
130
+ return (
131
+ <Card>
132
+ <ControlledTabPanel
133
+ isStyled
134
+ tabs={ tabs }
135
+ selected={ child }
136
+ onSelect={ onSelect }
137
+ >
138
+ { ( tab ) => <ModuleTab modules={ tab.modules } /> }
139
+ </ControlledTabPanel>
140
+ </Card>
141
+ );
142
+ }
143
+
144
+ function Navigation() {
145
+ const { next } = useNavigation();
146
+
147
+ return (
148
+ <Flex>
149
+ <FlexSpacer />
150
+ <FlexItem>
151
+ <Link
152
+ component={ withNavigate( Button ) }
153
+ isPrimary
154
+ to={ next }
155
+ >
156
+ { __( 'Next', 'better-wp-security' ) }
157
+ </Link>
158
+ </FlexItem>
159
+ </Flex>
160
+ );
161
+ }
162
+
163
+ function ModuleTab( { modules } ) {
164
+ return (
165
+ <CardBody>
166
+ <ModuleGrid modules={ modules } />
167
+ </CardBody>
168
+ );
169
+ }
170
+
171
+ function ModuleGrid( { modules } ) {
172
+ const { root } = useParams();
173
+ const statusToggle =
174
+ root === 'onboard' ? StatusToggleOnboard : StatusToggleSettings;
175
+
176
+ return (
177
+ <div className="itsec-modules">
178
+ { modules.map( ( module ) => (
179
+ <Module
180
+ key={ module.id }
181
+ module={ module }
182
+ statusToggle={
183
+ module.side_effects
184
+ ? StatusToggleSettings
185
+ : statusToggle
186
+ }
187
+ />
188
+ ) ) }
189
+ { modules.length === 1 && <div aria-hidden="true" /> }
190
+ </div>
191
+ );
192
+ }
193
+
194
+ function Module( { module, statusToggle: StatusToggle } ) {
195
+ const { hash } = useLocation();
196
+ const { root } = useParams();
197
+
198
+ const apiError = useSelect( ( select ) =>
199
+ select( MODULES_STORE_NAME ).getError( module.id )
200
+ );
201
+ const validRequirements = validateModuleRequirements( module, 'run' );
202
+
203
+ return (
204
+ <Card
205
+ key={ module.id }
206
+ className={ classnames( 'itsec-module', {
207
+ 'itsec-highlighted-search-result': hash === `#${ module.id }`,
208
+ } ) }
209
+ >
210
+ <CardBody className="itsec-module__body">
211
+ <h3>{ module.title }</h3>
212
+ { root !== 'onboard' &&
213
+ module.status.selected === 'active' &&
214
+ ! validRequirements.hasErrors() && (
215
+ <>
216
+ { module.settings?.interactive.length > 0 && (
217
+ <Tooltip text={ __( 'Edit Settings', 'better-wp-security' ) }>
218
+ <Link
219
+ className="itsec-module__settings"
220
+ to={ `/settings/configure/${ module.type }/${ module.id }` }
221
+ >
222
+ <VisuallyHidden>
223
+ { __( 'Edit Settings', 'better-wp-security' ) }
224
+ </VisuallyHidden>
225
+ </Link>
226
+ </Tooltip>
227
+ ) }
228
+ { ! isEmpty( module.user_groups ) && (
229
+ <Link
230
+ className="itsec-module__user-groups"
231
+ to={ `/settings/user-groups?module=${ module.id }` }
232
+ >
233
+ { sprintf(
234
+ /* translators: 1. The number of user groups. */
235
+ __( 'User Groups (%d)', 'better-wp-security' ),
236
+ size( module.user_groups )
237
+ ) }
238
+ </Link>
239
+ ) }
240
+ </>
241
+ ) }
242
+ <Markup
243
+ content={ module.description }
244
+ tagName="p"
245
+ id={ `itsec-module-description--${ module.id }` }
246
+ />
247
+ <StatusToggle module={ module } />
248
+ <ErrorList
249
+ apiError={ apiError }
250
+ errors={ validRequirements.getAllErrorMessages() }
251
+ />
252
+ </CardBody>
253
+ </Card>
254
+ );
255
+ }
256
+
257
+ function StatusToggleOnboard( { module } ) {
258
+ const { editModule } = useDispatch( MODULES_STORE_NAME );
259
+
260
+ return (
261
+ <FormToggle
262
+ checked={ module.status.selected === 'active' }
263
+ onChange={ ( e ) =>
264
+ editModule( module.id, {
265
+ status: {
266
+ selected: e.target.checked ? 'active' : 'inactive',
267
+ },
268
+ } )
269
+ }
270
+ aria-label={ sprintf(
271
+ /* translators: 1. The module name. */
272
+ __( 'Enable the “%s” module.', 'better-wp-security' ),
273
+ module.title
274
+ ) }
275
+ aria-describedby={ `itsec-module-description--${ module.id }` }
276
+ />
277
+ );
278
+ }
279
+
280
+ function StatusToggleSettings( { module } ) {
281
+ const isActive = module.status.selected === 'active';
282
+ const [ toggling, setIsToggling ] = useState( false );
283
+ const { activateModule, deactivateModule } = useDispatch(
284
+ MODULES_STORE_NAME
285
+ );
286
+
287
+ const toggleStatus = async ( checked ) => {
288
+ setIsToggling( true );
289
+ if ( checked ) {
290
+ await activateModule( module.id );
291
+ } else {
292
+ await deactivateModule( module.id );
293
+ }
294
+ setIsToggling( false );
295
+ };
296
+
297
+ return (
298
+ <FormToggle
299
+ checked={ isActive }
300
+ onChange={ ( e ) => toggleStatus( e.target.checked ) }
301
+ disabled={ toggling }
302
+ aria-label={ sprintf(
303
+ /* translators: 1. The module name. */
304
+ __( 'Enable the “%s” module.', 'better-wp-security' ),
305
+ module.title
306
+ ) }
307
+ aria-describedby={ `itsec-module-description--${ module.id }` }
308
+ />
309
+ );
310
+ }
311
+
312
+ function HelpPage( { help } ) {
313
+ return (
314
+ <HelpFill>
315
+ <PageHeader
316
+ title={ __( 'Features', 'better-wp-security' ) }
317
+ description={ help }
318
+ breadcrumbs={
319
+ <Breadcrumbs trail={ useHelpBreadcrumbTrail() } />
320
+ }
321
+ />
322
+ <HelpList topic="modules" />
323
+ </HelpFill>
324
+ );
325
+ }
{dist/vendors/core → core/admin-pages/entries/settings/pages/modules}/index.php RENAMED
File without changes
core/admin-pages/entries/settings/pages/modules/style.scss ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-modules {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fit, minmax(Min(25rem, 100%), 1fr));
4
+ grid-gap: 1rem;
5
+ }
6
+
7
+ .itsec-module {
8
+ .itsec-module__body {
9
+ display: grid;
10
+ grid-template-columns: 1fr min-content;
11
+ grid-template-rows: 20px 1rem 1fr;
12
+ grid-gap: .25rem 1rem;
13
+ height: 100%;
14
+ padding: 16px;
15
+
16
+ h5, p, .itsec-module__user-groups {
17
+ grid-column: 1 / span 1;
18
+ margin: 0;
19
+ }
20
+
21
+ p {
22
+ grid-row: 3 / span 1;
23
+ }
24
+ }
25
+
26
+ .itsec-module__settings {
27
+ grid-column: 2 / span 1;
28
+ justify-self: end;
29
+ text-decoration: none;
30
+ color: $light-text;
31
+
32
+ &:hover {
33
+ color: $medium-text;
34
+ }
35
+
36
+ &::before {
37
+ @include dashicon('\f111');
38
+ }
39
+ }
40
+
41
+ .itsec-module__user-groups {
42
+ font-size: 13px;
43
+ line-height: 1;
44
+ align-self: center;
45
+ }
46
+
47
+ .components-form-toggle {
48
+ grid-column: 2 / span 1;
49
+ grid-row: 3 / span 1;
50
+ }
51
+
52
+ .itsec-message-list {
53
+ grid-column: 1 / span 2;
54
+ grid-row: 4 / span 1;
55
+ margin-top: 1rem;
56
+ margin-bottom: 0;
57
+ }
58
+
59
+ &.itsec-highlighted-search-result h5 {
60
+ position: relative;
61
+ width: min-content;
62
+ white-space: nowrap;
63
+
64
+ &:after {
65
+ position: absolute;
66
+ content: '';
67
+ background: $main-blue;
68
+ left: 0;
69
+ bottom: -3px;
70
+ width: 100%;
71
+ height: 3px;
72
+ }
73
+ }
74
+ }
core/admin-pages/entries/settings/pages/onboard/index.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ Switch,
6
+ Route,
7
+ Redirect,
8
+ useRouteMatch,
9
+ useLocation,
10
+ Link,
11
+ } from 'react-router-dom';
12
+
13
+ /**
14
+ * WordPress dependencies
15
+ */
16
+ import { __ } from '@wordpress/i18n';
17
+ import { NoticeList } from '@ithemes/security-components';
18
+ import { useEffect, useState } from '@wordpress/element';
19
+ import { useDispatch } from '@wordpress/data';
20
+ import { useMediaQuery } from '@wordpress/compose';
21
+
22
+ /**
23
+ * Internal dependencies
24
+ */
25
+ import { Rocket } from '@ithemes/security-style-guide';
26
+ import WelcomePage from './welcome';
27
+ import { Sidebar, Main, Navigation } from '../../components';
28
+ import { useNavigation, usePages } from '../../page-registration';
29
+ import { ONBOARD_STORE_NAME } from '../../stores';
30
+ import './style.scss';
31
+
32
+ export default function Onboard() {
33
+ const pages = usePages();
34
+ const { url, path } = useRouteMatch();
35
+ const { pathname } = useLocation();
36
+ const { recordVisitedLocation } = useDispatch( ONBOARD_STORE_NAME );
37
+
38
+ useEffect( () => {
39
+ recordVisitedLocation( pathname );
40
+ }, [ pathname ] );
41
+
42
+ return (
43
+ <Switch>
44
+ { pages.map( ( { id, render } ) => (
45
+ <Route path={ `${ path }/:page(${ id })` } key={ id }>
46
+ <DynamicPage render={ render } />
47
+ </Route>
48
+ ) ) }
49
+
50
+ <Route path={ url }>
51
+ { pages.length > 0 && (
52
+ <Redirect to={ `${ url }/${ pages[ 0 ].id }` } />
53
+ ) }
54
+ <Sidebar>
55
+ <Navigation guided allowBack />
56
+ </Sidebar>
57
+ <Main />
58
+ </Route>
59
+ </Switch>
60
+ );
61
+ }
62
+
63
+ function DynamicPage( { render } ) {
64
+ const [ showWelcome, setShowWelcome ] = useState( true );
65
+ const isLarge = useMediaQuery( '(min-width: 960px)' );
66
+ const {
67
+ isExact,
68
+ params: { page },
69
+ } = useRouteMatch();
70
+
71
+ if ( isExact && page === 'site-type' ) {
72
+ if ( isLarge || ! showWelcome ) {
73
+ return (
74
+ <>
75
+ { isLarge ? (
76
+ <WelcomeSidebar />
77
+ ) : (
78
+ <DefaultSidebar page={ page } />
79
+ ) }
80
+ <DefaultMain render={ render } />
81
+ </>
82
+ );
83
+ }
84
+
85
+ return <WelcomePage onDismiss={ () => setShowWelcome( false ) } />;
86
+ }
87
+
88
+ return (
89
+ <>
90
+ <DefaultSidebar page={ page } />
91
+ <DefaultMain render={ render } />
92
+ </>
93
+ );
94
+ }
95
+
96
+ function DefaultSidebar( { page } ) {
97
+ return (
98
+ <Sidebar>
99
+ <Navigation guided allowBack />
100
+ { page === 'site-type' && <SkipSetup /> }
101
+ </Sidebar>
102
+ );
103
+ }
104
+
105
+ function DefaultMain( { render: Component } ) {
106
+ return (
107
+ <Main>
108
+ <NoticeList />
109
+ <Component />
110
+ </Main>
111
+ );
112
+ }
113
+
114
+ function WelcomeSidebar() {
115
+ return (
116
+ <Sidebar className="itsec-onboard-welcome-sidebar" logo="white">
117
+ <p className="itsec-onboard-welcome-sidebar__lead">
118
+ { __(
119
+ 'Welcome to iThemes Security. You are just a few clicks away from securing your site.',
120
+ 'better-wp-security'
121
+ ) }
122
+ </p>
123
+ <p className="itsec-onboard-welcome-sidebar__content">
124
+ { __(
125
+ 'The next steps will guide you through the setup process so the most important security featured are enabled for your site.',
126
+ 'better-wp-security'
127
+ ) }
128
+ </p>
129
+ <SkipSetup showGraphic />
130
+ </Sidebar>
131
+ );
132
+ }
133
+
134
+ function SkipSetup( { showGraphic } ) {
135
+ const { next } = useNavigation();
136
+
137
+ return (
138
+ <div className="itsec-onboard-skip-setup">
139
+ { showGraphic && (
140
+ <Rocket className="itsec-onboard-welcome-sidebar__graphic" />
141
+ ) }
142
+ <div className="itsec-onboard-skip-setup__text">
143
+ <Link to={ next }>{ __( 'Skip Setup', 'better-wp-security' ) }</Link>
144
+ <p className="itsec-onboard-skip-setup__description">
145
+ { __( 'Proceed with default settings.', 'better-wp-security' ) }
146
+ </p>
147
+ </div>
148
+ </div>
149
+ );
150
+ }
core/admin-pages/entries/settings/pages/onboard/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/onboard/style.scss ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings-sidebar--root-onboard:not(.itsec-settings-sidebar--expanded) {
2
+ .itsec-settings-sidebar__inner {
3
+ > :not(.itsec-settings-sidebar__toggle) {
4
+ display: none;
5
+
6
+ @include break-large {
7
+ display: initial;
8
+ }
9
+ }
10
+ }
11
+ }
12
+
13
+ .itsec-onboard-skip-setup {
14
+ margin-top: auto;
15
+ align-self: center;
16
+
17
+ a {
18
+ font-size: 1.1rem;
19
+ text-decoration-skip-ink: all;
20
+ }
21
+
22
+ p.itsec-onboard-skip-setup__description {
23
+ font-style: italic;
24
+ margin: 0;
25
+ }
26
+ }
27
+
28
+ .itsec-settings-sidebar.itsec-onboard-welcome-sidebar {
29
+ background: $main-blue;
30
+ color: $highlight-blue;
31
+ overflow-x: hidden;
32
+ width: 22rem;
33
+
34
+ .itsec-settings-sidebar__logo svg {
35
+ max-width: 16rem;
36
+ }
37
+
38
+ p {
39
+ color: $highlight-blue;
40
+ }
41
+
42
+ .itsec-onboard-welcome-sidebar__lead {
43
+ font-size: 1.6rem;
44
+ line-height: 1.2;
45
+ font-weight: 500;
46
+ margin: 1rem 0 1rem 0;
47
+
48
+ @include break-small {
49
+ margin-top: 1.5rem;
50
+ margin-bottom: 1.5rem;
51
+ }
52
+ }
53
+
54
+ .itsec-onboard-welcome-sidebar__content {
55
+ font-size: 1.125rem;
56
+ font-weight: 300;
57
+ margin: 0;
58
+ }
59
+
60
+ .itsec-onboard-skip-setup {
61
+ align-self: stretch;
62
+
63
+ a {
64
+ color: $highlight-blue;
65
+
66
+ &:hover {
67
+ color: $highlight-blue;
68
+ text-decoration: none;
69
+ }
70
+ }
71
+ }
72
+
73
+ .itsec-onboard-skip-setup__text {
74
+ margin-top: 2rem;
75
+ }
76
+
77
+ p.itsec-onboard-skip-setup__description {
78
+ color: $highlight-blue;
79
+ }
80
+
81
+ .itsec-onboard-welcome-sidebar__graphic {
82
+ float: right;
83
+ margin-right: -6rem;
84
+ shape-outside: polygon(66.63% 0px, 0px 72.4%, 65.51% 99.48%);
85
+ height: 12rem;
86
+ }
87
+ }
core/admin-pages/entries/settings/pages/onboard/welcome/index.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Link } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Button } from '@wordpress/components';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { Rocket } from '@ithemes/security-style-guide';
15
+ import { Logo, PageHeader } from '../../../components';
16
+ import { useNavigation } from '../../../page-registration';
17
+ import './style.scss';
18
+ import { __ } from '@wordpress/i18n';
19
+
20
+ export default function WelcomePage( { onDismiss } ) {
21
+ const { next } = useNavigation();
22
+
23
+ return (
24
+ <div className="itsec-onboard-welcome-page">
25
+ <Logo style="white" className="itsec-onboard-welcome-page__logo" />
26
+
27
+ <PageHeader
28
+ title={ __(
29
+ 'Welcome to iThemes Security. You are just a few clicks away from securing your site.',
30
+ 'better-wp-security'
31
+ ) }
32
+ subtitle={ __(
33
+ 'The next steps will guide you through the setup process so the most important security featured are enabled for your site.',
34
+ 'better-wp-security'
35
+ ) }
36
+ breadcrumbs={ false }
37
+ />
38
+
39
+ <div className="itsec-onboard-welcome-page__actions-container">
40
+ <Rocket className="itsec-onboard-welcome-page__graphic" />
41
+ <div className="itsec-onboard-welcome-page__actions">
42
+ <Button
43
+ onClick={ onDismiss }
44
+ icon="arrow-right-alt"
45
+ text={ __( 'Start', 'better-wp-security' ) }
46
+ iconPosition="right"
47
+ isPrimary
48
+ className="itsec-button-icon-right"
49
+ />
50
+ <Link to={ next }>{ __( 'Skip Setup', 'better-wp-security' ) }</Link>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
core/admin-pages/entries/settings/pages/onboard/welcome/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/onboard/welcome/style.scss ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings .itsec-onboard-welcome-page {
2
+ display: flex;
3
+ flex-direction: column;
4
+ overflow: hidden;
5
+ background: $main-blue;
6
+ padding: 3rem 2rem 0;
7
+
8
+ @include break-small {
9
+ overflow: auto;
10
+ }
11
+
12
+ h1, h2, p, a {
13
+ color: $highlight-blue;
14
+ }
15
+
16
+ a:hover {
17
+ color: $highlight-blue;
18
+ text-decoration: none;
19
+ }
20
+
21
+ .itsec-onboard-welcome-page__logo {
22
+ max-width: 16rem;
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ .itsec-page-header {
27
+ margin: 1rem 0;
28
+
29
+ @include break-small {
30
+ margin: 2rem 0;
31
+ }
32
+ }
33
+
34
+ .itsec-onboard-welcome-page__actions-container {
35
+ margin-top: auto;
36
+ display: flex;
37
+ justify-content: space-between;
38
+ align-items: center;
39
+ }
40
+
41
+ .itsec-onboard-welcome-page__graphic {
42
+ flex-grow: 1;
43
+ max-width: 80vw;
44
+ margin-bottom: -5%;
45
+ margin-left: calc(-2rem - 10%); // 2rem for the padding.
46
+ }
47
+
48
+ .itsec-onboard-welcome-page__actions {
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: flex-end;
52
+
53
+ & > *:first-child {
54
+ margin-bottom: 1rem;
55
+
56
+ @include break-small {
57
+ margin-bottom: 2rem;
58
+ }
59
+ }
60
+
61
+ @supports (-webkit-touch-callout: none) {
62
+ padding-bottom: 3rem; // Try to account for safari action bar.
63
+ }
64
+ }
65
+
66
+ .components-button {
67
+ font-size: 1rem;
68
+ font-weight: 500;
69
+ text-transform: uppercase;
70
+ color: $main-blue;
71
+ background: $white;
72
+ }
73
+ }
core/admin-pages/entries/settings/pages/secure-site/index.js ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { sortBy } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __, sprintf } from '@wordpress/i18n';
10
+ import { Card, Button, Flex, FlexItem } from '@wordpress/components';
11
+ import { useSelect, useDispatch } from '@wordpress/data';
12
+ import {
13
+ useMemo,
14
+ useState,
15
+ createInterpolateElement,
16
+ } from '@wordpress/element';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { useSingletonEffect } from '@ithemes/security-hocs';
22
+ import { Accordion, Spinner } from '@ithemes/security-components';
23
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
24
+ import { useGlobalNavigationUrl } from '@ithemes/security-utils';
25
+ import { PageHeader } from '../../components';
26
+ import { ONBOARD_STORE_NAME } from '../../stores';
27
+ import ToughGuy from './tough-guy.svg';
28
+ import './style.scss';
29
+
30
+ export default function SecureSite() {
31
+ useCompletionSteps();
32
+ const [ isEndScreen, setIsEndScreen ] = useState( false );
33
+
34
+ if ( isEndScreen ) {
35
+ return <EndScreen />;
36
+ }
37
+
38
+ return <OverviewScreen goToEnd={ () => setIsEndScreen( true ) } />;
39
+ }
40
+
41
+ function OverviewScreen( { goToEnd } ) {
42
+ const { completeOnboarding } = useDispatch( ONBOARD_STORE_NAME );
43
+ const { steps, currentStep } = useSelect( ( select ) => ( {
44
+ steps: select( ONBOARD_STORE_NAME ).getCompletionSteps(),
45
+ currentStep: select( ONBOARD_STORE_NAME ).getCompletionStep(),
46
+ } ) );
47
+
48
+ let subtitle;
49
+
50
+ if ( currentStep === true ) {
51
+ subtitle = __( 'Your site has been secured.', 'better-wp-security' );
52
+ } else if ( currentStep === false ) {
53
+ subtitle = __( 'Click finish to secure your site.', 'better-wp-security' );
54
+ } else {
55
+ subtitle = __( 'Your site is being secured.', 'better-wp-security' );
56
+ }
57
+
58
+ return (
59
+ <>
60
+ <PageHeader
61
+ title={ __( 'Secure Site', 'better-wp-security' ) }
62
+ subtitle={ subtitle }
63
+ breadcrumbs={ false }
64
+ />
65
+
66
+ <h2 className="itsec-secure-site-overview">
67
+ { __( 'Overview', 'better-wp-security' ) }
68
+ </h2>
69
+
70
+ <Steps steps={ steps } currentStep={ currentStep } />
71
+
72
+ <Flex justify="right">
73
+ <FlexItem>
74
+ { currentStep === true ? (
75
+ <Button isPrimary onClick={ goToEnd }>
76
+ { __( 'Finish', 'better-wp-security' ) }
77
+ </Button>
78
+ ) : (
79
+ <Button
80
+ isPrimary
81
+ onClick={ completeOnboarding }
82
+ disabled={ currentStep !== false }
83
+ >
84
+ { __( 'Secure Site', 'better-wp-security' ) }
85
+ </Button>
86
+ ) }
87
+ </FlexItem>
88
+ </Flex>
89
+ </>
90
+ );
91
+ }
92
+
93
+ function EndScreen() {
94
+ const dashboardLink = useGlobalNavigationUrl( 'dashboard' ),
95
+ settingsLink = useGlobalNavigationUrl( 'settings' );
96
+
97
+ return (
98
+ <>
99
+ <PageHeader
100
+ align="center"
101
+ title={ __(
102
+ 'Good work! Your site is more secure than ever.',
103
+ 'better-wp-security'
104
+ ) }
105
+ subtitle={ __(
106
+ 'You can now move on with other things in your life.',
107
+ 'better-wp-security'
108
+ ) }
109
+ breadcrumbs={ false }
110
+ />
111
+
112
+ <figure className="itsec-secure-site-end-graphic">
113
+ <ToughGuy />
114
+ </figure>
115
+
116
+ <p className="itsec-secure-site-end-content">
117
+ { createInterpolateElement(
118
+ __(
119
+ 'If you want to dig into your site’s security further, checkout your <dashboard>security dashboard</dashboard>, and make changes via <settings>settings</settings>.',
120
+ 'better-wp-security'
121
+ ),
122
+ {
123
+ dashboard: <a href={ dashboardLink } />,
124
+ settings: <a href={ settingsLink } />,
125
+ }
126
+ ) }
127
+ </p>
128
+
129
+ <Flex justify="center">
130
+ <FlexItem>
131
+ <Button isPrimary href={ dashboardLink }>
132
+ { __( 'Dashboard', 'better-wp-security' ) }
133
+ </Button>
134
+ </FlexItem>
135
+
136
+ <FlexItem>
137
+ <Button isPrimary href={ settingsLink }>
138
+ { __( 'Settings', 'better-wp-security' ) }
139
+ </Button>
140
+ </FlexItem>
141
+ </Flex>
142
+ </>
143
+ );
144
+ }
145
+
146
+ function Steps( { steps, currentStep } ) {
147
+ const [ expanded, setExpanded ] = useState( false );
148
+ const panels = useMemo(
149
+ () =>
150
+ sortBy( steps, 'priority' ).map(
151
+ ( { render: Component, ...step } ) => {
152
+ const isCurrent = step.id === currentStep?.id;
153
+ const isDone =
154
+ step.priority < ( currentStep?.priority || 0 );
155
+ const isPending =
156
+ step.priority > ( currentStep?.priority || 0 );
157
+
158
+ return {
159
+ name: step.id,
160
+ title: step.label,
161
+ text: step.label,
162
+ icon: 'yes-alt',
163
+ render:
164
+ Component &&
165
+ ( ( props ) => (
166
+ <div { ...props }>
167
+ <Component />
168
+ </div>
169
+ ) ),
170
+ showSpinner:
171
+ currentStep !== true &&
172
+ ( isCurrent || isPending ) ? (
173
+ <Spinner
174
+ size={ 30 }
175
+ color="--itsec-primary-theme-color"
176
+ paused={ isPending }
177
+ />
178
+ ) : (
179
+ false
180
+ ),
181
+ className: isDone && 'itsec-secure-site-step--complete',
182
+ };
183
+ }
184
+ ),
185
+ [ steps, currentStep ]
186
+ );
187
+
188
+ return (
189
+ <Card>
190
+ <Accordion
191
+ isStyled
192
+ className="itsec-secure-site-steps"
193
+ allowNone
194
+ panels={ panels }
195
+ expanded={ expanded }
196
+ setExpanded={ setExpanded }
197
+ />
198
+ </Card>
199
+ );
200
+ }
201
+
202
+ function useCompletionSteps() {
203
+ const { registerCompletionStep } = useDispatch( ONBOARD_STORE_NAME );
204
+ const { saveModules, saveSettings } = useDispatch( MODULES_STORE_NAME );
205
+
206
+ useSingletonEffect( useCompletionSteps, () => {
207
+ registerCompletionStep( {
208
+ id: 'savingModules',
209
+ label: __( 'Enable Features', 'better-wp-security' ),
210
+ priority: 5,
211
+ callback() {
212
+ return saveModules();
213
+ },
214
+ render: function SavingModules() {
215
+ const modules = useSelect(
216
+ ( select ) =>
217
+ select( MODULES_STORE_NAME ).getEditedModules(),
218
+ []
219
+ ).filter(
220
+ ( module ) =>
221
+ module.status.selected === 'active' && module.onboard
222
+ );
223
+
224
+ if ( ! modules.length ) {
225
+ return (
226
+ <p>
227
+ { __(
228
+ 'No additional security features have been selected.',
229
+ 'better-wp-security'
230
+ ) }
231
+ </p>
232
+ );
233
+ }
234
+
235
+ return (
236
+ <>
237
+ <p>
238
+ { __(
239
+ 'The following security features will be enabled:',
240
+ 'better-wp-security'
241
+ ) }
242
+ </p>
243
+ <ul>
244
+ { modules.map( ( module ) => (
245
+ <li key={ module.id }>{ module.title }</li>
246
+ ) ) }
247
+ </ul>
248
+ </>
249
+ );
250
+ },
251
+ } );
252
+
253
+ registerCompletionStep( {
254
+ id: 'savingSettings',
255
+ label: __( 'Configure Settings', 'better-wp-security' ),
256
+ priority: 10,
257
+ callback() {
258
+ return saveSettings();
259
+ },
260
+ render: function SavingSettings() {
261
+ const settings = useSelect( ( select ) => {
262
+ return select( MODULES_STORE_NAME )
263
+ .getEditedModules()
264
+ .filter(
265
+ ( module ) =>
266
+ module.status.selected === 'active' &&
267
+ module.settings?.onboard?.length > 0
268
+ )
269
+ .flatMap( ( module ) => {
270
+ const edits = select(
271
+ MODULES_STORE_NAME
272
+ ).getSettingEdits( module.id );
273
+
274
+ return module.settings.onboard.reduce(
275
+ ( acc, setting ) => {
276
+ if ( ! edits || ! edits[ setting ] ) {
277
+ return acc;
278
+ }
279
+
280
+ const title =
281
+ module.settings.schema?.uiSchema?.[
282
+ setting
283
+ ]?.[ 'ui:title' ] ||
284
+ module.settings.schema.properties[
285
+ setting
286
+ ].title;
287
+
288
+ acc.push(
289
+ sprintf(
290
+ /* translators: 1. Module title, 2. Setting title. */
291
+ __( '%1$s: %2$s', 'better-wp-security' ),
292
+ module.title,
293
+ title
294
+ )
295
+ );
296
+
297
+ return acc;
298
+ },
299
+ []
300
+ );
301
+ } );
302
+ }, [] );
303
+
304
+ if ( ! settings.length ) {
305
+ return (
306
+ <p>
307
+ { __(
308
+ 'No settings have been configured.',
309
+ 'better-wp-security'
310
+ ) }
311
+ </p>
312
+ );
313
+ }
314
+
315
+ return (
316
+ <>
317
+ <p>
318
+ { __(
319
+ 'The following settings will be configured:',
320
+ 'better-wp-security'
321
+ ) }
322
+ </p>
323
+ <ul>
324
+ { settings.map( ( setting, i ) => (
325
+ <li key={ i }>{ setting }</li>
326
+ ) ) }
327
+ </ul>
328
+ </>
329
+ );
330
+ },
331
+ } );
332
+ } );
333
+ }
core/admin-pages/entries/settings/pages/secure-site/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/secure-site/style.scss ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-secure-site-steps {
2
+ .itsec-accordion__header-icon {
3
+ color: $main-blue;
4
+ }
5
+ }
6
+
7
+ .itsec-secure-site-end-graphic {
8
+ display: flex;
9
+ align-items: center;
10
+ flex-direction: column;
11
+
12
+ svg {
13
+ max-width: 40rem;
14
+ }
15
+ }
16
+
17
+ .itsec-secure-site-end-content {
18
+ text-align: center;
19
+ max-width: 40rem;
20
+ font-size: 1.25rem;
21
+ color: $medium-text;
22
+ margin-left: auto;
23
+ margin-right: auto;
24
+ }
25
+
26
+ .itsec-page--secure-site {
27
+ h2.itsec-secure-site-overview {
28
+ color: $dark-text;
29
+ font-weight: 500;
30
+ }
31
+
32
+ .itsec-spinner,
33
+ .itsec-accordion__header-icon {
34
+ animation: itsec-animation-fade-in-constant 400ms;
35
+ }
36
+
37
+ .itsec-accordion__header .itsec-spinner {
38
+ margin-right: 2px;
39
+ transform: scale(25/30);
40
+ }
41
+ }
core/admin-pages/entries/settings/pages/secure-site/tough-guy.svg ADDED
@@ -0,0 +1 @@
 
1
+ <svg viewBox="0 0 570.16 390.92" xmlns="http://www.w3.org/2000/svg"><path fill="#DCEBF3" d="M228.16 71.16c-69.77 31.91-136-40.64-181.26.92 -63.42 58.29 56.15 157.6-11.65 217.8 -73.67 65.41-26.7 132.69 92.89 84.9 30.11-12 164.72-90.63 205.62-64.73 17.79 11.27 119 63.24 171.27 15.17 73.8-67.83 71-191-6.23-275s-109.8-52.61-270.64 20.94Z"/><g fill="#FFF"><path d="M183.66 115.46h209v130h-209Z"/><path d="M64.66 161.46h62v100h-62Z"/><path d="M53.66 261.46h85v13h-85Z"/></g><g fill="#0083E3"><path d="M393.63 258.09H181.4h-.001c-3.51 0-6.35-2.84-6.35-6.34V114.89v-.001c.01-4.93 4-8.91 8.92-8.9 0 0 .01 0 .02 0h207.07v0c4.91 0 8.89 3.98 8.91 8.9v136.85 0c-.01 3.5-2.85 6.34-6.35 6.34ZM184 108.47v0c-3.55-.01-6.43 2.87-6.44 6.42v136.86 -.001c0 2.13 1.74 3.87 3.88 3.87h212.19v0c2.13-.01 3.86-1.74 3.87-3.87V114.88h-.001c-.01-3.55-2.89-6.43-6.43-6.42Z"/><path d="M393.12 245.45H183.73V115.59h209.39ZM186.21 243h204.44V118.07H186.21Z"/></g><path fill="none" d="M162.42 263.1c-3.4 0-7.66-2-9.48-2.9l-.58-.28v-3.83h270.3v3.83l-.58.28c-1.8.87-6 2.9-9.4 2.9Z"/><path fill="#017DE7" d="M413 265.93H162.74c-3.76 0-8.25-2.14-10.17-3.05l-.56-.26h-1v-7.11h273.65v6l-.17 1.05h-.83l-.53.25c-1.92.96-6.39 3.12-10.13 3.12Zm-259.52-5.37l.18.09c1.77.83 5.91 2.8 9.11 2.8H413c3.2 0 7.32-2 9.08-2.83l.14-.07V258H153.45Zm270 2v0Z"/><g fill="#0083E3"><path d="M301.68 260.49h-30.85v0c-1.66 0-3-1.35-3-3v0h36.87v0 0c0 1.65-1.35 3-3 3"/><path d="M287 112.05v0c-.01 1.18-.96 2.13-2.15 2.13 -1.19-.01-2.14-.96-2.14-2.15s.95-2.14 2.14-2.14c0 0 0 0 0 0h-.001c1.17 0 2.13.96 2.13 2.14"/><path d="M274.77 221.35v0c-2.96-.05-5.88-.69-8.58-1.9 -9.39-4-16.95-13.38-16.95-18.45 0-8.83 16.17-15.74 36.8-15.74 20.63 0 36.8 6.91 36.8 15.74v0c.12.97-.19 1.94-.85 2.67 -.85.67-2 .39-3.66 0 -4.73-1.14-14.59-3.51-31.77 12.87v0c-3.15 3.09-7.38 4.82-11.79 4.81ZM286 186.74c-19.14 0-35.31 6.53-35.31 14.26 0 4.6 7.4 13.39 16 17.09 5 2.16 12.37 3.45 18.75-2.63 17.77-16.94 28.16-14.44 33.15-13.24 1 .25 2.11.5 2.39.28 .08-.06.29-.35.29-1.5 .09-7.73-16.08-14.26-35.27-14.26Z"/><path d="M249.99 200.25h72.11v1.49h-72.11Z"/><path d="M258.93 190.77h1.49v23.95h-1.49Z"/><path d="M272.1 186.89h1.49v33.63h-1.49Z"/><path d="M285.29 186h1.49v29.99h-1.49Z"/><path d="M298.49 186.89h1.49v19.04h-1.49Z"/><path d="M311.68 190.77h1.49v11.33h-1.49Z"/><path d="M272 148.32c-3.41-.53-6.86-.95-10.24-1.63h0c-1.49-.25-2.92-.74-4.24-1.45v0c-1.21-.76-2.36-1.6-3.45-2.5v0c-2.41-1.9-5.22-3.2-8.21-3.8v0c-3.09-.53-6.22-.75-9.35-.68 -2.66 0-2.66 4.11 0 4.13l0 0c2.87-.1 5.76.1 8.6.6v0c2.75.66 5.32 1.96 7.49 3.8v-.001c2.38 1.95 5.21 3.3 8.24 3.92 3.33.65 6.71 1.07 10.06 1.59v0c1.09.26 2.2-.37 2.54-1.44v0c.29-1.1-.35-2.23-1.44-2.54Z"/><path d="M322.9 137v0c-6.26-.68-12.56.57-18.08 3.58 -5.23 2.71-10.41 6.54-16.58 6.31 -2.66-.1-2.65 4 0 4.13 6.41.23 11.93-3.22 17.35-6.17v0c2.55-1.47 5.29-2.59 8.15-3.31v0c3-.68 6.1-.82 9.16-.41h-.001c1.13-.01 2.05-.93 2.06-2.06v0c-.03-1.13-.94-2.04-2.06-2.07Z"/><path d="M256.06 153.63c-4.78 0-4.79 7.43 0 7.43s4.78-7.43 0-7.43Z"/><path d="M299 153.63c-4.78 0-4.79 7.43 0 7.43s4.77-7.43 0-7.43Z"/></g><path fill="#FFF" d="M126.66 168.46h301v22h-301Z"/><g fill="#0083E3"><path d="M127.8 261.75h-63V161.46h63Zm-62-1h61v-98.29h-61Z"/><path d="M139.3 275.75h-87v-15h87Zm-86-1h85v-13h-85Z"/><path d="M427.66 169.25v0 -.5H126.8v22h300.86v-.5 0Zm-224.29.5h21.78l14.19 20h-21.78Zm-9.88 20h-18.33l-14.2-20h18.34Zm55.73-20h19.48l14.19 20h-19.48Zm43.55 0h21.78l14.19 20H307Zm45.85 0h19.48l14.19 20h-19.48Zm43.55 0H404l14.19 20h-21.83Zm-254.22 0h9l14.19 20H128Z"/><path d="M113.42 104.75H95.3v21l4.55 2.87 -11 5.43v0c-.38-.08-.76-.12-1.14-.12v-.001c-3.04-.13-5.6 2.24-5.72 5.27 -.11 2.54 1.55 4.82 4 5.51l6.61 10.74h-2.89v0c-1.69 0-3.06 1.37-3.06 3.06v2.94h20v-2.94 0c-.01-1.69-1.38-3.06-3.06-3.06h-2.89l-8.88-12.46v0c.86-1 1.33-2.27 1.33-3.58v0c-.01-.41-.05-.81-.13-1.2l12.63-5.89 17.25 10.84 20.64-4v-14.58l8.89-.59Zm-12 51.71h2.18v0c1.13 0 2.06.92 2.06 2.06v1.94h-18v-1.94 0c0-1.14.92-2.06 2.06-2.06h11.7Zm-1.94-1h-5.65L87.36 145h.3v0c1.25 0 2.47-.43 3.45-1.21ZM87.66 144v0c-.34 0-.68-.04-1-.11l-.22-.07 -.001-.001c-2.36-.7-3.71-3.17-3.02-5.53 .69-2.36 3.16-3.71 5.52-3.02 2.35.69 3.7 3.16 3.01 5.52 -.57 1.91-2.33 3.21-4.33 3.19Zm5.13-6.47h0c-.57-1.47-1.73-2.62-3.2-3.17L100.4 129l4.29 2.71ZM122.16 125v16.6l-20.7-13.08 -.13-.08 -5-3.17v-19.15L128.8 125Zm20.39 13.47l-18.64 3.95v-5.92l14.75-2.1v-8.8l3.89-.31Zm-12.27-14l-32.6-18.7h15.5l35.47 17.5Z"/><path d="M139.75 266.25h289.32v0c4.97 0 9 4.02 9 9v0 0H139.75v0 -9 0Z"/><path d="M438.66 275.75h-299.5v-10h288.3c6.18 0 11.2 4.26 11.2 9.5Zm-298.32-1h297.13c-.31-4.45-4.68-8-10-8H140.34Z"/><path d="M562.27 275.35l7.88-.1h-7.87v0c-.01.03-.02.06-.01.1Z"/><path d="M561.6 275.85l.08-.57 .59.07 -.59-.09v-.47h.56 7.87v1l-7.88.1Z"/><path d="M558.77 275.25h-30.11c10.37.16 20.11.2 30.12.13h0c0-.05-.01-.09-.01-.13Z"/><path d="M549.37 275.91c-6.81 0-13.66-.05-20.72-.16v-1h30.65l.17 1.09h-.69Z"/><path d="M45.7 275.25v0Z"/><path d="M46.2 275.76l-4.54-.01 0-1 4.43 0 .11.51 0 .5Z"/><path d="M48.77 275.25v0c158.35.2 315.6.2 474.07 0Z"/><path d="M282.14 275.91c-77.41 0-154.84-.05-233.37-.15h-.7l.2-1h474.57v1c-80.97.1-160.84.15-240.7.15Z"/></g><path fill="none" d="M512.66 275.46l5 0"/><g fill="#0083E3"><path d="M115.66 202.5v-15.21 0c-.01-.14-.09-.25-.21-.29v0c-2.83-.86-5.75-1.37-8.7-1.51v0c-.12-.01-.22.08-.22.19 -.01 0-.01 0-.01.01v2.69 0c0 .1-.09.19-.19.19 -.02 0-.03-.01-.04-.01 -1.51-.24-3.18-.43-5-.57v-.001c-.13-.02-.22-.11-.22-.23v-2.62 0c0-.13-.1-.23-.22-.24h0c-1.5-.19-3-.3-4.5-.31l-.001 0c-1.42.02-2.83.14-4.23.35v0c-.13.02-.22.12-.22.25v2.58 -.001c0 .12-.1.22-.22.24 -1.79.13-3.45.33-5 .56v0c-.1.02-.2-.04-.22-.14 -.01-.02-.01-.03-.01-.05v-2.71 0c0-.12-.1-.22-.21-.22 -.01-.01-.01 0-.02 0v0c-2.94.14-5.84.65-8.65 1.51v0c-.13.04-.21.15-.21.29v15.21s-.43 10.22 6.52 18.27c6.15 7.12 11.29 8.83 12.39 9.12v0c.14.03.28.03.43 0 1.09-.29 6.24-2 12.39-9.12 6.95-8.05 6.53-18.27 6.53-18.27Zm-9.33 16c-4.18 4.85-7.76 6.94-9.59 7.77v0c-.14.06-.29.06-.42-.001 -1.83-.83-5.41-2.92-9.59-7.77 -6.16-7.12-5.92-16.47-5.91-16.56v-9.34 0c0-.14.09-.26.23-.29v0c5.02-1.27 10.18-1.93 15.37-1.95h.27v0c5.16.01 10.31.67 15.31 1.98v0c.13.03.22.15.23.29v9.37c.01.07.26 9.42-5.9 16.54Z"/><path d="M96.49 199.24v0c-1.83-.01-3.32 1.47-3.32 3.3 -.01 1.82 1.47 3.31 3.3 3.31 1.82 0 3.31-1.48 3.31-3.3v0c0-1.83-1.48-3.32-3.3-3.32 -.01-.01-.01-.01-.01-.01Z"/><path d="M108.82 202.06v-6.73 -.001c-.01-.14-.1-.25-.22-.28v-.001c-3.92-.81-7.91-1.22-11.9-1.22h-.25v0c-4.04.01-8.06.43-12 1.25v-.001c-.13.02-.22.13-.22.27v6.76c0 .49 0 8.33 5.08 14.19H89.3c1.98 2.41 4.34 4.49 7 6.15h0c.12.06.26.06.39 0h0c2.64-1.67 5.01-3.74 7-6.15 5.23-6.01 5.12-14.15 5.12-14.24Zm-7.7 12c-.41.47-.8.91-1.19 1.32 -.09.09-.16.06-.16-.07v-2.82c0-2.14 2.07-3.23 2.75-3.53v0c.12-.06.21-.19.22-.32v-2.14 0c0-.09-.05-.16-.14-.17 -.04-.01-.07 0-.09.01h-.001c-3.89 1.52-8.21 1.52-12.09 0v0c-.07-.04-.16-.02-.2.04 -.02.02-.03.05-.02.09v2.14 0c0 .13.09.26.22.32 2.27.86 2.73 2.35 2.73 3.55v2.78c0 .13-.07.16-.16.06 -.38-.4-.77-.83-1.18-1.29 -4.27-5-4.25-11.9-4.24-12v-4 0c0-.14.1-.25.23-.27v-.001c2.84-.45 5.71-.67 8.59-.68h.26v-.001c2.83-.01 5.66.22 8.47.66h-.001c.13.02.22.13.23.27v3.97c.01.12.05 7.12-4.25 12.09Z"/><path d="M489.37 267.65l-8.52-47.71h-4.12v-1.8l-.001 0c3.26-1.89 4.38-6.06 2.5-9.32 -1.89-3.27-6.06-4.39-9.32-2.51 -3.27 1.88-4.39 6.05-2.51 9.31 .59 1.04 1.46 1.9 2.5 2.5v1.8h-4.13l-8.51 47.71h-3.55v6.81h39.19v-6.81Zm-21.16-55.37v0c0-2.83 2.28-5.11 5.11-5.11 2.82 0 5.11 2.28 5.11 5.11 0 2.82-2.29 5.11-5.11 5.11v0c-2.83 0-5.11-2.29-5.11-5.11Zm11.21 9.37l2 11.25 -15.14-6.06 .93-5.19Zm-15 17l19.54 8.14 1.17 6.57 -22-8.7Zm-3.42 18l26.57 10.34 .11.6H459Zm30.25 16.05h-35.82v-3.41h35.78Z"/><path d="M539.37 267.65l-8.52-47.71h-4.12v-1.8 0c3.26-1.89 4.38-6.06 2.5-9.32 -1.89-3.27-6.06-4.39-9.32-2.51 -3.27 1.88-4.39 6.05-2.51 9.31 .59 1.04 1.46 1.9 2.5 2.5v1.8h-4.13l-8.51 47.71h-3.55v6.81h39.19v-6.81Zm-21.16-55.37v0c0-2.83 2.28-5.11 5.11-5.11 2.82 0 5.11 2.28 5.11 5.11 0 2.82-2.29 5.11-5.11 5.11v0c-2.83 0-5.11-2.29-5.11-5.11Zm11.21 9.37l2 11.25 -15.14-6.06 .93-5.19Zm-15 17l19.54 8.14 1.17 6.57 -22-8.7Zm-3.42 18l26.57 10.34 .11.6H509Zm30.25 16.05h-35.82v-3.41h35.78Z"/></g></svg>
core/admin-pages/entries/settings/pages/settings/index.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ Redirect,
6
+ Route,
7
+ Switch,
8
+ useRouteMatch,
9
+ NavLink,
10
+ } from 'react-router-dom';
11
+
12
+ /**
13
+ * WordPress dependencies
14
+ */
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ /**
18
+ * Internal dependencies
19
+ */
20
+ import { NoticeList } from '@ithemes/security-components';
21
+ import { usePages } from '../../page-registration';
22
+ import { Main, Navigation, Sidebar } from '../../components';
23
+ import './style.scss';
24
+
25
+ export default function Settings() {
26
+ const pages = usePages();
27
+ const { url, path } = useRouteMatch();
28
+
29
+ return (
30
+ <Switch>
31
+ { pages.map( ( { id, render: Component } ) => (
32
+ <Route path={ `${ path }/:page(${ id })` } key={ id }>
33
+ <Sidebar>
34
+ <Navigation />
35
+ <AdvancedNav url={ url } />
36
+ </Sidebar>
37
+ <Main>
38
+ <NoticeList />
39
+ <Component />
40
+ </Main>
41
+ </Route>
42
+ ) ) }
43
+
44
+ <Route path={ url }>
45
+ { pages.length > 0 && (
46
+ <Redirect
47
+ to={ `${ url }/${
48
+ pages.find( ( { priority } ) => priority !== false )
49
+ .id
50
+ }` }
51
+ />
52
+ ) }
53
+ <Sidebar>
54
+ <Navigation />
55
+ <AdvancedNav url={ url } />
56
+ </Sidebar>
57
+ <Main />
58
+ </Route>
59
+ </Switch>
60
+ );
61
+ }
62
+
63
+ function AdvancedNav( { url } ) {
64
+ return (
65
+ <ul className="itsec-settings-advanced-nav">
66
+ <li>
67
+ <NavLink
68
+ to={ `${ url }/tools` }
69
+ className="itsec-settings-advanced-nav--tools"
70
+ >
71
+ <span>{ __( 'Tools', 'better-wp-security' ) }</span>
72
+ </NavLink>
73
+ </li>
74
+ <li>
75
+ <NavLink
76
+ to={ `${ url }/configure/advanced` }
77
+ className="itsec-settings-advanced-nav--advanced"
78
+ >
79
+ <span>{ __( 'Advanced', 'better-wp-security' ) }</span>
80
+ </NavLink>
81
+ </li>
82
+ </ul>
83
+ );
84
+ }
core/admin-pages/entries/settings/pages/settings/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/settings/style.scss ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-settings-advanced-nav {
2
+ margin-top: auto;
3
+ align-self: center;
4
+ display: flex;
5
+ align-items: center;
6
+
7
+ li:not(:last-child) {
8
+ border-right: 1px solid $border-color;
9
+ margin-right: .5rem;
10
+ padding-right: .5rem;
11
+ }
12
+
13
+ a {
14
+ color: $medium-text;
15
+ display: flex;
16
+ align-items: center;
17
+ text-decoration: none;
18
+
19
+ &:hover, &.active {
20
+ color: $main-blue;
21
+ }
22
+
23
+ &::before {
24
+ @include dashicon();
25
+ margin-right: .15rem;
26
+ }
27
+
28
+ &.itsec-settings-advanced-nav--tools::before {
29
+ content: '\f107';
30
+ }
31
+
32
+ &.itsec-settings-advanced-nav--advanced::before {
33
+ content: '\f194';
34
+ }
35
+ }
36
+ }
core/admin-pages/entries/settings/pages/site-type/chooser/index.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useRouteMatch, Link } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useDispatch, useSelect } from '@wordpress/data';
10
+ import { createInterpolateElement } from '@wordpress/element';
11
+ import { __ } from '@wordpress/i18n';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { HelpList, MessageList } from '@ithemes/security-components';
17
+ import { PageHeader, SelectableCard, HelpFill } from '../../../components';
18
+ import { STORE_NAME } from '../../../stores/onboard';
19
+ import { useNavigateTo } from '../../../utils';
20
+ import './style.scss';
21
+
22
+ export default function SiteTypeChooser() {
23
+ const { clearVisitedLocations } = useDispatch( STORE_NAME );
24
+ const { siteTypes, lastVisitedLocation } = useSelect( ( select ) => ( {
25
+ siteTypes: select( STORE_NAME ).getSiteTypes(),
26
+ lastVisitedLocation: select( STORE_NAME ).getLastVisitedLocation(),
27
+ } ) );
28
+
29
+ return (
30
+ <>
31
+ <PageHeader
32
+ title={ __( 'Choose the Type of Website', 'better-wp-security' ) }
33
+ subtitle={ __(
34
+ 'Select one of the following that best represents your website.',
35
+ 'better-wp-security'
36
+ ) }
37
+ />
38
+
39
+ { lastVisitedLocation && (
40
+ <MessageList
41
+ hasBorder
42
+ onDismiss={ clearVisitedLocations }
43
+ messages={ [
44
+ createInterpolateElement(
45
+ __(
46
+ 'Already started setting up iThemes Security? <a>Resume</a> from where you left off.',
47
+ 'better-wp-security'
48
+ ),
49
+ {
50
+ a: <Link to={ lastVisitedLocation } />,
51
+ }
52
+ ),
53
+ ] }
54
+ />
55
+ ) }
56
+
57
+ <ul className="itsec-site-type-list">
58
+ { siteTypes.map( ( siteType ) => (
59
+ <li key={ siteType.id }>
60
+ <SiteType
61
+ id={ siteType.id }
62
+ title={ siteType.title }
63
+ description={ siteType.description }
64
+ icon={ siteType.icon }
65
+ recommended={ siteType.recommended }
66
+ />
67
+ </li>
68
+ ) ) }
69
+ </ul>
70
+
71
+ <HelpFill>
72
+ <PageHeader title={ __( 'Site Type', 'better-wp-security' ) } />
73
+ <HelpList topic="site-type" />
74
+ </HelpFill>
75
+ </>
76
+ );
77
+ }
78
+
79
+ function SiteType( { id, title, description, icon, recommended } ) {
80
+ const { clearSiteType } = useDispatch( STORE_NAME );
81
+ const match = useRouteMatch();
82
+ const navigateTo = useNavigateTo();
83
+ const onClick = () => {
84
+ clearSiteType();
85
+ navigateTo( `${ match.url }/${ id }` );
86
+ };
87
+
88
+ return (
89
+ <SelectableCard
90
+ onClick={ onClick }
91
+ title={ title }
92
+ description={ description }
93
+ icon={ icon }
94
+ recommended={ recommended }
95
+ />
96
+ );
97
+ }
core/admin-pages/entries/settings/pages/site-type/chooser/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/site-type/chooser/style.scss ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-site-type-list {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fit, minmax(Min(20rem, 100%), 1fr));
4
+ grid-gap: 1rem;
5
+
6
+ li {
7
+ margin-bottom: 0;
8
+ }
9
+
10
+ .itsec-selectable-card {
11
+ height: 100%;
12
+ width: 100%;
13
+ display: flex;
14
+ align-items: stretch;
15
+ flex-direction: column;
16
+ text-align: left;
17
+
18
+ .components-card {
19
+ height: 100%;
20
+ }
21
+ }
22
+ }
core/admin-pages/entries/settings/pages/site-type/index.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Switch, Route, useRouteMatch } from 'react-router-dom';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import Chooser from './chooser';
10
+ import Questions from './questions';
11
+
12
+ export default function SiteType() {
13
+ const { path } = useRouteMatch();
14
+
15
+ return (
16
+ <Switch>
17
+ <Route path={ `${ path }/:siteType` }>
18
+ <Questions />
19
+ </Route>
20
+
21
+ <Route path={ `${ path }` }>
22
+ <Chooser />
23
+ </Route>
24
+ </Switch>
25
+ );
26
+ }
core/admin-pages/entries/settings/pages/site-type/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/site-type/questions/index.js ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useParams } from 'react-router-dom';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Disabled } from '@wordpress/components';
10
+ import { useSelect, useDispatch } from '@wordpress/data';
11
+ import { useEffect, useLayoutEffect, memo } from '@wordpress/element';
12
+ import { __ } from '@wordpress/i18n';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { HelpList } from '@ithemes/security-components';
18
+ import { SchemaQuestion } from './question';
19
+ import useQuestions from './questions';
20
+ import { useNavigation } from '../../../page-registration';
21
+ import { HelpFill, PageHeader } from '../../../components';
22
+ import { STORE_NAME } from '../../../stores/onboard';
23
+ import './style.scss';
24
+
25
+ export default function Questions() {
26
+ useQuestions();
27
+ const { goNext } = useNavigation();
28
+ const { siteType } = useParams();
29
+
30
+ const { isAnswering, selectedSiteTypeId, questionId } = useSelect(
31
+ ( select ) => ( {
32
+ isAnswering: select( STORE_NAME ).isAnswering(),
33
+ selectedSiteTypeId: select( STORE_NAME ).getSelectedSiteTypeId(),
34
+ questionId: select( STORE_NAME ).getNextQuestion()?.id,
35
+ } )
36
+ );
37
+ const { selectSiteType, applyAnswerResponse } = useDispatch( STORE_NAME );
38
+ const next = useNextQuestion();
39
+
40
+ useLayoutEffect( () => {
41
+ if ( selectedSiteTypeId !== siteType ) {
42
+ selectSiteType( siteType );
43
+ }
44
+ }, [ selectedSiteTypeId, siteType ] );
45
+
46
+ useEffect( () => {
47
+ if ( next === null ) {
48
+ applyAnswerResponse();
49
+ goNext();
50
+ }
51
+ }, [ next ] );
52
+
53
+ if ( isAnswering ) {
54
+ return <Disabled>{ next }</Disabled>;
55
+ }
56
+
57
+ if ( next ) {
58
+ return (
59
+ <>
60
+ { next }
61
+ <HelpFill>
62
+ <PageHeader
63
+ title={ __( 'Site Type', 'better-wp-security' ) }
64
+ breadcrumbs={ false }
65
+ />
66
+ <HelpList
67
+ topic={ `site-type-${ questionId }` }
68
+ fallback="site-type"
69
+ />
70
+ </HelpFill>
71
+ </>
72
+ );
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ function useNextQuestion() {
79
+ const { answerQuestion, repeatQuestion } = useDispatch( STORE_NAME );
80
+ const question = useSelect( ( select ) =>
81
+ select( STORE_NAME ).getNextQuestion()
82
+ );
83
+ const id = question?.id;
84
+ const component = useSelect(
85
+ ( select ) => select( STORE_NAME ).getQuestionComponent( id ),
86
+ [ id ]
87
+ );
88
+
89
+ if ( ! question ) {
90
+ return question;
91
+ }
92
+
93
+ const Component = memo( component || SchemaQuestion );
94
+
95
+ return (
96
+ <Component
97
+ question={ question }
98
+ onAnswer={ answerQuestion }
99
+ goBack={ repeatQuestion }
100
+ />
101
+ );
102
+ }
core/admin-pages/entries/settings/pages/site-type/questions/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/site-type/questions/question.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { useSelect, useDispatch } from '@wordpress/data';
6
+ import { useState } from '@wordpress/element';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import { ErrorList } from '@ithemes/security-components';
12
+ import {
13
+ PrimarySchemaForm,
14
+ PageHeader,
15
+ Breadcrumbs,
16
+ } from '../../../components';
17
+ import { ONBOARD_STORE_NAME } from '../../../stores';
18
+
19
+ const formContext = {
20
+ disableInlineErrors: true,
21
+ };
22
+
23
+ export default function Question( {
24
+ prompt,
25
+ description,
26
+ showErrors = true,
27
+ children,
28
+ } ) {
29
+ const { error, siteTypeTitle } = useSelect( ( select ) => ( {
30
+ error: select( ONBOARD_STORE_NAME ).getLastError(),
31
+ siteTypeTitle: select( ONBOARD_STORE_NAME ).getSelectedSiteType()
32
+ ?.title,
33
+ } ) );
34
+
35
+ return (
36
+ <>
37
+ <PageHeader
38
+ title={ prompt }
39
+ subtitle={ description }
40
+ breadcrumbs={ <Breadcrumbs title={ siteTypeTitle } /> }
41
+ />
42
+ { showErrors && (
43
+ <ErrorList
44
+ apiError={ error }
45
+ className="itsec-site-type-question__error-list"
46
+ />
47
+ ) }
48
+ { children }
49
+ </>
50
+ );
51
+ }
52
+
53
+ export function SchemaQuestion( { question, onAnswer, goBack } ) {
54
+ const { editAnswer } = useDispatch( ONBOARD_STORE_NAME );
55
+ const { answer, error } = useSelect( ( select ) => ( {
56
+ error: select( ONBOARD_STORE_NAME ).getLastError(),
57
+ answer: select( ONBOARD_STORE_NAME ).getEditedAnswer(),
58
+ } ) );
59
+ const [ schemaError, setSchemaError ] = useState( [] );
60
+
61
+ return (
62
+ <Question
63
+ prompt={ question.prompt }
64
+ description={ question.description }
65
+ showErrors={ false }
66
+ >
67
+ <PrimarySchemaForm
68
+ schema={ question.answer_schema }
69
+ uiSchema={ question.answer_schema.uiSchema }
70
+ formData={ answer }
71
+ onChange={ ( { formData: changedData } ) =>
72
+ editAnswer( changedData )
73
+ }
74
+ onSubmit={ ( { formData: submittedData }, e ) => {
75
+ e.preventDefault();
76
+ setSchemaError( [] );
77
+ onAnswer( submittedData );
78
+ } }
79
+ saveLabel={ __( 'Next', 'better-wp-security' ) }
80
+ cancelLabel={ __( 'Back', 'better-wp-security' ) }
81
+ onCancel={ goBack }
82
+ formContext={ formContext }
83
+ apiError={ error }
84
+ schemaError={ schemaError }
85
+ onError={ setSchemaError }
86
+ showErrorList={ false }
87
+ />
88
+ </Question>
89
+ );
90
+ }
core/admin-pages/entries/settings/pages/site-type/questions/questions/index.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useDispatch } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { useSingletonEffect } from '@ithemes/security-hocs';
10
+ import { STORE_NAME } from '../../../../stores/onboard';
11
+ import IsClient from './is-client';
12
+
13
+ export default function useQuestions() {
14
+ const { registerQuestionComponent } = useDispatch( STORE_NAME );
15
+
16
+ useSingletonEffect( useQuestions, () => {
17
+ registerQuestionComponent( 'is-client', IsClient );
18
+ } );
19
+ }
core/admin-pages/entries/settings/pages/site-type/questions/questions/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/site-type/questions/questions/is-client.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { SelectableCard } from '../../../../components';
10
+ import Question from '../question';
11
+
12
+ export default function IsClient( { question, onAnswer } ) {
13
+ return (
14
+ <Question
15
+ prompt={ question.prompt }
16
+ description={ question.description }
17
+ >
18
+ <div className="itsec-site-type-questions-list">
19
+ <SelectableCard
20
+ onClick={ () => onAnswer( false ) }
21
+ title={ __( 'Self', 'better-wp-security' ) }
22
+ description={ __(
23
+ 'This is my own personal site.',
24
+ 'better-wp-security'
25
+ ) }
26
+ direction="vertical"
27
+ icon="admin-users"
28
+ className="itsec-site-type-question"
29
+ />
30
+ <SelectableCard
31
+ onClick={ () => onAnswer( true ) }
32
+ title={ __( 'Client', 'better-wp-security' ) }
33
+ description={ __(
34
+ "I'm making this for a client.",
35
+ 'better-wp-security'
36
+ ) }
37
+ direction="vertical"
38
+ icon="businessperson"
39
+ className="itsec-site-type-question"
40
+ />
41
+ </div>
42
+ </Question>
43
+ );
44
+ }
core/admin-pages/entries/settings/pages/site-type/questions/style.scss ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-site-type-questions-list {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fit, minmax(min-content, 250px));
4
+ grid-gap: 1rem;
5
+ }
6
+
7
+ .itsec-site-type-question .components-card {
8
+ width: 15rem;
9
+ }
10
+
11
+ .itsec-message-list.itsec-site-type-question__error-list {
12
+ margin-bottom: 2rem;
13
+ }
core/admin-pages/entries/settings/pages/tools/index.js ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import { mapValues } from 'lodash';
6
+ import { useLocation } from 'react-router-dom';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import { __ } from '@wordpress/i18n';
12
+ import { useEffect, useMemo, useState, Children } from '@wordpress/element';
13
+ import {
14
+ Card,
15
+ Button,
16
+ Slot,
17
+ Fill,
18
+ Flex,
19
+ FlexItem,
20
+ } from '@wordpress/components';
21
+ import { useInstanceId } from '@wordpress/compose';
22
+ import { useSelect, useDispatch } from '@wordpress/data';
23
+
24
+ /**
25
+ * Internal dependencies
26
+ */
27
+ import {
28
+ Accordion,
29
+ MessageList,
30
+ ErrorList,
31
+ HelpList,
32
+ Markup,
33
+ Spinner,
34
+ } from '@ithemes/security-components';
35
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
36
+ import {
37
+ HelpFill,
38
+ PageHeader,
39
+ PrimarySchemaFormInputs,
40
+ } from '../../components';
41
+ import { TOOLS_STORE_NAME } from '../../stores';
42
+ import { getAjv } from '../../utils';
43
+ import './style.scss';
44
+
45
+ export default function Tools() {
46
+ const { hash } = useLocation();
47
+ const { tools, running, activeModules, isLoaded } = useSelect(
48
+ ( select ) => ( {
49
+ tools: select( TOOLS_STORE_NAME ).getResolvedTools(),
50
+ running: select( TOOLS_STORE_NAME ).getRunning(),
51
+ activeModules: select( MODULES_STORE_NAME ).getActiveModules(),
52
+ isLoaded: select( TOOLS_STORE_NAME ).hasFinishedResolution(
53
+ 'getTools'
54
+ ),
55
+ } )
56
+ );
57
+ const panels = useMemo(
58
+ () =>
59
+ tools
60
+ .filter(
61
+ ( tool ) =>
62
+ activeModules.includes( tool.module ) &&
63
+ tool.available !== false
64
+ )
65
+ .map( ( tool ) => ( {
66
+ name: tool.slug,
67
+ title: tool.title,
68
+ description: tool.description,
69
+ showSpinner: (
70
+ <Spinner
71
+ size={ 30 }
72
+ paused={ ! running.includes( tool.slug ) }
73
+ />
74
+ ),
75
+ render: ToolPanel,
76
+ } ) ),
77
+ [ tools, running, activeModules ]
78
+ );
79
+ const [ expanded, setExpanded ] = useState( '' );
80
+
81
+ useEffect( () => {
82
+ if ( hash ) {
83
+ setExpanded( hash.slice( 1 ) );
84
+ }
85
+ }, [ hash ] );
86
+
87
+ return (
88
+ <>
89
+ <HelpFill>
90
+ <PageHeader title={ __( 'Tools', 'better-wp-security' ) } />
91
+ <HelpList topic="tools" />
92
+ </HelpFill>
93
+ <PageHeader title={ __( 'Tools', 'better-wp-security' ) } />
94
+ { tools.length > 0 && isLoaded && (
95
+ <Card>
96
+ <Accordion
97
+ className="itsec-tools-list"
98
+ isStyled
99
+ panels={ panels }
100
+ allowNone
101
+ expanded={ expanded }
102
+ setExpanded={ setExpanded }
103
+ />
104
+ </Card>
105
+ ) }
106
+ </>
107
+ );
108
+ }
109
+
110
+ function ToolPanel( { name, className, ...rest } ) {
111
+ const { tool, result, isRunning } = useSelect( ( select ) => ( {
112
+ tool: select( TOOLS_STORE_NAME ).getTool( name ),
113
+ result: select( TOOLS_STORE_NAME ).getLastResult( name ),
114
+ isRunning: select( TOOLS_STORE_NAME ).isRunning( name ),
115
+ } ) );
116
+
117
+ const [ schemaError, setSchemaError ] = useState( [] );
118
+ useEffect( () => setSchemaError( [] ), [ name ] );
119
+
120
+ return (
121
+ <div className={ classnames( className, 'itsec-tool' ) } { ...rest }>
122
+ <ResultSummary result={ result } schemaError={ schemaError } />
123
+ { tool.help && <Markup content={ tool.help } tagName="p" /> }
124
+
125
+ <ToolSlot tool={ tool.slug } />
126
+
127
+ { tool.toggleable ? (
128
+ <></>
129
+ ) : (
130
+ <RunTool
131
+ tool={ tool }
132
+ setSchemaError={ setSchemaError }
133
+ isRunning={ isRunning }
134
+ />
135
+ ) }
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function ResultSummary( { result, schemaError } ) {
141
+ return (
142
+ <>
143
+ <ErrorList schemaError={ schemaError } apiError={ result?.error } />
144
+ <MessageList messages={ result?.success } type="success" />
145
+ <MessageList messages={ result?.warning } type="warning" />
146
+ <MessageList messages={ result?.info } type="info" />
147
+ </>
148
+ );
149
+ }
150
+
151
+ function RunTool( { tool, setSchemaError, isRunning } ) {
152
+ const isActive = useIsToolConditionActive( tool );
153
+ const id = useInstanceId( RunTool, 'itsec-tool-form' );
154
+ const formContext = useMemo( () => ( {
155
+ disableInlineErrors: true,
156
+ tool: tool.slug,
157
+ } ) );
158
+ const { runTool } = useDispatch( TOOLS_STORE_NAME );
159
+ const onSubmit = ( { formData } ) => {
160
+ setSchemaError( [] );
161
+ runTool( tool.slug, formData );
162
+ };
163
+
164
+ return (
165
+ <>
166
+ { tool.form && isActive && (
167
+ <PrimarySchemaFormInputs
168
+ id={ id }
169
+ idPrefix={ `itsec_tool_${ tool.slug }` }
170
+ schema={ tool.form }
171
+ uiSchema={ tool.form.uiSchema }
172
+ formContext={ formContext }
173
+ showErrorList={ false }
174
+ onError={ setSchemaError }
175
+ onSubmit={ onSubmit }
176
+ />
177
+ ) }
178
+ <Flex className="itsec-tool__actions" justify="flex-start">
179
+ <FlexItem>
180
+ <Button
181
+ isPrimary
182
+ className="itsec-tool__trigger"
183
+ type={ tool.form ? 'submit' : 'button' }
184
+ form={ tool.form ? id : undefined }
185
+ onClick={
186
+ tool.form ? undefined : () => runTool( tool.slug )
187
+ }
188
+ isBusy={ isRunning }
189
+ disabled={ ! isActive }
190
+ >
191
+ { __( 'Run', 'better-wp-security' ) }
192
+ </Button>
193
+ </FlexItem>
194
+ <ToolSlot
195
+ tool={ tool.slug }
196
+ fillProps={ { isActive } }
197
+ area="actions"
198
+ >
199
+ { ( fills ) =>
200
+ Children.map( fills, ( child, i ) => (
201
+ <FlexItem key={ i }>{ child }</FlexItem>
202
+ ) )
203
+ }
204
+ </ToolSlot>
205
+ </Flex>
206
+
207
+ { ! isActive && tool.condition?.description && (
208
+ <MessageList
209
+ type="warning"
210
+ messages={ [ tool.condition.description ] }
211
+ />
212
+ ) }
213
+ </>
214
+ );
215
+ }
216
+
217
+ export function ToolFill( { tool, area = 'main', ...props } ) {
218
+ return <Fill name={ `Tool${ area }${ tool }` } { ...props } />;
219
+ }
220
+
221
+ function ToolSlot( { tool, area = 'main', ...props } ) {
222
+ return <Slot name={ `Tool${ area }${ tool }` } { ...props } />;
223
+ }
224
+
225
+ function useIsToolConditionActive( tool ) {
226
+ const { activeModules, settings } = useSelect(
227
+ ( select ) => ( {
228
+ settings: mapValues(
229
+ tool.condition?.settings || {},
230
+ ( value, key ) =>
231
+ select( MODULES_STORE_NAME ).getSettings( key )
232
+ ),
233
+ activeModules: select( MODULES_STORE_NAME ).getActiveModules(),
234
+ } ),
235
+ [ tool ]
236
+ );
237
+
238
+ if ( ! tool.condition ) {
239
+ return true;
240
+ }
241
+
242
+ if ( tool.condition[ 'active-modules' ] ) {
243
+ for ( const activeModule of tool.condition[ 'active-modules' ] ) {
244
+ if ( ! activeModules.includes( activeModule ) ) {
245
+ return false;
246
+ }
247
+ }
248
+ }
249
+
250
+ if ( tool.condition.settings ) {
251
+ const ajv = getAjv();
252
+
253
+ for ( const [ module, schema ] of Object.entries(
254
+ tool.condition.settings
255
+ ) ) {
256
+ const validate = ajv.compile( schema );
257
+
258
+ if ( ! validate( settings[ module ] ) ) {
259
+ return false;
260
+ }
261
+ }
262
+ }
263
+
264
+ return true;
265
+ }
core/admin-pages/entries/settings/pages/tools/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/pages/tools/style.scss ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ .itsec-tool {
2
+ .rjsf {
3
+ margin-bottom: 1rem;
4
+ }
5
+
6
+ .itsec-tool__actions + .itsec-message-list {
7
+ margin-top: 1rem;
8
+ }
9
+ }
core/admin-pages/entries/settings/search.js ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { reduce } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useDispatch } from '@wordpress/data';
10
+ import { __ } from '@wordpress/i18n';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { useSingletonEffect } from '@ithemes/security-hocs';
16
+ import { STORE_NAME } from '@ithemes/security-search';
17
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
18
+ import { TOOLS_STORE_NAME } from './stores';
19
+
20
+ export default function useSearchProviders() {
21
+ const { registerProvider } = useDispatch( STORE_NAME );
22
+
23
+ useSingletonEffect( useSearchProviders, () => {
24
+ registerProvider(
25
+ 'modules',
26
+ __( 'Features', 'better-wp-security' ),
27
+ 5,
28
+ ( { registry, evaluate, results } ) => {
29
+ const modules = registry
30
+ .select( MODULES_STORE_NAME )
31
+ .getEditedModules();
32
+
33
+ return modules.reduce( ( count, module ) => {
34
+ const moduleRoute = getModuleRoute( module );
35
+
36
+ if ( ! moduleRoute ) {
37
+ return count;
38
+ }
39
+
40
+ if (
41
+ ! evaluate.stringMatch( module.title ) &&
42
+ ! evaluate.stringMatch( module.description ) &&
43
+ ! evaluate.keywordMatch( module.keywords )
44
+ ) {
45
+ return count;
46
+ }
47
+
48
+ results.items.push( {
49
+ title: module.title,
50
+ description: module.description,
51
+ route: moduleRoute,
52
+ } );
53
+
54
+ return count + 1;
55
+ }, 0 );
56
+ }
57
+ );
58
+
59
+ registerProvider(
60
+ 'settings',
61
+ __( 'Settings', 'better-wp-security' ),
62
+ 20,
63
+ ( { registry, evaluate, results } ) => {
64
+ const modules = registry
65
+ .select( MODULES_STORE_NAME )
66
+ .getEditedModules();
67
+
68
+ return modules.reduce( ( total, module ) => {
69
+ if (
70
+ module.status.selected !== 'active' ||
71
+ ! module.settings?.interactive?.length
72
+ ) {
73
+ return total;
74
+ }
75
+
76
+ const moduleRoute = getModuleRoute( module );
77
+
78
+ if ( ! moduleRoute ) {
79
+ return total;
80
+ }
81
+
82
+ return (
83
+ total +
84
+ module.settings.interactive.reduce(
85
+ ( count, setting ) => {
86
+ const schema =
87
+ module.settings.schema.properties[
88
+ setting
89
+ ];
90
+ const uiSchema =
91
+ module.settings.schema.uiSchema?.[
92
+ setting
93
+ ];
94
+
95
+ if ( ! schema ) {
96
+ return count;
97
+ }
98
+
99
+ const title =
100
+ uiSchema?.ui?.title ||
101
+ uiSchema?.[ 'ui:title' ] ||
102
+ schema.title;
103
+ const description =
104
+ uiSchema?.ui?.description ||
105
+ uiSchema?.[ 'ui:description' ] ||
106
+ schema.description;
107
+
108
+ if (
109
+ ! evaluate.stringMatch( title ) &&
110
+ ! evaluate.stringMatch( description ) &&
111
+ ! evaluate.keywordMatch( schema.keywords )
112
+ ) {
113
+ return count;
114
+ }
115
+
116
+ results.groups[ module.id ] ??= {
117
+ title: module.title,
118
+ items: [],
119
+ };
120
+
121
+ results.groups[ module.id ].items.push( {
122
+ title,
123
+ description,
124
+ route: `${ moduleRoute }#${ setting }`,
125
+ } );
126
+
127
+ return count++;
128
+ },
129
+ 0
130
+ )
131
+ );
132
+ }, 0 );
133
+ }
134
+ );
135
+
136
+ registerProvider(
137
+ 'tools',
138
+ __( 'Tools', 'better-wp-security' ),
139
+ 100,
140
+ ( { registry, evaluate, results } ) => {
141
+ const tools = registry
142
+ .select( TOOLS_STORE_NAME )
143
+ .getResolvedTools();
144
+
145
+ return tools.reduce( ( total, tool ) => {
146
+ if ( ! tool.available ) {
147
+ return total;
148
+ }
149
+
150
+ if (
151
+ ! evaluate.stringMatch( tool.title ) &&
152
+ ! evaluate.stringMatch( tool.description ) &&
153
+ ! evaluate.keywordMatch( tool.keywords )
154
+ ) {
155
+ return total;
156
+ }
157
+
158
+ results.items.push( {
159
+ title: tool.title,
160
+ description: tool.description,
161
+ route: `/settings/tools#${ tool.slug }`,
162
+ } );
163
+
164
+ return total + 1;
165
+ }, 0 );
166
+ }
167
+ );
168
+ } );
169
+ }
170
+
171
+ function getModuleRoute( module ) {
172
+ if ( module.type === 'custom' || module.type === 'tool' ) {
173
+ return;
174
+ }
175
+
176
+ const featureLink = `/settings/modules/${ module.type }#${ module.id }`;
177
+
178
+ if ( module.status.selected === 'inactive' ) {
179
+ return featureLink;
180
+ }
181
+
182
+ if ( module.settings?.interactive.length > 0 ) {
183
+ return module.type === 'recommended'
184
+ ? `/settings/configure/${ module.id }`
185
+ : `/settings/configure/${ module.type }/${ module.id }`;
186
+ }
187
+
188
+ if ( module.status.default !== 'always-active' ) {
189
+ return featureLink;
190
+ }
191
+ }
core/admin-pages/entries/settings/stores/controls.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { uniqueId } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { default as triggerApiFetch } from '@wordpress/api-fetch';
10
+ import { createRegistryControl } from '@wordpress/data';
11
+ import { doAction as doActionHook } from '@wordpress/hooks';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { responseToError } from '@ithemes/security-utils';
17
+
18
+ /**
19
+ * Trigger an API Fetch request.
20
+ *
21
+ * @param {Object} request API Fetch Request Object.
22
+ * @return {Object} control descriptor.
23
+ */
24
+ export function apiFetch( request ) {
25
+ return {
26
+ type: 'API_FETCH',
27
+ request,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Yields action objects used in signalling that a notice is to be created.
33
+ *
34
+ * @see @wordpress/notices#createNotice()
35
+ *
36
+ * @param {?string} status Notice status.
37
+ * Defaults to `info`.
38
+ * @param {string} content Notice message.
39
+ * @param {?Object} options Notice options.
40
+ * @param {?string} options.context Context under which to
41
+ * group notice.
42
+ * @param {?string} options.id Identifier for notice.
43
+ * Automatically assigned
44
+ * if not specified.
45
+ * @param {?boolean} options.isDismissible Whether the notice can
46
+ * be dismissed by user.
47
+ * Defaults to `true`.
48
+ * @param {?number} options.autoDismiss Whether the notice should
49
+ * by automatically dismissed
50
+ * after x milliseconds.
51
+ * Defaults to `false`.
52
+ * @param {?string} options.type Notice type. Either 'default' or 'snackbar'.
53
+ * @param {?Array<Object>} options.actions User actions to be
54
+ * presented with notice.
55
+ *
56
+ * @return {Object} control descriptor.
57
+ */
58
+ export function createNotice( status = 'info', content, options = {} ) {
59
+ return {
60
+ type: 'CREATE_NOTICE',
61
+ status,
62
+ content,
63
+ options: {
64
+ context: 'ithemes-security',
65
+ ...options,
66
+ },
67
+ };
68
+ }
69
+
70
+ export function awaitPromise( promise, delay ) {
71
+ return {
72
+ type: 'AWAIT_PROMISE',
73
+ promise,
74
+ delay,
75
+ };
76
+ }
77
+
78
+ export function doAction( action, ...args ) {
79
+ return {
80
+ type: 'DO_ACTION',
81
+ action,
82
+ args,
83
+ };
84
+ }
85
+
86
+ function timeout( ms ) {
87
+ return new Promise( ( resolve ) => setTimeout( resolve, ms ) );
88
+ }
89
+
90
+ const controls = {
91
+ AWAIT_PROMISE: ( { promise, delay } ) => {
92
+ if ( delay ) {
93
+ return Promise.all( [ promise, timeout( delay ) ] );
94
+ }
95
+
96
+ return promise;
97
+ },
98
+ DO_ACTION: createRegistryControl( ( registry ) => ( { action, args } ) => {
99
+ doActionHook( `ithemes-security.${ action }`, registry, ...args );
100
+ } ),
101
+ API_FETCH( { request } ) {
102
+ return triggerApiFetch( request ).catch( responseToError );
103
+ },
104
+ CREATE_NOTICE: createRegistryControl(
105
+ ( registry ) => ( { status, content, options } ) => {
106
+ if ( options.autoDismiss ) {
107
+ options.id = options.id || uniqueId( 'itsec-auto-dismiss-' );
108
+ setTimeout(
109
+ () =>
110
+ registry
111
+ .dispatch( 'core/notices' )
112
+ .removeNotice( options.id, options.context ),
113
+ options.autoDismiss
114
+ );
115
+ }
116
+
117
+ registry
118
+ .dispatch( 'core/notices' )
119
+ .createNotice( status, content, options );
120
+ }
121
+ ),
122
+ };
123
+
124
+ export default controls;
core/admin-pages/entries/settings/stores/index.js ADDED
@@ -0,0 +1,2 @@
 
 
1
+ export { STORE_NAME as ONBOARD_STORE_NAME } from './onboard';
2
+ export { STORE_NAME as TOOLS_STORE_NAME } from './tools';
core/admin-pages/entries/settings/stores/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/stores/onboard/actions.js ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { find, sortBy, random, trimEnd } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { controls } from '@wordpress/data';
10
+ import { __, sprintf } from '@wordpress/i18n';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
16
+ import { apiFetch, createNotice, awaitPromise, doAction } from '../controls';
17
+ import { STORE_NAME } from './';
18
+
19
+ export function* selectSiteType( id ) {
20
+ const siteTypes = yield controls.resolveSelect(
21
+ STORE_NAME,
22
+ 'getSiteTypes'
23
+ );
24
+ const siteType = find(
25
+ siteTypes,
26
+ ( maybeSiteType ) => maybeSiteType.id === id
27
+ );
28
+
29
+ if ( ! siteType ) {
30
+ throw __( 'No site type found with that id.', 'better-wp-security' );
31
+ }
32
+
33
+ yield receiveSiteType( siteType );
34
+ yield resetOnboarding();
35
+ }
36
+
37
+ export function clearSiteType() {
38
+ return { type: CLEAR_SITE_TYPE };
39
+ }
40
+
41
+ export function editAnswer( answer ) {
42
+ return {
43
+ type: EDIT_ANSWER,
44
+ answer,
45
+ };
46
+ }
47
+
48
+ export function* answerQuestion( answer ) {
49
+ const request = yield controls.select(
50
+ STORE_NAME,
51
+ 'getAnswerRequest',
52
+ answer
53
+ );
54
+ let response;
55
+
56
+ yield setIsAnswering();
57
+
58
+ try {
59
+ response = yield apiFetch( {
60
+ method: 'PUT',
61
+ path: `/ithemes-security/v1/site-types/${ request.id }`,
62
+ data: request,
63
+ } );
64
+ yield receiveSiteType( response );
65
+ } catch ( e ) {
66
+ yield setError( e );
67
+ }
68
+
69
+ yield setIsAnswering( false );
70
+ }
71
+
72
+ export function* repeatQuestion() {
73
+ const request = yield controls.select(
74
+ STORE_NAME,
75
+ 'getRestoreSiteTypeRequest'
76
+ );
77
+ request.answers.pop();
78
+
79
+ yield setIsAnswering();
80
+ const response = yield apiFetch( {
81
+ method: 'PUT',
82
+ path: `/ithemes-security/v1/site-types/${ request.id }`,
83
+ data: request,
84
+ } );
85
+
86
+ yield receiveSiteType( response );
87
+ yield setIsAnswering( false );
88
+ }
89
+
90
+ export function* applyAnswerResponse() {
91
+ const answers = yield controls.select( STORE_NAME, 'getAnswers' );
92
+
93
+ for ( const answer of answers ) {
94
+ for ( const module of answer.modules ) {
95
+ yield controls.dispatch( MODULES_STORE_NAME, 'editModule', module, {
96
+ status: {
97
+ selected: 'active',
98
+ },
99
+ } );
100
+ }
101
+
102
+ for ( const module in answer.settings ) {
103
+ if ( answer.settings.hasOwnProperty( module ) ) {
104
+ yield controls.dispatch(
105
+ MODULES_STORE_NAME,
106
+ 'editSettings',
107
+ module,
108
+ answer.settings[ module ]
109
+ );
110
+ }
111
+ }
112
+
113
+ yield doAction( 'onboard.applyAnswerResponse', answer );
114
+ }
115
+ }
116
+
117
+ export function* resetOnboarding() {
118
+ yield editAnswer( null );
119
+ yield controls.dispatch( MODULES_STORE_NAME, 'resetModuleEdits' );
120
+ yield controls.dispatch( MODULES_STORE_NAME, 'resetSettingEdits' );
121
+ yield doAction( 'onboard.reset' );
122
+ }
123
+
124
+ export function* completeOnboarding() {
125
+ const throwIf = ( maybeError ) => {
126
+ if ( maybeError instanceof Error ) {
127
+ throw maybeError;
128
+ }
129
+ };
130
+
131
+ const steps = sortBy(
132
+ yield controls.select( STORE_NAME, 'getCompletionSteps' ),
133
+ 'priority'
134
+ );
135
+
136
+ try {
137
+ for ( const step of steps ) {
138
+ yield { type: SET_COMPLETION_STEP, step };
139
+ const callback = step.callback();
140
+
141
+ if ( callback instanceof Promise ) {
142
+ throwIf( yield awaitPromise( callback, random( 1500, 2500 ) ) );
143
+ }
144
+ }
145
+
146
+ yield controls.dispatch(
147
+ MODULES_STORE_NAME,
148
+ 'editSetting',
149
+ 'global',
150
+ 'onboard_complete',
151
+ true
152
+ );
153
+ yield controls.dispatch( MODULES_STORE_NAME, 'saveSettings', 'global' );
154
+
155
+ yield { type: SET_COMPLETION_STEP, step: true };
156
+ } catch ( error ) {
157
+ yield createNotice(
158
+ 'error',
159
+ sprintf(
160
+ /* translators: 1. Error message */
161
+ __( 'Could not complete setup: %s', 'better-wp-security' ),
162
+ error.message
163
+ )
164
+ );
165
+ }
166
+ }
167
+
168
+ export function receiveSiteTypes( siteTypes ) {
169
+ return {
170
+ type: RECEIVE_SITE_TYPES,
171
+ siteTypes,
172
+ };
173
+ }
174
+
175
+ export function receiveSiteType( siteType ) {
176
+ return {
177
+ type: RECEIVE_SITE_TYPE,
178
+ siteType,
179
+ };
180
+ }
181
+
182
+ function setIsAnswering( isAnswering = true ) {
183
+ return {
184
+ type: SET_IS_ANSWERING,
185
+ isAnswering,
186
+ };
187
+ }
188
+
189
+ export function registerQuestionComponent( id, component ) {
190
+ return {
191
+ type: REGISTER_QUESTION_COMPONENT,
192
+ id,
193
+ component,
194
+ };
195
+ }
196
+
197
+ export function registerCompletionStep( {
198
+ id,
199
+ label,
200
+ priority,
201
+ render,
202
+ callback,
203
+ } ) {
204
+ return {
205
+ type: REGISTER_COMPLETION_STEP,
206
+ id,
207
+ label,
208
+ priority,
209
+ render,
210
+ callback,
211
+ };
212
+ }
213
+
214
+ export function recordVisitedLocation( location ) {
215
+ return {
216
+ type: RECORD_VISITED_LOCATION,
217
+ location: trimEnd( location, '/' ),
218
+ };
219
+ }
220
+
221
+ export function clearVisitedLocations() {
222
+ return {
223
+ type: CLEAR_VISITED_LOCATIONS,
224
+ };
225
+ }
226
+
227
+ function setError( error ) {
228
+ return {
229
+ type: SET_ERROR,
230
+ error,
231
+ };
232
+ }
233
+
234
+ export const RECEIVE_SITE_TYPES = 'RECEIVE_SITE_TYPES';
235
+ export const RECEIVE_SITE_TYPE = 'RECEIVE_SITE_TYPE';
236
+ export const SELECT_SITE_TYPE = 'SELECT_SITE_TYPE';
237
+ export const CLEAR_SITE_TYPE = 'CLEAR_SITE_TYPE';
238
+ export const EDIT_ANSWER = 'EDIT_ANSWER';
239
+ export const SET_IS_ANSWERING = 'SET_IS_ANSWERING';
240
+ export const SET_ERROR = 'SET_ERROR';
241
+ export const REGISTER_QUESTION_COMPONENT = 'REGISTER_QUESTION_COMPONENT';
242
+ export const REGISTER_COMPLETION_STEP = 'REGISTER_COMPLETION_STEP';
243
+ export const SET_COMPLETION_STEP = 'SET_COMPLETION_STEP';
244
+ export const RECORD_VISITED_LOCATION = 'RECORD_VISITED_LOCATION';
245
+ export const CLEAR_VISITED_LOCATIONS = 'CLEAR_VISITED_LOCATIONS';
core/admin-pages/entries/settings/stores/onboard/index.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { registerStore } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import controls from '../controls';
10
+ import * as actions from './actions';
11
+ import * as selectors from './selectors';
12
+ import reducer from './reducers';
13
+ import * as resolvers from './resolvers';
14
+
15
+ export const STORE_NAME = 'ithemes-security/onboard';
16
+
17
+ const store = registerStore( STORE_NAME, {
18
+ controls,
19
+ actions,
20
+ selectors,
21
+ resolvers,
22
+ reducer,
23
+ persist: [ 'selectedSiteType', 'answers', 'visitedLocations' ],
24
+ } );
25
+
26
+ export default store;
core/admin-pages/entries/settings/stores/onboard/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/stores/onboard/reducers.js ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { omit, without, last } from 'lodash';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import {
10
+ RECEIVE_SITE_TYPES,
11
+ RECEIVE_SITE_TYPE,
12
+ SELECT_SITE_TYPE,
13
+ SET_IS_ANSWERING,
14
+ REGISTER_QUESTION_COMPONENT,
15
+ CLEAR_SITE_TYPE,
16
+ SET_COMPLETION_STEP,
17
+ REGISTER_COMPLETION_STEP,
18
+ EDIT_ANSWER,
19
+ SET_ERROR,
20
+ RECORD_VISITED_LOCATION,
21
+ CLEAR_VISITED_LOCATIONS,
22
+ } from './actions';
23
+
24
+ const DEFAULT_STATE = {
25
+ siteTypes: [],
26
+ selectedSiteType: '',
27
+ answers: [],
28
+ nextQuestion: undefined,
29
+ editedAnswer: undefined,
30
+ lastError: undefined,
31
+ isAnswering: false,
32
+ questionComponents: {},
33
+ completionStep: false,
34
+ completionSteps: {},
35
+ visitedLocations: [],
36
+ };
37
+
38
+ export default function ( state = DEFAULT_STATE, action ) {
39
+ switch ( action.type ) {
40
+ case RECEIVE_SITE_TYPES:
41
+ return {
42
+ ...state,
43
+ siteTypes: action.siteTypes,
44
+ };
45
+ case RECEIVE_SITE_TYPE:
46
+ return {
47
+ ...state,
48
+ selectedSiteType: action.siteType.id,
49
+ nextQuestion: action.siteType.next_question,
50
+ answers: action.siteType.answers,
51
+ editedAnswer:
52
+ action.siteType.next_question?.answer_schema?.default,
53
+ lastError: undefined,
54
+ };
55
+ case SELECT_SITE_TYPE:
56
+ return {
57
+ ...state,
58
+ selectedSiteType: action.id,
59
+ answers: [],
60
+ nextQuestion: undefined,
61
+ lastError: undefined,
62
+ };
63
+ case CLEAR_SITE_TYPE:
64
+ return {
65
+ ...state,
66
+ selectedSiteType: '',
67
+ answers: [],
68
+ nextQuestion: undefined,
69
+ lastError: undefined,
70
+ };
71
+ case EDIT_ANSWER:
72
+ return {
73
+ ...state,
74
+ editedAnswer: action.answer,
75
+ };
76
+ case SET_IS_ANSWERING:
77
+ return {
78
+ ...state,
79
+ isAnswering: action.isAnswering,
80
+ };
81
+ case SET_ERROR:
82
+ return {
83
+ ...state,
84
+ lastError: action.error,
85
+ };
86
+ case SET_COMPLETION_STEP:
87
+ return {
88
+ ...state,
89
+ completionStep: action.step,
90
+ };
91
+ case REGISTER_QUESTION_COMPONENT:
92
+ return {
93
+ ...state,
94
+ questionComponents: {
95
+ ...state.questionComponents,
96
+ [ action.id ]: action.component,
97
+ },
98
+ };
99
+ case REGISTER_COMPLETION_STEP:
100
+ return {
101
+ ...state,
102
+ completionSteps: {
103
+ ...state.completionSteps,
104
+ [ action.id ]: omit( action, [ 'type' ] ),
105
+ },
106
+ };
107
+ case RECORD_VISITED_LOCATION:
108
+ return {
109
+ ...state,
110
+ visitedLocations:
111
+ last( state.visitedLocations ) === action.location
112
+ ? state.visitedLocations
113
+ : without(
114
+ state.visitedLocations,
115
+ action.location
116
+ ).concat( action.location ),
117
+ };
118
+ case CLEAR_VISITED_LOCATIONS:
119
+ return {
120
+ ...state,
121
+ visitedLocations: [],
122
+ };
123
+ default:
124
+ return state;
125
+ }
126
+ }
core/admin-pages/entries/settings/stores/onboard/resolvers.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { controls } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { apiFetch } from '../controls';
10
+ import { receiveSiteTypes, receiveSiteType } from './actions';
11
+ import { STORE_NAME } from './';
12
+
13
+ export const getSiteTypes = {
14
+ *fulfill() {
15
+ const siteTypes = yield apiFetch( {
16
+ path: '/ithemes-security/v1/site-types',
17
+ } );
18
+ yield receiveSiteTypes( siteTypes );
19
+ },
20
+ isFulfilled( state ) {
21
+ return state.siteTypes.length > 0;
22
+ },
23
+ };
24
+
25
+ export const getSelectedSiteType = {
26
+ *fulfill() {
27
+ yield controls.resolveSelect( STORE_NAME, 'getSiteTypes' );
28
+ },
29
+ isFulfilled( state ) {
30
+ return state.siteTypes.length > 0;
31
+ },
32
+ };
33
+
34
+ export const getNextQuestion = {
35
+ *fulfill() {
36
+ const request = yield controls.select(
37
+ STORE_NAME,
38
+ 'getRestoreSiteTypeRequest'
39
+ );
40
+ const response = yield apiFetch( {
41
+ method: 'PUT',
42
+ path: `/ithemes-security/v1/site-types/${ request.id }`,
43
+ data: request,
44
+ } );
45
+ yield receiveSiteType( response );
46
+ },
47
+ isFulfilled( state ) {
48
+ return state.nextQuestion !== undefined;
49
+ },
50
+ };
core/admin-pages/entries/settings/stores/onboard/selectors.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { find } from 'lodash';
5
+
6
+ export function getSiteTypes( state ) {
7
+ return state.siteTypes;
8
+ }
9
+
10
+ export function getSelectedSiteTypeId( state ) {
11
+ return state.selectedSiteType;
12
+ }
13
+
14
+ export function getSelectedSiteType( state ) {
15
+ return find( getSiteTypes( state ), {
16
+ id: getSelectedSiteTypeId( state ),
17
+ } );
18
+ }
19
+
20
+ export function getAnswers( state ) {
21
+ return state.answers;
22
+ }
23
+
24
+ export function getNextQuestion( state ) {
25
+ return state.nextQuestion;
26
+ }
27
+
28
+ export function getEditedAnswer( state ) {
29
+ return state.editedAnswer;
30
+ }
31
+
32
+ export function isAnswering( state ) {
33
+ return state.isAnswering;
34
+ }
35
+
36
+ export function getLastError( state ) {
37
+ return state.lastError;
38
+ }
39
+
40
+ export function getQuestionComponent( state, questionId ) {
41
+ return state.questionComponents[ questionId ];
42
+ }
43
+
44
+ export function getCompletionSteps( state ) {
45
+ return state.completionSteps;
46
+ }
47
+
48
+ export function getAnswerRequest( state, answer ) {
49
+ const id = state.selectedSiteType;
50
+ const answers = state.answers;
51
+ const nextQuestion = state.nextQuestion;
52
+
53
+ return {
54
+ id,
55
+ answers: [
56
+ ...answers,
57
+ {
58
+ question: nextQuestion.id,
59
+ answer,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+
65
+ export function getRestoreSiteTypeRequest( state ) {
66
+ const id = state.selectedSiteType;
67
+ const answers = state.answers;
68
+
69
+ return {
70
+ id,
71
+ answers: [ ...answers ],
72
+ };
73
+ }
74
+
75
+ export function getLastVisitedLocation( state ) {
76
+ const ignore = [ '/onboard', '/onboard/site-type' ];
77
+
78
+ for ( let i = state.visitedLocations.length - 1; i >= 0; i-- ) {
79
+ const path = state.visitedLocations[ i ];
80
+
81
+ if ( ! ignore.includes( path ) ) {
82
+ return path;
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Gets the current completion step.
89
+ *
90
+ * @param {Object} state
91
+ * @return {boolean|{id: string, label: string}} True if completed, false if not started. Object describing the current step otherwise.
92
+ */
93
+ export function getCompletionStep( state ) {
94
+ return state.completionStep;
95
+ }
core/admin-pages/entries/settings/stores/tools/actions.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { Result } from '@ithemes/security-utils';
5
+ import { apiFetch, awaitPromise } from '../controls';
6
+
7
+ export function* fetchTools() {
8
+ const response = yield apiFetch( {
9
+ path: '/ithemes-security/v1/tools',
10
+ } );
11
+
12
+ yield { type: RECEIVE_TOOLS, tools: response };
13
+ }
14
+
15
+ export function* runTool( tool, form = {} ) {
16
+ yield { type: START_TOOL, tool, form };
17
+ let response;
18
+
19
+ try {
20
+ response = yield apiFetch( {
21
+ path: `/ithemes-security/v1/tools/${ tool }`,
22
+ method: 'POST',
23
+ data: form,
24
+ parse: false,
25
+ } );
26
+ } catch ( error ) {
27
+ const result = yield awaitPromise(
28
+ Result.fromResponse( error.getResponse() )
29
+ );
30
+ yield { type: FINISH_TOOL, tool, result };
31
+
32
+ return result;
33
+ }
34
+
35
+ const result = yield awaitPromise( Result.fromResponse( response ) );
36
+ yield { type: FINISH_TOOL, tool, result };
37
+
38
+ return result;
39
+ }
40
+
41
+ export function* toggleTool( tool, enabled = true ) {
42
+ yield { type: START_TOGGLE_TOOL, tool, enabled };
43
+ let response;
44
+
45
+ try {
46
+ response = yield apiFetch( {
47
+ path: `/ithemes-security/v1/tools/${ tool }`,
48
+ method: 'PUT',
49
+ data: {
50
+ enabled,
51
+ },
52
+ } );
53
+ } catch ( error ) {
54
+ yield { type: FAILED_TOGGLE_TOOL, tool, error };
55
+
56
+ return error;
57
+ }
58
+
59
+ yield { type: FINISH_TOGGLE_TOOL, tool, data: response };
60
+
61
+ return response;
62
+ }
63
+
64
+ export const RECEIVE_TOOLS = 'RECEIVE_TOOLS';
65
+
66
+ export const START_TOOL = 'START_TOOL';
67
+ export const FINISH_TOOL = 'FINISH_TOOL';
68
+
69
+ export const START_TOGGLE_TOOL = 'START_TOGGLE_TOOL';
70
+ export const FAILED_TOGGLE_TOOL = 'FAILED_TOGGLE_TOOL';
71
+ export const FINISH_TOGGLE_TOOL = 'FINISH_TOGGLE_TOOL';
core/admin-pages/entries/settings/stores/tools/index.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { registerStore } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import controls from '../controls';
10
+ import * as actions from './actions';
11
+ import * as selectors from './selectors';
12
+ import reducer from './reducers';
13
+ import * as resolvers from './resolvers';
14
+
15
+ export const STORE_NAME = 'ithemes-security/tools';
16
+
17
+ const store = registerStore( STORE_NAME, {
18
+ controls,
19
+ actions,
20
+ selectors,
21
+ resolvers,
22
+ reducer,
23
+ } );
24
+
25
+ export default store;
core/admin-pages/entries/settings/stores/tools/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/admin-pages/entries/settings/stores/tools/reducers.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { keyBy, map, without, omit } from 'lodash';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import {
10
+ RECEIVE_TOOLS,
11
+ START_TOOL,
12
+ FINISH_TOOL,
13
+ START_TOGGLE_TOOL,
14
+ FAILED_TOGGLE_TOOL,
15
+ FINISH_TOGGLE_TOOL,
16
+ } from './actions';
17
+
18
+ const DEFAULT_STATE = {
19
+ bySlug: {},
20
+ slugs: [],
21
+ running: [],
22
+ lastResult: {},
23
+ updating: [],
24
+ lastError: {},
25
+ };
26
+
27
+ export default function ( state = DEFAULT_STATE, action ) {
28
+ switch ( action.type ) {
29
+ case RECEIVE_TOOLS:
30
+ return {
31
+ ...state,
32
+ bySlug: keyBy( action.tools, 'slug' ),
33
+ slugs: map( action.tools, 'slug' ),
34
+ };
35
+ case START_TOOL:
36
+ return {
37
+ ...state,
38
+ running: [ ...state.running, action.tool ],
39
+ };
40
+ case FINISH_TOOL:
41
+ return {
42
+ ...state,
43
+ running: without( state.running, action.tool ),
44
+ lastResult: {
45
+ ...state.lastResult,
46
+ [ action.tool ]: action.result,
47
+ },
48
+ };
49
+ case START_TOGGLE_TOOL:
50
+ return {
51
+ ...state,
52
+ updating: [ ...state.updating, action.tool ],
53
+ };
54
+ case FAILED_TOGGLE_TOOL:
55
+ return {
56
+ ...state,
57
+ updating: without( state.updating, action.tool ),
58
+ lastError: {
59
+ ...state.lastError,
60
+ [ action.tool ]: action.error,
61
+ },
62
+ };
63
+ case FINISH_TOGGLE_TOOL:
64
+ return {
65
+ ...state,
66
+ updating: without( state.updating, action.tool ),
67
+ lastError: omit( state.lastError, action.tool ),
68
+ bySlug: {
69
+ ...state.bySlug,
70
+ [ action.tool ]: action.data,
71
+ },
72
+ };
73
+ default:
74
+ return state;
75
+ }
76
+ }
core/admin-pages/entries/settings/stores/tools/resolvers.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { controls } from '@wordpress/data';
2
+
3
+ /**
4
+ * Internal dependencies
5
+ */
6
+ import { fetchTools } from './actions';
7
+ import { TOOLS_STORE_NAME } from '../';
8
+
9
+ export function* getTools() {
10
+ yield fetchTools();
11
+ }
12
+
13
+ export function* getResolvedTools() {
14
+ yield controls.resolveSelect( TOOLS_STORE_NAME, 'getTools' );
15
+ }
16
+
17
+ export const getTool = {
18
+ *fulfill() {
19
+ yield controls.resolveSelect( TOOLS_STORE_NAME, 'getTools' );
20
+ },
21
+ isFulfilled( state, tool ) {
22
+ return !! state.bySlug[ tool ];
23
+ },
24
+ };
core/admin-pages/entries/settings/stores/tools/selectors.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import createSelector from 'rememo';
5
+ import memize from 'memize';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { createRegistrySelector } from '@wordpress/data';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
16
+ import { TOOLS_STORE_NAME } from '../';
17
+
18
+ const _combineTools = memize(
19
+ ( fromConfig, fromApi ) =>
20
+ fromConfig.map( ( config ) => fromApi[ config.slug ] || config ),
21
+ { maxSize: 1 }
22
+ );
23
+
24
+ export const getResolvedTools = createRegistrySelector(
25
+ ( select ) => ( state ) =>
26
+ _combineTools(
27
+ select( TOOLS_STORE_NAME ).getToolsConfig(),
28
+ state.bySlug
29
+ )
30
+ );
31
+
32
+ export const getTools = createSelector(
33
+ ( state ) => state.slugs.map( ( slug ) => state.bySlug[ slug ] ),
34
+ ( state ) => [ state.bySlug, state.slugs ]
35
+ );
36
+
37
+ const _transformTools = memize(
38
+ ( modules ) => {
39
+ return modules.reduce( ( acc, module ) => {
40
+ for ( const [ slug, config ] of Object.entries( module.tools ) ) {
41
+ acc.push( {
42
+ slug,
43
+ module: module.id,
44
+ toggleable: false,
45
+ schedule: '',
46
+ form: null,
47
+ ...config,
48
+ } );
49
+ }
50
+
51
+ return acc;
52
+ }, [] );
53
+ },
54
+ { maxSize: 1 }
55
+ );
56
+
57
+ export const getToolsConfig = createRegistrySelector( ( select ) => () =>
58
+ _transformTools( select( MODULES_STORE_NAME ).getModules() )
59
+ );
60
+
61
+ export const getTool = createRegistrySelector( ( select ) => ( state, tool ) =>
62
+ state.bySlug[ tool ] ||
63
+ select( TOOLS_STORE_NAME )
64
+ .getToolsConfig()
65
+ .find( ( maybe ) => tool === maybe.slug )
66
+ );
67
+
68
+ export function getRunning( state ) {
69
+ return state.running;
70
+ }
71
+
72
+ export function isRunning( state, tool ) {
73
+ return state.running.includes( tool );
74
+ }
75
+
76
+ export function getLastResult( state, tool ) {
77
+ return state.lastResult[ tool ];
78
+ }
79
+
80
+ export function isUpdating( state, tool ) {
81
+ return state.updating.includes( tool );
82
+ }
83
+
84
+ export function getLastError( state, tool ) {
85
+ return state.lastError[ tool ];
86
+ }
core/admin-pages/entries/settings/style.scss ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "css-vars";
2
+
3
+ body.security_page_itsec {
4
+ background-color: $background-blue;
5
+ }
6
+
7
+ #wpcontent {
8
+ padding-left: 0;
9
+ }
10
+
11
+ .auto-fold #wpcontent {
12
+ padding-left: 0;
13
+ }
14
+
15
+ .wp-responsive-open #wpbody {
16
+ right: calc(-16em + 20px);
17
+ }
18
+
19
+
20
+ #wpbody-content {
21
+ padding-bottom: 0;
22
+ }
23
+
24
+ #wpfooter {
25
+ display: none;
26
+ }
27
+
28
+ #adminmenuback {
29
+ z-index: 5;
30
+ }
31
+
32
+ ul#adminmenu #toplevel_page_itsec a.wp-has-current-submenu:after,
33
+ ul#adminmenu > li.current#toplevel_page_itsec > a.current:after {
34
+ border-right-color: #fff;
35
+ }
36
+
37
+ @include break-small {
38
+ #itsec-settings-root {
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ right: 0;
43
+ bottom: 0;
44
+ }
45
+ }
46
+
47
+ .itsec-settings {
48
+ position: relative;
49
+ display: flex;
50
+ align-items: stretch;
51
+ height: 100%;
52
+ min-height: calc(100vh - 46px);
53
+ overflow: auto;
54
+ --wp-admin-theme-color: #{$main-blue};
55
+ --wp-admin-theme-color-darker-10: #{darken($main-blue, 10)};
56
+ --wp-admin-theme-color-darker-20: #{darken($main-blue, 20)};
57
+
58
+ @include break-small {
59
+ }
60
+
61
+ @include break-medium {
62
+ min-height: calc(100vh - 32px);
63
+ }
64
+
65
+ @media screen and (width: $small) {
66
+ min-height: 100vh;
67
+ margin-top: 46px;
68
+ }
69
+ }
70
+
71
+ .itsec-settings *,
72
+ .itsec-settings *::before,
73
+ .itsec-settings *::after {
74
+ box-sizing: border-box;
75
+ }
76
+
77
+ .itsec-settings h1 {
78
+ font-weight: 600;
79
+ font-size: 1.6rem;
80
+ line-height: 1;
81
+ margin: .75rem 0;
82
+ }
83
+
84
+ .itsec-settings h2 {
85
+ color: $medium-text;
86
+ font-weight: normal;
87
+ font-size: 1.25rem;
88
+ line-height: 1.3;
89
+ margin: .75rem 0;
90
+ }
91
+
92
+ .itsec-settings h3 {
93
+ font-size: 1rem;
94
+ font-weight: 400;
95
+ margin: .5rem 0;
96
+ }
97
+
98
+ .itsec-settings h4 {
99
+ font-size: 1rem;
100
+ font-weight: 700;
101
+ text-transform: uppercase;
102
+ margin: .5rem 0;
103
+ }
104
+
105
+ .itsec-settings p,
106
+ .itsec-settings ul,
107
+ .itsec-settings ol,
108
+ .itsec-settings dl {
109
+ color: $medium-text;
110
+ }
111
+
112
+ .itsec-settings h1:first-child,
113
+ .itsec-settings h2:first-child,
114
+ .itsec-settings h3:first-child,
115
+ .itsec-settings h4:first-child,
116
+ .itsec-settings h5:first-child,
117
+ .itsec-settings h6:first-child {
118
+ margin-top: 0;
119
+ }
120
+
121
+ .itsec-settings p:first-child {
122
+ margin-top: 0;
123
+ }
124
+
125
+ .itsec-settings p:last-child {
126
+ margin-bottom: 0;
127
+ }
128
+
129
+ .itsec-settings label,
130
+ .itsec-settings legend {
131
+ color: $dark-text;
132
+ }
133
+
134
+ .itsec-settings .components-checkbox-control__input-container {
135
+ margin-right: 8px;
136
+ }
137
+
138
+ .itsec-rjsf-entity-select-control__input input[type="text"]:focus {
139
+ box-shadow: none;
140
+ outline: none;
141
+ }
142
+
143
+ .itsec-settings .components-snackbar-list {
144
+ width: auto;
145
+ position: fixed;
146
+ bottom: 0;
147
+ }
148
+
149
+ .itsec-settings .components-notice {
150
+ margin-left: 0;
151
+ margin-right: 0;
152
+
153
+ &.is-dismissible {
154
+ padding-right: 12px;
155
+ }
156
+ }
157
+
158
+ .form-group > .components-base-control:last-child > .components-base-control__field:last-child {
159
+ margin-bottom: 0;
160
+ }
161
+
162
+ .itsec-settings .components-base-control__label,
163
+ .itsec-settings .components-base-control__field {
164
+ margin-bottom: 4px;
165
+ }
166
+
167
+ .itsec-settings .components-base-control__help {
168
+ margin-top: 4px;
169
+ }
170
+
171
+ .itsec-settings .components-textarea-control__input {
172
+ margin-bottom: 0;
173
+ }
174
+
175
+ .itsec-settings .components-text-control__input[type="search"]:focus {
176
+ border-color: var(--wp-admin-theme-color);
177
+ box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
178
+ outline: 2px solid transparent;
179
+ }
180
+
181
+ .itsec-settings select.components-select-control__input {
182
+ max-width: none;
183
+
184
+ &:focus ~ .components-input-control__backdrop {
185
+ border-color: var(--wp-admin-theme-color);
186
+ box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
187
+ }
188
+ }
189
+
190
+ :where(.itsec-settings) .components-button.has-icon.has-text {
191
+ padding: 6px 12px;
192
+ }
193
+
194
+ .itsec-button-icon-right.components-button.has-icon {
195
+ padding: 6px 12px;
196
+
197
+ .dashicon {
198
+ margin-left: 10px;
199
+ margin-right: 2px;
200
+ }
201
+ }
202
+
203
+ :where(.itsec-settings) a,
204
+ :where(.itsec-settings) .components-button.is-link {
205
+ color: var(--wp-admin-theme-color);
206
+
207
+ &:hover {
208
+ color: var(--wp-admin-theme-color-darker-20);
209
+ }
210
+ }
211
+
212
+ :where(.itsec-settings) .dashicon {
213
+ transition: none;
214
+ }
215
+
216
+ @media (max-width: $break-large - 1) and (min-width: $break-medium + 1) {
217
+ .itsec-settings-main {
218
+ // Size of the sidebar.
219
+ margin-left: calc(30px + 2rem);
220
+ }
221
+ }
222
+
223
+ @media (max-width: $break-medium) {
224
+ .itsec-settings-toolbar {
225
+ padding-left: calc(20px + 2rem);
226
+ position: fixed;
227
+ width: 100%;
228
+ min-height: calc(40px + 1rem);
229
+ z-index: 2;
230
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
231
+
232
+ .itsec-search {
233
+ margin-right: 0;
234
+ }
235
+ }
236
+ }
237
+
238
+ @media (max-width: $break-small) {
239
+ .itsec-settings-toolbar {
240
+ position: initial;
241
+ }
242
+ }
core/admin-pages/entries/settings/utils.js ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
5
+ import { createLocation } from 'history';
6
+ import { pickBy, get, set } from 'lodash';
7
+ import Ajv from 'ajv';
8
+ import classnames from 'classnames';
9
+
10
+ /**
11
+ * WordPress dependencies
12
+ */
13
+ import { createContext, useContext } from '@wordpress/element';
14
+ import { useDispatch, useSelect, useRegistry } from '@wordpress/data';
15
+ import { applyFilters } from '@wordpress/hooks';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { MODULES_STORE_NAME } from '@ithemes/security-data';
22
+ import { WPError } from '@ithemes/security-utils';
23
+
24
+ export const ConfigContext = createContext( {
25
+ serverType: '',
26
+ installType: '',
27
+ onboardComplete: false,
28
+ } );
29
+
30
+ export function useConfigContext() {
31
+ return useContext( ConfigContext );
32
+ }
33
+
34
+ export function useNavigateTo() {
35
+ const history = useHistory();
36
+
37
+ return ( route, mode = 'push' ) =>
38
+ history[ mode ]( createLocation( route ) );
39
+ }
40
+
41
+ /**
42
+ * Gets the child path of the current route.
43
+ *
44
+ * For instance, if `/configure/settings/advanced` is the current URL,
45
+ * and `/configure/settings` is the current route, then `/advanced` will be returned.
46
+ *
47
+ * @return {string} The current child path.
48
+ */
49
+ export function useChildPath() {
50
+ const { url } = useRouteMatch();
51
+ const { pathname: locationPath } = useLocation();
52
+ const { pathname: matchedPath } = createLocation( url );
53
+
54
+ return locationPath.replace( matchedPath, '' );
55
+ }
56
+
57
+ /**
58
+ * Grabs a global instance of Ajv.
59
+ *
60
+ * @return {Ajv} The ajv instance.
61
+ */
62
+ export function getAjv() {
63
+ if ( ! getAjv.instance ) {
64
+ getAjv.instance = new Ajv( { schemaId: 'id' } );
65
+ getAjv.instance.addMetaSchema(
66
+ require( 'ajv/lib/refs/json-schema-draft-04.json' )
67
+ );
68
+ }
69
+
70
+ return getAjv.instance;
71
+ }
72
+
73
+ const isConditionalSettingActive = ( definition, module, context ) => {
74
+ const { serverType, installType, activeModules, settings } = context;
75
+
76
+ if (
77
+ definition[ 'server-type' ] &&
78
+ definition[ 'server-type' ] !== serverType
79
+ ) {
80
+ return false;
81
+ }
82
+
83
+ if (
84
+ definition[ 'install-type' ] &&
85
+ definition[ 'install-type' ] !== installType
86
+ ) {
87
+ return false;
88
+ }
89
+
90
+ if ( definition[ 'active-modules' ] ) {
91
+ for ( const activeModule of definition[ 'active-modules' ] ) {
92
+ if ( ! activeModules.includes( activeModule ) ) {
93
+ return false;
94
+ }
95
+ }
96
+ }
97
+
98
+ if ( definition.settings ) {
99
+ const ajv = getAjv();
100
+ const validate = ajv.compile( definition.settings );
101
+
102
+ if ( ! validate( settings ) ) {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Filters whether a conditional setting is active.
109
+ *
110
+ * This hook can only be used to turn a conditional setting inactive,
111
+ * if it is inactive due to other conditional rules, this filter won't run.
112
+ *
113
+ * @param {boolean} isActive Whether the setting is active.
114
+ * @param {Object} module The module definition.
115
+ * @param {Object} definition The conditional setting definition.
116
+ * @param {Object} context Context used to determine whether the setting is active.
117
+ */
118
+ return applyFilters(
119
+ 'ithemes-security.settings.isConditionalSettingActive',
120
+ true,
121
+ module,
122
+ definition,
123
+ context
124
+ );
125
+ };
126
+
127
+ /**
128
+ * Makes a settings schema conditional based on the module definition.
129
+ *
130
+ * @param {Object} module The module definition.
131
+ * @param {Object} context The context used to evaluate the conditional settings.
132
+ * @param {string} context.serverType The web server type.
133
+ * @param {string} context.installType The ITSEC installation type.
134
+ * @param {Array<string>} context.activeModules The list of active modules.
135
+ * @param {Object} context.settings The module's setting value.
136
+ * @param {Object} context.registry The @wordpress/data registry.
137
+ *
138
+ * @return {Object} The settings schema.
139
+ */
140
+ export function makeConditionalSettingsSchema( module, context ) {
141
+ const isActive = ( definition ) =>
142
+ isConditionalSettingActive( definition, module, context );
143
+ const reduceConditional = ( parent, subSchema ) => {
144
+ if ( ! subSchema.properties ) {
145
+ return subSchema;
146
+ }
147
+
148
+ return {
149
+ ...subSchema,
150
+ properties: Object.entries( subSchema.properties ).reduce(
151
+ ( acc, [ propName, propSchema ] ) => {
152
+ const conditionalKey = `${ parent }.${ propName }`;
153
+
154
+ if (
155
+ module.settings.conditional[ conditionalKey ] &&
156
+ ! isActive(
157
+ module.settings.conditional[ conditionalKey ]
158
+ )
159
+ ) {
160
+ return acc;
161
+ }
162
+
163
+ acc[ propName ] = reduceConditional(
164
+ conditionalKey,
165
+ propSchema
166
+ );
167
+
168
+ return acc;
169
+ },
170
+ {}
171
+ ),
172
+ };
173
+ };
174
+
175
+ const properties = Object.entries(
176
+ module.settings.schema.properties
177
+ ).reduce( ( acc, [ propName, propSchema ] ) => {
178
+ if ( ! module.settings.interactive.includes( propName ) ) {
179
+ return acc;
180
+ }
181
+
182
+ if (
183
+ module.settings.conditional[ propName ] &&
184
+ ! isActive( module.settings.conditional[ propName ] )
185
+ ) {
186
+ return acc;
187
+ }
188
+
189
+ acc[ propName ] = reduceConditional( propName, propSchema );
190
+
191
+ return acc;
192
+ }, {} );
193
+
194
+ return {
195
+ ...module.settings.schema,
196
+ properties,
197
+ };
198
+ }
199
+
200
+ export function useConditionalSchema( module, settings ) {
201
+ const { serverType, installType } = useConfigContext();
202
+ const registry = useRegistry();
203
+ const activeModules = useSelect( ( select ) =>
204
+ select( MODULES_STORE_NAME ).getActiveModules()
205
+ );
206
+ const context = {
207
+ serverType,
208
+ installType,
209
+ activeModules,
210
+ settings,
211
+ registry,
212
+ };
213
+
214
+ return makeConditionalSettingsSchema( module, context );
215
+ }
216
+
217
+ /**
218
+ * A hook to allow for convenient editing of module settings.
219
+ *
220
+ * @param {Object} module The module definition.
221
+ * @param {function(Object, string): boolean} [filterFields] An optional function to filter the included settings.
222
+ * @return {{schema: Object, uiSchema: Object, setFormData: Function, formData: Object}} The settings form components.
223
+ */
224
+ export function useSettingsForm( module, filterFields ) {
225
+ const formData = useSelect( ( select ) =>
226
+ select( MODULES_STORE_NAME ).getEditedSettings( module.id )
227
+ );
228
+ const { editSettings } = useDispatch( MODULES_STORE_NAME );
229
+ const conditionalSchema = useConditionalSchema( module, formData );
230
+
231
+ if ( filterFields ) {
232
+ conditionalSchema.properties = pickBy(
233
+ conditionalSchema.properties,
234
+ filterFields
235
+ );
236
+ }
237
+
238
+ const setFormData = ( e ) => {
239
+ editSettings( module.id, e.formData );
240
+ };
241
+
242
+ return {
243
+ schema: conditionalSchema,
244
+ uiSchema: module.settings.schema.uiSchema,
245
+ formData,
246
+ setFormData,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Gets the list of module types.
252
+ *
253
+ * @return {({label: string, slug: string})[]} The list of module types.
254
+ */
255
+ export function getModuleTypes() {
256
+ return [
257
+ {
258
+ slug: 'login',
259
+ label: __( 'Login Security', 'better-wp-security' ),
260
+ },
261
+ {
262
+ slug: 'lockout',
263
+ label: __( 'Lockouts', 'better-wp-security' ),
264
+ },
265
+ {
266
+ slug: 'site-check',
267
+ label: __( 'Site Check', 'better-wp-security' ),
268
+ },
269
+ {
270
+ slug: 'utility',
271
+ label: __( 'Utilities', 'better-wp-security' ),
272
+ },
273
+ {
274
+ slug: 'advanced',
275
+ label: __( 'Advanced', 'better-wp-security' ),
276
+ },
277
+ ];
278
+ }
279
+
280
+ /**
281
+ * Checks if a module's requirements are met.
282
+ *
283
+ * @param {Object} module The module definition.
284
+ * @param {string} mode The mode to check for. Either 'activate' or 'run'.
285
+ * @return {WPError} An empty error object if all requirements are met.
286
+ */
287
+ export function validateModuleRequirements( module, mode ) {
288
+ const error = new WPError();
289
+
290
+ if ( ! module.requirements ) {
291
+ return error;
292
+ }
293
+
294
+ const isForMode = ( requirement ) =>
295
+ requirement.validate === mode || mode === 'activate';
296
+
297
+ if (
298
+ module.requirements.ssl &&
299
+ isForMode( module.requirements.ssl ) &&
300
+ document.location.protocol !== 'https:'
301
+ ) {
302
+ error.add( 'ssl', __( 'Your site must support SSL.', 'better-wp-security' ) );
303
+ }
304
+
305
+ return error;
306
+ }
307
+
308
+ /**
309
+ * Appends a classname to a property at an arbitrary depth.
310
+ *
311
+ * This method mutates the object.
312
+ *
313
+ * @param {Object} object The object to modify.
314
+ * @param {Array<string>} path The path at which to append it.
315
+ * @param {string} className The class to append.
316
+ * @return {Object} The object.
317
+ */
318
+ export function appendClassNameAtPath( object, path, className ) {
319
+ set( object, path, classnames( get( object, path ), className ) );
320
+
321
+ return object;
322
+ }
core/admin-pages/init.php CHANGED
@@ -2,10 +2,8 @@
2
 
3
 
4
  final class ITSEC_Admin_Page_Loader {
5
- private $page_refs = array();
6
  private $page_id;
7
 
8
-
9
  public function __construct() {
10
  if ( is_multisite() ) {
11
  add_action( 'network_admin_menu', array( $this, 'add_admin_pages' ) );
@@ -13,7 +11,6 @@ final class ITSEC_Admin_Page_Loader {
13
  add_action( 'admin_menu', array( $this, 'add_admin_pages' ) );
14
  }
15
 
16
- add_action( 'wp_ajax_itsec_settings_page', array( $this, 'handle_ajax_request' ) );
17
  add_action( 'wp_ajax_itsec_logs_page', array( $this, 'handle_ajax_request' ) );
18
  add_action( 'wp_ajax_itsec_help_page', array( $this, 'handle_ajax_request' ) );
19
  add_action( 'wp_ajax_itsec_debug_page', array( $this, 'handle_ajax_request' ) );
@@ -23,34 +20,34 @@ final class ITSEC_Admin_Page_Loader {
23
  add_filter( 'itsec-user-setting-valid-itsec-settings-view', array( $this, 'validate_view' ), null, 2 );
24
  }
25
 
26
- public function add_scripts() {
27
- wp_register_script( 'itsec-jquery-multi-select', plugin_dir_url( __FILE__ ) . '/js/lib/jquery.multiselect.js', array( 'jquery' ), '2.4.17' );
28
- wp_register_script( 'itsec-form-user-groups', plugin_dir_url( __FILE__ ) . '/js/form-user-groups.js', array( 'itsec-jquery-multi-select', 'lodash' ), 1 );
29
- }
30
-
31
- public function add_styles() {
32
- wp_register_style( 'itsec-jquery-multi-select', plugin_dir_url( __FILE__ ) . '/js/lib/jquery.multiselect.css', array(), '2.4.17' );
33
- wp_enqueue_style( 'itsec-settings-page-style', plugins_url( 'css/style.css', __FILE__ ), array(), ITSEC_Core::get_plugin_build() );
34
- }
35
-
36
  public function add_admin_pages() {
 
37
  $capability = ITSEC_Core::get_required_cap();
38
- $page_refs = array();
39
 
40
- add_menu_page( __( 'Settings', 'better-wp-security' ), __( 'Security', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
41
- $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Settings', 'better-wp-security' ), __( 'Settings', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
42
- $page_refs[] = add_submenu_page( 'itsec', '', __( 'Security Check', 'better-wp-security' ), $capability, 'itsec-security-check', array( $this, 'show_page' ) );
 
 
 
 
 
43
 
44
- $page_refs = apply_filters( 'itsec-admin-page-refs', $page_refs, $capability, array( $this, 'show_page' ) );
45
 
46
- $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Logs', 'better-wp-security' ), __( 'Logs', 'better-wp-security' ), $capability, 'itsec-logs', array( $this, 'show_page' ) );
 
 
 
 
47
 
48
  if ( ! ITSEC_Core::is_pro() ) {
49
- $page_refs[] = add_submenu_page( 'itsec', '', '<span style="color:#2EA2CC">' . __( 'Go Pro', 'better-wp-security' ) . '</span>', $capability, 'itsec-go-pro', array( $this, 'show_page' ) );
50
  }
51
 
52
  if ( defined( 'ITSEC_DEBUG' ) && ITSEC_DEBUG ) {
53
- $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Debug', 'better-wp-security' ), __( 'Debug' ), $capability, 'itsec-debug', array( $this, 'show_page' ) );
54
  }
55
 
56
  foreach ( $page_refs as $page_ref ) {
@@ -69,9 +66,9 @@ final class ITSEC_Admin_Page_Loader {
69
  if ( isset( $_REQUEST['action'] ) && preg_match( '/^itsec_(.+)_page$/', $_REQUEST['action'], $match ) ) {
70
  $this->page_id = $match[1];
71
  }
72
- } else if ( 'itsec-' === substr( $plugin_page, 0, 6 ) ) {
73
  $this->page_id = substr( $plugin_page, 6 );
74
- } else if ( 'itsec' === substr( $plugin_page, 0, 5 ) ) {
75
  $this->page_id = 'settings';
76
  }
77
 
@@ -83,9 +80,6 @@ final class ITSEC_Admin_Page_Loader {
83
  }
84
 
85
  public function load() {
86
- add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
87
- add_action( 'admin_print_styles', array( $this, 'add_styles' ) );
88
-
89
  $this->load_file( 'page-%s.php' );
90
  }
91
 
@@ -111,7 +105,7 @@ final class ITSEC_Admin_Page_Loader {
111
  $id = $this->get_page_id();
112
 
113
  if ( empty( $id ) ) {
114
- if ( isset( $GLOBALS['pagenow'] ) && 'admin.php' === $GLOBALS['pagenow'] && isset( $_GET['page'] ) && 'itsec-' === substr( $_GET['page'], 0, 6 ) ) {
115
  $id = substr( $_GET['page'], 6 );
116
  } else {
117
  return;
@@ -120,7 +114,7 @@ final class ITSEC_Admin_Page_Loader {
120
 
121
  $id = str_replace( '_', '-', $id );
122
 
123
- $file = dirname( __FILE__ ) . '/' . sprintf( $file, $id );
124
  $file = apply_filters( "itsec-admin-page-file-path-$id", $file );
125
 
126
  if ( is_file( $file ) ) {
@@ -129,24 +123,25 @@ final class ITSEC_Admin_Page_Loader {
129
  }
130
 
131
  public function handle_user_setting() {
132
- $whitelist_settings = array(
133
- 'itsec-settings-view'
134
- );
135
 
136
- if ( in_array( $_REQUEST['setting'], $whitelist_settings ) ) {
137
- $_REQUEST['setting'] = sanitize_title_with_dashes( $_REQUEST['setting'] );
138
 
139
- // Verify nonce is valid and for this setting, and allow a filter to
140
- if ( wp_verify_nonce( $_REQUEST['itsec-user-setting-nonce'], 'set-user-setting-' . $_REQUEST['setting'] ) &&
141
- apply_filters( 'itsec-user-setting-valid-' . $_REQUEST['setting'], true, $_REQUEST['value'] ) ) {
142
 
143
- if ( false !== update_user_meta( get_current_user_id(), $_REQUEST['setting'], $_REQUEST['value'] ) ) {
144
- wp_send_json_success();
145
- }
146
 
147
- }
 
148
  }
149
- wp_send_json_error();
 
150
  }
151
 
152
  public function validate_view( $valid, $view ) {
2
 
3
 
4
  final class ITSEC_Admin_Page_Loader {
 
5
  private $page_id;
6
 
 
7
  public function __construct() {
8
  if ( is_multisite() ) {
9
  add_action( 'network_admin_menu', array( $this, 'add_admin_pages' ) );
11
  add_action( 'admin_menu', array( $this, 'add_admin_pages' ) );
12
  }
13
 
 
14
  add_action( 'wp_ajax_itsec_logs_page', array( $this, 'handle_ajax_request' ) );
15
  add_action( 'wp_ajax_itsec_help_page', array( $this, 'handle_ajax_request' ) );
16
  add_action( 'wp_ajax_itsec_debug_page', array( $this, 'handle_ajax_request' ) );
20
  add_filter( 'itsec-user-setting-valid-itsec-settings-view', array( $this, 'validate_view' ), null, 2 );
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
23
  public function add_admin_pages() {
24
+ $onboarded = ITSEC_Core::is_onboarded();
25
  $capability = ITSEC_Core::get_required_cap();
26
+ $page_refs = array();
27
 
28
+ if ( $onboarded && current_user_can( 'itsec_dashboard_menu' ) ) {
29
+ $parent = 'itsec-dashboard';
30
+ add_menu_page( __( 'Security', 'better-wp-security' ), __( 'Security', 'better-wp-security' ), 'itsec_dashboard_menu', $parent, array( $this, 'show_page' ) );
31
+ $page_refs[] = add_submenu_page( $parent, __( 'Dashboard', 'better-wp-security' ), __( 'Dashboard', 'better-wp-security' ), 'itsec_dashboard_menu', 'itsec-dashboard', array( $this, 'show_page' ) );
32
+ } else {
33
+ $parent = 'itsec';
34
+ add_menu_page( __( 'Setup', 'better-wp-security' ), __( 'Security', 'better-wp-security' ), $capability, $parent, array( $this, 'show_page' ) );
35
+ }
36
 
37
+ $page_refs[] = add_submenu_page( $parent, __( 'iThemes Security Settings', 'better-wp-security' ), $onboarded ? __( 'Settings', 'better-wp-security' ) : __( 'Setup', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
38
 
39
+ $page_refs = apply_filters( 'itsec-admin-page-refs', $page_refs, $capability, array( $this, 'show_page' ), $parent );
40
+
41
+ if ( $onboarded ) {
42
+ $page_refs[] = add_submenu_page( $parent, __( 'iThemes Security Logs', 'better-wp-security' ), __( 'Logs', 'better-wp-security' ), $capability, 'itsec-logs', array( $this, 'show_page' ) );
43
+ }
44
 
45
  if ( ! ITSEC_Core::is_pro() ) {
46
+ $page_refs[] = add_submenu_page( $parent, '', '<span style="color:#2EA2CC">' . __( 'Go Pro', 'better-wp-security' ) . '</span>', $capability, 'itsec-go-pro', array( $this, 'show_page' ) );
47
  }
48
 
49
  if ( defined( 'ITSEC_DEBUG' ) && ITSEC_DEBUG ) {
50
+ $page_refs[] = add_submenu_page( $parent, __( 'iThemes Security Debug', 'better-wp-security' ), __( 'Debug' ), $capability, 'itsec-debug', array( $this, 'show_page' ) );
51
  }
52
 
53
  foreach ( $page_refs as $page_ref ) {
66
  if ( isset( $_REQUEST['action'] ) && preg_match( '/^itsec_(.+)_page$/', $_REQUEST['action'], $match ) ) {
67
  $this->page_id = $match[1];
68
  }
69
+ } elseif ( strpos( $plugin_page, 'itsec-' ) === 0 ) {
70
  $this->page_id = substr( $plugin_page, 6 );
71
+ } elseif ( strpos( $plugin_page, 'itsec' ) === 0 ) {
72
  $this->page_id = 'settings';
73
  }
74
 
80
  }
81
 
82
  public function load() {
 
 
 
83
  $this->load_file( 'page-%s.php' );
84
  }
85
 
105
  $id = $this->get_page_id();
106
 
107
  if ( empty( $id ) ) {
108
+ if ( isset( $GLOBALS['pagenow'], $_GET['page'] ) && 'admin.php' === $GLOBALS['pagenow'] && strpos( $_GET['page'], 'itsec-' ) === 0 ) {
109
  $id = substr( $_GET['page'], 6 );
110
  } else {
111
  return;
114
 
115
  $id = str_replace( '_', '-', $id );
116
 
117
+ $file = __DIR__ . '/' . sprintf( $file, $id );
118
  $file = apply_filters( "itsec-admin-page-file-path-$id", $file );
119
 
120
  if ( is_file( $file ) ) {
123
  }
124
 
125
  public function handle_user_setting() {
126
+ if ( 'itsec-settings-view' !== $_REQUEST['setting'] ) {
127
+ wp_send_json_error();
128
+ }
129
 
130
+ $_REQUEST['setting'] = sanitize_title_with_dashes( $_REQUEST['setting'] );
 
131
 
132
+ if ( ! wp_verify_nonce( $_REQUEST['itsec-user-setting-nonce'], 'set-user-setting-' . $_REQUEST['setting'] ) ) {
133
+ wp_send_json_error();
134
+ }
135
 
136
+ if ( ! apply_filters( 'itsec-user-setting-valid-' . $_REQUEST['setting'], true, $_REQUEST['value'] ) ) {
137
+ wp_send_json_error();
138
+ }
139
 
140
+ if ( false === update_user_meta( get_current_user_id(), $_REQUEST['setting'], $_REQUEST['value'] ) ) {
141
+ wp_send_json_error();
142
  }
143
+
144
+ wp_send_json_success();
145
  }
146
 
147
  public function validate_view( $valid, $view ) {
core/admin-pages/js/form-user-groups.js DELETED
@@ -1,103 +0,0 @@
1
- ( function( $, lodash, itsec, data ) {
2
- function getValue( obj, path ) {
3
- return path ? lodash.get( obj, path ) : obj;
4
- }
5
-
6
- function defaultCompare( a, b ) {
7
- return a === b;
8
- }
9
-
10
- // Based on redux-watch licensed MIT: https://github.com/jprichardson/redux-watch
11
- function watch( getState, objectPath, compare ) {
12
- compare = compare || defaultCompare;
13
- var currentValue = getValue( getState(), objectPath );
14
- return function w( fn ) {
15
- return function() {
16
- var newValue = getValue( getState(), objectPath );
17
- if ( !compare( currentValue, newValue ) ) {
18
- var oldValue = currentValue;
19
- currentValue = newValue;
20
- fn( newValue, oldValue, objectPath );
21
- }
22
- };
23
- };
24
- }
25
-
26
- $( function() {
27
- $( '.itsec-form-input--type-user-groups' ).multiselect( {
28
- selectAll: true,
29
- } );
30
-
31
- if ( itsec[ 'user-groups' ] && itsec[ 'user-groups' ][ 'api' ] && itsec[ 'user-groups' ][ 'api' ][ 'store' ] ) {
32
- var store = itsec[ 'user-groups' ][ 'api' ][ 'store' ];
33
-
34
- var watchMatchables = watch( data.select( 'ithemes-security/user-groups' ).getMatchables );
35
- var watchSettings = watch( function() {
36
- return store.getState().settings;
37
- } );
38
-
39
- store.subscribe( watchMatchables( function( currentValue, previousValue ) {
40
- if ( !data.select( 'core/data' ).hasFinishedResolution( 'ithemes-security/user-groups', 'getMatchables' ) ) {
41
- return;
42
- }
43
-
44
- $( '.itsec-form-input--type-user-groups' ).each( function() {
45
- var $el = $( this ),
46
- checked = $el.val() || [];
47
-
48
- var options = ( currentValue || [] )
49
- .sort( function( groupA, groupB ) {
50
- if ( groupA.type === groupB.type ) {
51
- return 0;
52
- }
53
-
54
- if ( groupA.type === 'user-group' ) {
55
- return -1;
56
- }
57
-
58
- if ( groupB.type === 'user-group' ) {
59
- return 1;
60
- }
61
-
62
- return 0;
63
- } )
64
- .map( function( userGroup ) {
65
- return {
66
- name : userGroup.label,
67
- value : userGroup.id,
68
- checked: checked.includes( userGroup.id ),
69
- };
70
- } );
71
-
72
- $el.multiselect( 'loadOptions', options );
73
- } );
74
- } ) );
75
-
76
- store.subscribe( watchSettings( lodash.debounce( function( currentValue, previousValue ) {
77
- const settings = data.select( 'ithemes-security/user-groups' ).getGroupsBySetting();
78
-
79
- for ( var module in settings ) {
80
- if ( !settings.hasOwnProperty( module ) ) {
81
- continue;
82
- }
83
-
84
- for ( var setting in settings[ module ] ) {
85
- if ( !settings[ module ].hasOwnProperty( setting ) ) {
86
- continue;
87
- }
88
-
89
- var groupIds = settings[ module ][ setting ];
90
-
91
- var $option = $( '.itsec-form-input--type-user-groups[data-module="' + module + '"][data-setting="' + setting + '"]' );
92
- var current = $option.val();
93
-
94
- if ( !lodash.isEqual( current, groupIds ) ) {
95
- $option.val( groupIds );
96
- $option.multiselect( 'reload' );
97
- }
98
- }
99
- }
100
- }, 100 ) ) );
101
- }
102
- } );
103
- } )( jQuery, lodash, window[ 'itsec' ], window[ 'wp' ][ 'data' ] );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/js/index.php CHANGED
@@ -1,2 +1 @@
1
- <?php
2
- // Silence is golden.
1
+ <?php // Silence is golden.
 
core/admin-pages/js/lib/jquery.multiselect.css DELETED
@@ -1,148 +0,0 @@
1
- .ms-options-wrap,
2
- .ms-options-wrap * {
3
- box-sizing: border-box;
4
- }
5
-
6
- .ms-options-wrap > button:focus,
7
- .ms-options-wrap > button {
8
- position: relative;
9
- width: 100%;
10
- text-align: left;
11
- border: 1px solid #aaa;
12
- background-color: #fff;
13
- padding: 5px 20px 5px 5px;
14
- margin-top: 1px;
15
- font-size: 13px;
16
- color: #aaa;
17
- outline-offset: -2px;
18
- white-space: nowrap;
19
- }
20
-
21
- .ms-options-wrap > button > span {
22
- display: inline-block;
23
- }
24
-
25
- .ms-options-wrap > button[disabled] {
26
- background-color: #e5e9ed;
27
- color: #808080;
28
- opacity: 0.6;
29
- }
30
-
31
- .ms-options-wrap > button:after {
32
- content: ' ';
33
- height: 0;
34
- position: absolute;
35
- top: 50%;
36
- right: 5px;
37
- width: 0;
38
- border: 6px solid rgba(0, 0, 0, 0);
39
- border-top-color: #999;
40
- margin-top: -3px;
41
- }
42
-
43
- .ms-options-wrap.ms-has-selections > button {
44
- color: #333;
45
- }
46
-
47
- .ms-options-wrap > .ms-options {
48
- position: absolute;
49
- left: 0;
50
- width: 100%;
51
- margin-top: 1px;
52
- margin-bottom: 20px;
53
- background: white;
54
- z-index: 2000;
55
- border: 1px solid #aaa;
56
- overflow: auto;
57
- visibility: hidden;
58
- }
59
-
60
- .ms-options-wrap.ms-active > .ms-options {
61
- visibility: visible
62
- }
63
-
64
- .ms-options-wrap > .ms-options > .ms-search input {
65
- width: 100%;
66
- padding: 4px 5px;
67
- border: none;
68
- border-bottom: 1px groove;
69
- outline: none;
70
- }
71
-
72
- .ms-options-wrap > .ms-options .ms-selectall {
73
- display: inline-block;
74
- font-size: .9em;
75
- text-transform: lowercase;
76
- text-decoration: none;
77
- }
78
- .ms-options-wrap > .ms-options .ms-selectall:hover {
79
- text-decoration: underline;
80
- }
81
-
82
- .ms-options-wrap > .ms-options > .ms-selectall.global {
83
- margin: 4px 5px;
84
- }
85
-
86
- .ms-options-wrap > .ms-options > ul,
87
- .ms-options-wrap > .ms-options > ul > li.optgroup ul {
88
- list-style-type: none;
89
- padding: 0;
90
- margin: 0;
91
- }
92
-
93
- .ms-options-wrap > .ms-options > ul li.ms-hidden {
94
- display: none;
95
- }
96
-
97
- .ms-options-wrap > .ms-options > ul > li.optgroup {
98
- padding: 5px;
99
- }
100
- .ms-options-wrap > .ms-options > ul > li.optgroup + li.optgroup {
101
- border-top: 1px solid #aaa;
102
- }
103
-
104
- .ms-options-wrap > .ms-options > ul > li.optgroup .label {
105
- display: block;
106
- padding: 5px 0 0 0;
107
- font-weight: bold;
108
- }
109
-
110
- .ms-options-wrap > .ms-options > ul label {
111
- position: relative;
112
- display: inline-block;
113
- width: 100%;
114
- padding: 4px 4px 4px 20px;
115
- margin: 1px 0;
116
- border: 1px dotted transparent;
117
- }
118
- .ms-options-wrap > .ms-options.checkbox-autofit > ul label,
119
- .ms-options-wrap > .ms-options.hide-checkbox > ul label {
120
- padding: 4px;
121
- }
122
-
123
- .ms-options-wrap > .ms-options > ul label.focused,
124
- .ms-options-wrap > .ms-options > ul label:hover {
125
- background-color: #efefef;
126
- border-color: #999;
127
- }
128
-
129
- .ms-options-wrap > .ms-options > ul li.selected label {
130
- background-color: #efefef;
131
- border-color: transparent;
132
- }
133
-
134
- .ms-options-wrap > .ms-options > ul input[type="checkbox"] {
135
- margin: 0 5px 0 0;
136
- position: absolute;
137
- left: 4px;
138
- top: 7px;
139
- }
140
-
141
- .ms-options-wrap > .ms-options.hide-checkbox > ul input[type="checkbox"] {
142
- position: absolute !important;
143
- height: 1px;
144
- width: 1px;
145
- overflow: hidden;
146
- clip: rect(1px 1px 1px 1px);
147
- clip: rect(1px, 1px, 1px, 1px);
148
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/js/lib/jquery.multiselect.js DELETED
@@ -1,936 +0,0 @@
1
- /**
2
- * Display a nice easy to use multiselect list
3
- * @Version: 2.4.17
4
- * @Author: Patrick Springstubbe
5
- * @Contact: @JediNobleclem
6
- * @Website: springstubbe.us
7
- * @Source: https://github.com/nobleclem/jQuery-MultiSelect
8
- *
9
- * Usage:
10
- * $('select[multiple]').multiselect();
11
- * $('select[multiple]').multiselect({ texts: { placeholder: 'Select options' } });
12
- * $('select[multiple]').multiselect('reload');
13
- * $('select[multiple]').multiselect( 'loadOptions', [{
14
- * name : 'Option Name 1',
15
- * value : 'option-value-1',
16
- * checked: false,
17
- * attributes : {
18
- * custom1: 'value1',
19
- * custom2: 'value2'
20
- * }
21
- * },{
22
- * name : 'Option Name 2',
23
- * value : 'option-value-2',
24
- * checked: false,
25
- * attributes : {
26
- * custom1: 'value1',
27
- * custom2: 'value2'
28
- * }
29
- * }]);
30
- *
31
- **/
32
- (function($){
33
- var defaults = {
34
- columns: 1, // how many columns should be use to show options
35
- search : false, // include option search box
36
-
37
- // search filter options
38
- searchOptions : {
39
- delay : 250, // time (in ms) between keystrokes until search happens
40
- showOptGroups: false, // show option group titles if no options remaining
41
- searchText : true, // search within the text
42
- searchValue : false, // search within the value
43
- onSearch : function( element ){} // fires on keyup before search on options happens
44
- },
45
-
46
- // plugin texts
47
- texts: {
48
- placeholder : 'Select options', // text to use in dummy input
49
- search : 'Search', // search input placeholder text
50
- selectedOptions: ' selected', // selected suffix text
51
- selectAll : 'Select all', // select all text
52
- unselectAll : 'Unselect all', // unselect all text
53
- noneSelected : 'None Selected' // None selected text
54
- },
55
-
56
- // general options
57
- selectAll : false, // add select all option
58
- selectGroup : false, // select entire optgroup
59
- minHeight : 200, // minimum height of option overlay
60
- maxHeight : null, // maximum height of option overlay
61
- maxWidth : null, // maximum width of option overlay (or selector)
62
- maxPlaceholderWidth: null, // maximum width of placeholder button
63
- maxPlaceholderOpts : 10, // maximum number of placeholder options to show until "# selected" shown instead
64
- showCheckbox : true, // display the checkbox to the user
65
- checkboxAutoFit : false, // auto calc checkbox padding
66
- optionAttributes : [], // attributes to copy to the checkbox from the option element
67
-
68
- // Callbacks
69
- onLoad : function( element ){}, // fires at end of list initialization
70
- onOptionClick : function( element, option ){}, // fires when an option is clicked
71
- onControlClose: function( element ){}, // fires when the options list is closed
72
- onSelectAll : function( element, selected ){}, // fires when (un)select all is clicked
73
- onPlaceholder : function( element, placeholder, selectedOpts ){}, // fires when the placeholder txt is updated
74
- };
75
-
76
- var msCounter = 1; // counter for each select list
77
- var msOptCounter = 1; // counter for each option on page
78
-
79
- // FOR LEGACY BROWSERS (talking to you IE8)
80
- if( typeof Array.prototype.map !== 'function' ) {
81
- Array.prototype.map = function( callback, thisArg ) {
82
- if( typeof thisArg === 'undefined' ) {
83
- thisArg = this;
84
- }
85
-
86
- return $.isArray( thisArg ) ? $.map( thisArg, callback ) : [];
87
- };
88
- }
89
- if( typeof String.prototype.trim !== 'function' ) {
90
- String.prototype.trim = function() {
91
- return this.replace(/^\s+|\s+$/g, '');
92
- };
93
- }
94
-
95
- function MultiSelect( element, options )
96
- {
97
- this.element = element;
98
- this.options = $.extend( true, {}, defaults, options );
99
- this.updateSelectAll = true;
100
- this.updatePlaceholder = true;
101
- this.listNumber = msCounter;
102
-
103
- msCounter = msCounter + 1; // increment counter
104
-
105
- /* Make sure its a multiselect list */
106
- if( !$(this.element).attr('multiple') ) {
107
- throw new Error( '[jQuery-MultiSelect] Select list must be a multiselect list in order to use this plugin' );
108
- }
109
-
110
- /* Options validation checks */
111
- if( this.options.search ){
112
- if( !this.options.searchOptions.searchText && !this.options.searchOptions.searchValue ){
113
- throw new Error( '[jQuery-MultiSelect] Either searchText or searchValue should be true.' );
114
- }
115
- }
116
-
117
- /** BACKWARDS COMPATIBILITY **/
118
- if( 'placeholder' in this.options ) {
119
- this.options.texts.placeholder = this.options.placeholder;
120
- delete this.options.placeholder;
121
- }
122
- if( 'default' in this.options.searchOptions ) {
123
- this.options.texts.search = this.options.searchOptions['default'];
124
- delete this.options.searchOptions['default'];
125
- }
126
- /** END BACKWARDS COMPATIBILITY **/
127
-
128
- // load this instance
129
- this.load();
130
- }
131
-
132
- MultiSelect.prototype = {
133
- /* LOAD CUSTOM MULTISELECT DOM/ACTIONS */
134
- load: function() {
135
- var instance = this;
136
-
137
- // make sure this is a select list and not loaded
138
- if( (instance.element.nodeName != 'SELECT') || $(instance.element).hasClass('jqmsLoaded') ) {
139
- return true;
140
- }
141
-
142
- // sanity check so we don't double load on a select element
143
- $(instance.element).addClass('jqmsLoaded ms-list-'+ instance.listNumber ).data( 'plugin_multiselect-instance', instance );
144
-
145
- // add option container
146
- $(instance.element).after('<div id="ms-list-'+ instance.listNumber +'" class="ms-options-wrap"><button type="button"><span>None Selected</span></button><div class="ms-options"><ul></ul></div></div>');
147
-
148
- var placeholder = $(instance.element).siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> button:first-child');
149
- var optionsWrap = $(instance.element).siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> .ms-options');
150
- var optionsList = optionsWrap.find('> ul');
151
-
152
- // don't show checkbox (add class for css to hide checkboxes)
153
- if( !instance.options.showCheckbox ) {
154
- optionsWrap.addClass('hide-checkbox');
155
- }
156
- else if( instance.options.checkboxAutoFit ) {
157
- optionsWrap.addClass('checkbox-autofit');
158
- }
159
-
160
- // check if list is disabled
161
- if( $(instance.element).prop( 'disabled' ) ) {
162
- placeholder.prop( 'disabled', true );
163
- }
164
-
165
- // set placeholder maxWidth
166
- if( instance.options.maxPlaceholderWidth ) {
167
- placeholder.css( 'maxWidth', instance.options.maxPlaceholderWidth );
168
- }
169
-
170
- // override with user defined maxHeight
171
- if( instance.options.maxHeight ) {
172
- var maxHeight = instance.options.maxHeight;
173
- }
174
- else {
175
- // cacl default maxHeight
176
- var maxHeight = ($(window).height() - optionsWrap.offset().top + $(window).scrollTop() - 20);
177
- }
178
-
179
- // maxHeight cannot be less than options.minHeight
180
- maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight;
181
-
182
- optionsWrap.css({
183
- maxWidth : instance.options.maxWidth,
184
- minHeight: instance.options.minHeight,
185
- maxHeight: maxHeight,
186
- });
187
-
188
- // isolate options scroll
189
- // @source: https://github.com/nobleclem/jQuery-IsolatedScroll
190
- optionsWrap.on( 'touchmove mousewheel DOMMouseScroll', function ( e ) {
191
- if( ($(this).outerHeight() < $(this)[0].scrollHeight) ) {
192
- var e0 = e.originalEvent,
193
- delta = e0.wheelDelta || -e0.detail;
194
-
195
- if( ($(this).outerHeight() + $(this)[0].scrollTop) > $(this)[0].scrollHeight ) {
196
- e.preventDefault();
197
- this.scrollTop += ( delta < 0 ? 1 : -1 );
198
- }
199
- }
200
- });
201
-
202
- // hide options menus if click happens off of the list placeholder button
203
- $(document).off('click.ms-hideopts').on('click.ms-hideopts', function( event ){
204
- if( !$(event.target).closest('.ms-options-wrap').length ) {
205
- $('.ms-options-wrap.ms-active > .ms-options').each(function(){
206
- $(this).closest('.ms-options-wrap').removeClass('ms-active');
207
-
208
- var listID = $(this).closest('.ms-options-wrap').attr('id');
209
-
210
- var thisInst = $(this).parent().siblings('.'+ listID +'.jqmsLoaded').data('plugin_multiselect-instance');
211
-
212
- // USER CALLBACK
213
- if( typeof thisInst.options.onControlClose == 'function' ) {
214
- thisInst.options.onControlClose( thisInst.element );
215
- }
216
- });
217
- }
218
- // hide open option lists if escape key pressed
219
- }).on('keydown', function( event ){
220
- if( (event.keyCode || event.which) == 27 ) { // esc key
221
- $(this).trigger('click.ms-hideopts');
222
- }
223
- });
224
-
225
- // handle pressing enter|space while tabbing through
226
- placeholder.on('keydown', function( event ){
227
- var code = (event.keyCode || event.which);
228
- if( (code == 13) || (code == 32) ) { // enter OR space
229
- placeholder.trigger( 'mousedown' );
230
- }
231
- });
232
-
233
- // disable button action
234
- placeholder.on( 'mousedown', function( event ){
235
- // ignore if its not a left click
236
- if( event.which && (event.which != 1) ) {
237
- return true;
238
- }
239
-
240
- // hide other menus before showing this one
241
- $('.ms-options-wrap.ms-active').each(function(){
242
- if( $(this).siblings( '.'+ $(this).attr('id') +'.jqmsLoaded')[0] != optionsWrap.parent().siblings('.ms-list-'+ instance.listNumber +'.jqmsLoaded')[0] ) {
243
- $(this).removeClass('ms-active');
244
-
245
- var thisInst = $(this).siblings( '.'+ $(this).attr('id') +'.jqmsLoaded').data('plugin_multiselect-instance');
246
-
247
- // USER CALLBACK
248
- if( typeof thisInst.options.onControlClose == 'function' ) {
249
- thisInst.options.onControlClose( thisInst.element );
250
- }
251
- }
252
- });
253
-
254
- // show/hide options
255
- optionsWrap.closest('.ms-options-wrap').toggleClass('ms-active');
256
-
257
- // recalculate height
258
- if( optionsWrap.closest('.ms-options-wrap').hasClass('ms-active') ) {
259
- optionsWrap.css( 'maxHeight', '' );
260
-
261
- // override with user defined maxHeight
262
- if( instance.options.maxHeight ) {
263
- var maxHeight = instance.options.maxHeight;
264
- }
265
- else {
266
- // cacl default maxHeight
267
- var maxHeight = ($(window).height() - optionsWrap.offset().top + $(window).scrollTop() - 20);
268
- }
269
-
270
- if( maxHeight ) {
271
- // maxHeight cannot be less than options.minHeight
272
- maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight;
273
-
274
- optionsWrap.css( 'maxHeight', maxHeight );
275
- }
276
- }
277
- else if( typeof instance.options.onControlClose == 'function' ) {
278
- instance.options.onControlClose( instance.element );
279
- }
280
- }).click(function( event ){ event.preventDefault(); });
281
-
282
- // add placeholder copy
283
- if( instance.options.texts.placeholder ) {
284
- placeholder.find('span').text( instance.options.texts.placeholder );
285
- }
286
-
287
- // add search box
288
- if( instance.options.search ) {
289
- optionsList.before('<div class="ms-search"><input type="text" value="" placeholder="'+ instance.options.texts.search +'" /></div>');
290
-
291
- var search = optionsWrap.find('.ms-search input');
292
- search.on('keyup', function(){
293
- // ignore keystrokes that don't make a difference
294
- if( $(this).data('lastsearch') == $(this).val() ) {
295
- return true;
296
- }
297
-
298
- // pause timeout
299
- if( $(this).data('searchTimeout') ) {
300
- clearTimeout( $(this).data('searchTimeout') );
301
- }
302
-
303
- var thisSearchElem = $(this);
304
-
305
- $(this).data('searchTimeout', setTimeout(function(){
306
- thisSearchElem.data('lastsearch', thisSearchElem.val() );
307
-
308
- // USER CALLBACK
309
- if( typeof instance.options.searchOptions.onSearch == 'function' ) {
310
- instance.options.searchOptions.onSearch( instance.element );
311
- }
312
-
313
- // search non optgroup li's
314
- var searchString = $.trim( search.val().toLowerCase() );
315
- if( searchString ) {
316
- optionsList.find('li[data-search-term*="'+ searchString +'"]:not(.optgroup)').removeClass('ms-hidden');
317
- optionsList.find('li:not([data-search-term*="'+ searchString +'"], .optgroup)').addClass('ms-hidden');
318
- }
319
- else {
320
- optionsList.find('.ms-hidden').removeClass('ms-hidden');
321
- }
322
-
323
- // show/hide optgroups based on if there are items visible within
324
- if( !instance.options.searchOptions.showOptGroups ) {
325
- optionsList.find('.optgroup').each(function(){
326
- if( $(this).find('li:not(.ms-hidden)').length ) {
327
- $(this).show();
328
- }
329
- else {
330
- $(this).hide();
331
- }
332
- });
333
- }
334
-
335
- instance._updateSelectAllText();
336
- }, instance.options.searchOptions.delay ));
337
- });
338
- }
339
-
340
- // add global select all options
341
- if( instance.options.selectAll ) {
342
- optionsList.before('<a href="#" class="ms-selectall global">' + instance.options.texts.selectAll + '</a>');
343
- }
344
-
345
- // handle select all option
346
- optionsWrap.on('click', '.ms-selectall', function( event ){
347
- event.preventDefault();
348
-
349
- instance.updateSelectAll = false;
350
- instance.updatePlaceholder = false;
351
-
352
- var select = optionsWrap.parent().siblings('.ms-list-'+ instance.listNumber +'.jqmsLoaded');
353
-
354
- if( $(this).hasClass('global') ) {
355
- // check if any options are not selected if so then select them
356
- if( optionsList.find('li:not(.optgroup, .selected, .ms-hidden)').length ) {
357
- // get unselected vals, mark as selected, return val list
358
- optionsList.find('li:not(.optgroup, .selected, .ms-hidden)').addClass('selected');
359
- optionsList.find('li.selected input[type="checkbox"]:not(:disabled)').prop( 'checked', true );
360
- }
361
- // deselect everything
362
- else {
363
- optionsList.find('li:not(.optgroup, .ms-hidden).selected').removeClass('selected');
364
- optionsList.find('li:not(.optgroup, .ms-hidden, .selected) input[type="checkbox"]:not(:disabled)').prop( 'checked', false );
365
- }
366
- }
367
- else if( $(this).closest('li').hasClass('optgroup') ) {
368
- var optgroup = $(this).closest('li.optgroup');
369
-
370
- // check if any selected if so then select them
371
- if( optgroup.find('li:not(.selected, .ms-hidden)').length ) {
372
- optgroup.find('li:not(.selected, .ms-hidden)').addClass('selected');
373
- optgroup.find('li.selected input[type="checkbox"]:not(:disabled)').prop( 'checked', true );
374
- }
375
- // deselect everything
376
- else {
377
- optgroup.find('li:not(.ms-hidden).selected').removeClass('selected');
378
- optgroup.find('li:not(.ms-hidden, .selected) input[type="checkbox"]:not(:disabled)').prop( 'checked', false );
379
- }
380
- }
381
-
382
- var vals = [];
383
- optionsList.find('li.selected input[type="checkbox"]').each(function(){
384
- vals.push( $(this).val() );
385
- });
386
- select.val( vals ).trigger('change');
387
-
388
- instance.updateSelectAll = true;
389
- instance.updatePlaceholder = true;
390
-
391
- // USER CALLBACK
392
- if( typeof instance.options.onSelectAll == 'function' ) {
393
- instance.options.onSelectAll( instance.element, vals.length );
394
- }
395
-
396
- instance._updateSelectAllText();
397
- instance._updatePlaceholderText();
398
- });
399
-
400
- // add options to wrapper
401
- var options = [];
402
- $(instance.element).children().each(function(){
403
- if( this.nodeName == 'OPTGROUP' ) {
404
- var groupOptions = [];
405
-
406
- $(this).children('option').each(function(){
407
- var thisOptionAtts = {};
408
- for( var i = 0; i < instance.options.optionAttributes.length; i++ ) {
409
- var thisOptAttr = instance.options.optionAttributes[ i ];
410
-
411
- if( $(this).attr( thisOptAttr ) !== undefined ) {
412
- thisOptionAtts[ thisOptAttr ] = $(this).attr( thisOptAttr );
413
- }
414
- }
415
-
416
- groupOptions.push({
417
- name : $(this).text(),
418
- value : $(this).val(),
419
- checked: $(this).prop( 'selected' ),
420
- attributes: thisOptionAtts
421
- });
422
- });
423
-
424
- options.push({
425
- label : $(this).attr('label'),
426
- options: groupOptions
427
- });
428
- }
429
- else if( this.nodeName == 'OPTION' ) {
430
- var thisOptionAtts = {};
431
- for( var i = 0; i < instance.options.optionAttributes.length; i++ ) {
432
- var thisOptAttr = instance.options.optionAttributes[ i ];
433
-
434
- if( $(this).attr( thisOptAttr ) !== undefined ) {
435
- thisOptionAtts[ thisOptAttr ] = $(this).attr( thisOptAttr );
436
- }
437
- }
438
-
439
- options.push({
440
- name : $(this).text(),
441
- value : $(this).val(),
442
- checked : $(this).prop( 'selected' ),
443
- attributes: thisOptionAtts
444
- });
445
- }
446
- else {
447
- // bad option
448
- return true;
449
- }
450
- });
451
- instance.loadOptions( options, true, false );
452
-
453
- // BIND SELECT ACTION
454
- optionsWrap.on( 'click', 'input[type="checkbox"]', function(){
455
- $(this).closest( 'li' ).toggleClass( 'selected' );
456
-
457
- var select = optionsWrap.parent().siblings('.ms-list-'+ instance.listNumber +'.jqmsLoaded');
458
-
459
- // toggle clicked option
460
- select.find('option[value="'+ instance._escapeSelector( $(this).val() ) +'"]').prop(
461
- 'selected', $(this).is(':checked')
462
- ).closest('select').trigger('change');
463
-
464
- // USER CALLBACK
465
- if( typeof instance.options.onOptionClick == 'function' ) {
466
- instance.options.onOptionClick(instance.element, this);
467
- }
468
-
469
- instance._updateSelectAllText();
470
- instance._updatePlaceholderText();
471
- });
472
-
473
- // BIND FOCUS EVENT
474
- optionsWrap.on('focusin', 'input[type="checkbox"]', function(){
475
- $(this).closest('label').addClass('focused');
476
- }).on('focusout', 'input[type="checkbox"]', function(){
477
- $(this).closest('label').removeClass('focused');
478
- });
479
-
480
- // USER CALLBACK
481
- if( typeof instance.options.onLoad === 'function' ) {
482
- instance.options.onLoad( instance.element );
483
- }
484
-
485
- // hide native select list
486
- $(instance.element).hide();
487
- },
488
-
489
- /* LOAD SELECT OPTIONS */
490
- loadOptions: function( options, overwrite, updateSelect ) {
491
- overwrite = (typeof overwrite == 'boolean') ? overwrite : true;
492
- updateSelect = (typeof updateSelect == 'boolean') ? updateSelect : true;
493
-
494
- var instance = this;
495
- var select = $(instance.element);
496
- var optionsList = select.siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> .ms-options > ul');
497
- var optionsWrap = select.siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> .ms-options');
498
-
499
- if( overwrite ) {
500
- optionsList.find('> li').remove();
501
-
502
- if( updateSelect ) {
503
- select.find('> *').remove();
504
- }
505
- }
506
-
507
- var containers = [];
508
- for( var key in options ) {
509
- // Prevent prototype methods injected into options from being iterated over.
510
- if( !options.hasOwnProperty( key ) ) {
511
- continue;
512
- }
513
-
514
- var thisOption = options[ key ];
515
- var container = $('<li/>');
516
- var appendContainer = true;
517
-
518
- // OPTION
519
- if( thisOption.hasOwnProperty('value') ) {
520
- if( instance.options.showCheckbox && instance.options.checkboxAutoFit ) {
521
- container.addClass('ms-reflow');
522
- }
523
-
524
- // add option to ms dropdown
525
- instance._addOption( container, thisOption );
526
-
527
- if( updateSelect ) {
528
- var selOption = $('<option value="'+ thisOption.value +'">'+ thisOption.name +'</option>');
529
-
530
- // add custom user attributes
531
- if( thisOption.hasOwnProperty('attributes') && Object.keys( thisOption.attributes ).length ) {
532
- selOption.attr( thisOption.attributes );
533
- }
534
-
535
- // mark option as selected
536
- if( thisOption.checked ) {
537
- selOption.prop( 'selected', true );
538
- }
539
-
540
- select.append( selOption );
541
- }
542
- }
543
- // OPTGROUP
544
- else if( thisOption.hasOwnProperty('options') ) {
545
- var optGroup = $('<optgroup label="'+ thisOption.label +'"></optgroup>');
546
-
547
- optionsList.find('> li.optgroup > span.label').each(function(){
548
- if( $(this).text() == thisOption.label ) {
549
- container = $(this).closest('.optgroup');
550
- appendContainer = false;
551
- }
552
- });
553
-
554
- // prepare to append optgroup to select element
555
- if( updateSelect ) {
556
- if( select.find('optgroup[label="'+ thisOption.label +'"]').length ) {
557
- optGroup = select.find('optgroup[label="'+ thisOption.label +'"]');
558
- }
559
- else {
560
- select.append( optGroup );
561
- }
562
- }
563
-
564
- // setup container
565
- if( appendContainer ) {
566
- container.addClass('optgroup');
567
- container.append('<span class="label">'+ thisOption.label +'</span>');
568
- container.find('> .label').css({
569
- clear: 'both'
570
- });
571
-
572
- // add select all link
573
- if( instance.options.selectGroup ) {
574
- container.append('<a href="#" class="ms-selectall">' + instance.options.texts.selectAll + '</a>');
575
- }
576
-
577
- container.append('<ul/>');
578
- }
579
-
580
- for( var gKey in thisOption.options ) {
581
- // Prevent prototype methods injected into options from
582
- // being iterated over.
583
- if( !thisOption.options.hasOwnProperty( gKey ) ) {
584
- continue;
585
- }
586
-
587
- var thisGOption = thisOption.options[ gKey ];
588
- var gContainer = $('<li/>');
589
- if( instance.options.showCheckbox && instance.options.checkboxAutoFit ) {
590
- gContainer.addClass('ms-reflow');
591
- }
592
-
593
- // no clue what this is we hit (skip)
594
- if( !thisGOption.hasOwnProperty('value') ) {
595
- continue;
596
- }
597
-
598
- instance._addOption( gContainer, thisGOption );
599
-
600
- container.find('> ul').append( gContainer );
601
-
602
- // add option to optgroup in select element
603
- if( updateSelect ) {
604
- var selOption = $('<option value="'+ thisGOption.value +'">'+ thisGOption.name +'</option>');
605
-
606
- // add custom user attributes
607
- if( thisGOption.hasOwnProperty('attributes') && Object.keys( thisGOption.attributes ).length ) {
608
- selOption.attr( thisGOption.attributes );
609
- }
610
-
611
- // mark option as selected
612
- if( thisGOption.checked ) {
613
- selOption.prop( 'selected', true );
614
- }
615
-
616
- optGroup.append( selOption );
617
- }
618
- }
619
- }
620
- else {
621
- // no clue what this is we hit (skip)
622
- continue;
623
- }
624
-
625
- if( appendContainer ) {
626
- containers.push( container );
627
- }
628
- }
629
- optionsList.append( containers );
630
-
631
- // pad out label for room for the checkbox
632
- if( instance.options.checkboxAutoFit && instance.options.showCheckbox && !optionsWrap.hasClass('hide-checkbox') ) {
633
- var chkbx = optionsList.find('.ms-reflow:eq(0) input[type="checkbox"]');
634
- if( chkbx.length ) {
635
- var checkboxWidth = chkbx.outerWidth();
636
- checkboxWidth = checkboxWidth ? checkboxWidth : 15;
637
-
638
- optionsList.find('.ms-reflow label').css(
639
- 'padding-left',
640
- (parseInt( chkbx.closest('label').css('padding-left') ) * 2) + checkboxWidth
641
- );
642
-
643
- optionsList.find('.ms-reflow').removeClass('ms-reflow');
644
- }
645
- }
646
-
647
- // update placeholder text
648
- instance._updatePlaceholderText();
649
-
650
- // RESET COLUMN STYLES
651
- optionsWrap.find('ul').css({
652
- 'column-count' : '',
653
- 'column-gap' : '',
654
- '-webkit-column-count': '',
655
- '-webkit-column-gap' : '',
656
- '-moz-column-count' : '',
657
- '-moz-column-gap' : ''
658
- });
659
-
660
- // COLUMNIZE
661
- if( select.find('optgroup').length ) {
662
- // float non grouped options
663
- optionsList.find('> li:not(.optgroup)').css({
664
- 'float': 'left',
665
- width: (100 / instance.options.columns) +'%'
666
- });
667
-
668
- // add CSS3 column styles
669
- optionsList.find('li.optgroup').css({
670
- clear: 'both'
671
- }).find('> ul').css({
672
- 'column-count' : instance.options.columns,
673
- 'column-gap' : 0,
674
- '-webkit-column-count': instance.options.columns,
675
- '-webkit-column-gap' : 0,
676
- '-moz-column-count' : instance.options.columns,
677
- '-moz-column-gap' : 0
678
- });
679
-
680
- // for crappy IE versions float grouped options
681
- if( this._ieVersion() && (this._ieVersion() < 10) ) {
682
- optionsList.find('li.optgroup > ul > li').css({
683
- 'float': 'left',
684
- width: (100 / instance.options.columns) +'%'
685
- });
686
- }
687
- }
688
- else {
689
- // add CSS3 column styles
690
- optionsList.css({
691
- 'column-count' : instance.options.columns,
692
- 'column-gap' : 0,
693
- '-webkit-column-count': instance.options.columns,
694
- '-webkit-column-gap' : 0,
695
- '-moz-column-count' : instance.options.columns,
696
- '-moz-column-gap' : 0
697
- });
698
-
699
- // for crappy IE versions float grouped options
700
- if( this._ieVersion() && (this._ieVersion() < 10) ) {
701
- optionsList.find('> li').css({
702
- 'float': 'left',
703
- width: (100 / instance.options.columns) +'%'
704
- });
705
- }
706
- }
707
-
708
- // update un/select all logic
709
- instance._updateSelectAllText();
710
- },
711
-
712
- /* UPDATE MULTISELECT CONFIG OPTIONS */
713
- settings: function( options ) {
714
- this.options = $.extend( true, {}, this.options, options );
715
- this.reload();
716
- },
717
-
718
- /* RESET THE DOM */
719
- unload: function() {
720
- $(this.element).siblings('#ms-list-'+ this.listNumber +'.ms-options-wrap').remove();
721
- $(this.element).show(function(){
722
- $(this).css('display','').removeClass('jqmsLoaded');
723
- });
724
- },
725
-
726
- /* RELOAD JQ MULTISELECT LIST */
727
- reload: function() {
728
- // remove existing options
729
- $(this.element).siblings('#ms-list-'+ this.listNumber +'.ms-options-wrap').remove();
730
- $(this.element).removeClass('jqmsLoaded');
731
-
732
- // load element
733
- this.load();
734
- },
735
-
736
- // RESET BACK TO DEFAULT VALUES & RELOAD
737
- reset: function() {
738
- var defaultVals = [];
739
- $(this.element).find('option').each(function(){
740
- if( $(this).prop('defaultSelected') ) {
741
- defaultVals.push( $(this).val() );
742
- }
743
- });
744
-
745
- $(this.element).val( defaultVals );
746
-
747
- this.reload();
748
- },
749
-
750
- disable: function( status ) {
751
- status = (typeof status === 'boolean') ? status : true;
752
- $(this.element).prop( 'disabled', status );
753
- $(this.element).siblings('#ms-list-'+ this.listNumber +'.ms-options-wrap').find('button:first-child')
754
- .prop( 'disabled', status );
755
- },
756
-
757
- /** PRIVATE FUNCTIONS **/
758
- // update the un/select all texts based on selected options and visibility
759
- _updateSelectAllText: function(){
760
- if( !this.updateSelectAll ) {
761
- return;
762
- }
763
-
764
- var instance = this;
765
-
766
- // select all not used at all so just do nothing
767
- if( !instance.options.selectAll && !instance.options.selectGroup ) {
768
- return;
769
- }
770
-
771
- var optionsWrap = $(instance.element).siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> .ms-options');
772
-
773
- // update un/select all text
774
- optionsWrap.find('.ms-selectall').each(function(){
775
- var unselected = $(this).parent().find('li:not(.optgroup,.selected,.ms-hidden)');
776
-
777
- $(this).text(
778
- unselected.length ? instance.options.texts.selectAll : instance.options.texts.unselectAll
779
- );
780
- });
781
- },
782
-
783
- // update selected placeholder text
784
- _updatePlaceholderText: function(){
785
- if( !this.updatePlaceholder ) {
786
- return;
787
- }
788
-
789
- var instance = this;
790
- var select = $(instance.element);
791
- var selectVals = select.val() ? select.val() : [];
792
- var placeholder = select.siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> button:first-child');
793
- var placeholderTxt = placeholder.find('span');
794
- var optionsWrap = select.siblings('#ms-list-'+ instance.listNumber +'.ms-options-wrap').find('> .ms-options');
795
-
796
- // if there are disabled options get those values as well
797
- if( select.find('option:selected:disabled').length ) {
798
- selectVals = [];
799
- select.find('option:selected').each(function(){
800
- selectVals.push( $(this).val() );
801
- });
802
- }
803
-
804
- // get selected options
805
- var selOpts = [];
806
- for( var key in selectVals ) {
807
- // Prevent prototype methods injected into options from being iterated over.
808
- if( !selectVals.hasOwnProperty( key ) ) {
809
- continue;
810
- }
811
-
812
- selOpts.push(
813
- $.trim( select.find('option[value="'+ instance._escapeSelector( selectVals[ key ] ) +'"]').text() )
814
- );
815
-
816
- if( selOpts.length >= instance.options.maxPlaceholderOpts ) {
817
- break;
818
- }
819
- }
820
-
821
- // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED
822
- placeholderTxt.text( selOpts.join( ', ' ) );
823
-
824
- if( selOpts.length ) {
825
- optionsWrap.closest('.ms-options-wrap').addClass('ms-has-selections');
826
-
827
- // USER CALLBACK
828
- if( typeof instance.options.onPlaceholder == 'function' ) {
829
- instance.options.onPlaceholder( instance.element, placeholderTxt, selOpts );
830
- }
831
- }
832
- else {
833
- optionsWrap.closest('.ms-options-wrap').removeClass('ms-has-selections');
834
- }
835
-
836
- // replace placeholder text
837
- if( !selOpts.length ) {
838
- placeholderTxt.text( instance.options.texts.placeholder );
839
- }
840
- // if copy is larger than button width use "# selected"
841
- else if( (placeholderTxt.width() > placeholder.width()) || (selOpts.length != selectVals.length) ) {
842
- placeholderTxt.text( selectVals.length + instance.options.texts.selectedOptions );
843
- }
844
- },
845
-
846
- // Add option to the custom dom list
847
- _addOption: function( container, option ) {
848
- var instance = this;
849
- var thisOption = $('<label/>', {
850
- for : 'ms-opt-'+ msOptCounter,
851
- text: option.name
852
- });
853
-
854
- var thisCheckbox = $('<input>', {
855
- type : 'checkbox',
856
- title: option.name,
857
- id : 'ms-opt-'+ msOptCounter,
858
- value: option.value
859
- });
860
-
861
- // add user defined attributes
862
- if( option.hasOwnProperty('attributes') && Object.keys( option.attributes ).length ) {
863
- thisCheckbox.attr( option.attributes );
864
- }
865
-
866
- if( option.checked ) {
867
- container.addClass('default selected');
868
- thisCheckbox.prop( 'checked', true );
869
- }
870
-
871
- thisOption.prepend( thisCheckbox );
872
-
873
- var searchTerm = '';
874
- if( instance.options.searchOptions.searchText ) {
875
- searchTerm += ' ' + option.name.toLowerCase();
876
- }
877
- if( instance.options.searchOptions.searchValue ) {
878
- searchTerm += ' ' + option.value.toLowerCase();
879
- }
880
-
881
- container.attr( 'data-search-term', $.trim( searchTerm ) ).prepend( thisOption );
882
-
883
- msOptCounter = msOptCounter + 1;
884
- },
885
-
886
- // check ie version
887
- _ieVersion: function() {
888
- var myNav = navigator.userAgent.toLowerCase();
889
- return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
890
- },
891
-
892
- // escape selector
893
- _escapeSelector: function( string ) {
894
- if( typeof $.escapeSelector == 'function' ) {
895
- return $.escapeSelector( string );
896
- }
897
- else {
898
- return string.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&");
899
- }
900
- }
901
- };
902
-
903
- // ENABLE JQUERY PLUGIN FUNCTION
904
- $.fn.multiselect = function( options ){
905
- if( !this.length ) {
906
- return;
907
- }
908
-
909
- var args = arguments;
910
- var ret;
911
-
912
- // menuize each list
913
- if( (options === undefined) || (typeof options === 'object') ) {
914
- return this.each(function(){
915
- if( !$.data( this, 'plugin_multiselect' ) ) {
916
- $.data( this, 'plugin_multiselect', new MultiSelect( this, options ) );
917
- }
918
- });
919
- } else if( (typeof options === 'string') && (options[0] !== '_') && (options !== 'init') ) {
920
- this.each(function(){
921
- var instance = $.data( this, 'plugin_multiselect' );
922
-
923
- if( instance instanceof MultiSelect && typeof instance[ options ] === 'function' ) {
924
- ret = instance[ options ].apply( instance, Array.prototype.slice.call( args, 1 ) );
925
- }
926
-
927
- // special destruct handler
928
- if( options === 'unload' ) {
929
- $.data( this, 'plugin_multiselect', null );
930
- }
931
- });
932
-
933
- return ret;
934
- }
935
- };
936
- }(jQuery));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/js/settings.js DELETED
@@ -1,994 +0,0 @@
1
- "use strict";
2
-
3
- var itsecSettingsPage = {
4
-
5
- events: jQuery( {} ),
6
-
7
- init: function() {
8
- jQuery( '.itsec-module-settings-container' ).hide();
9
-
10
- this.bindEvents();
11
-
12
- jQuery( '.itsec-settings-view-toggle .itsec-selected' ).removeClass( 'itsec-selected' ).trigger( 'click' );
13
- jQuery( '.itsec-settings-toggle' ).trigger( 'change' );
14
-
15
- this.initFilters();
16
- this.initCurrentModule();
17
- this.makeNoticesDismissible();
18
- },
19
-
20
- initFilters: function() {
21
- var module_type = itsecUtil.getUrlParameter( 'module_type' );
22
- if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
23
- module_type = 'recommended';
24
- }
25
- jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) + ' a' ).trigger( 'click' );
26
- },
27
-
28
- initCurrentModule: function() {
29
-
30
- var module = itsecUtil.getUrlParameter( 'module' );
31
- if ( 'string' === typeof module ) {
32
- jQuery( '#itsec-module-card-' + module.replace( /[^\w-]/g, '' ) + ' button.itsec-toggle-settings' ).trigger( 'click' );
33
- }
34
- },
35
-
36
- bindEvents: function() {
37
-
38
- if ( itsecSettingsPage.bindEvents.bound ) {
39
- return;
40
- }
41
-
42
- jQuery(window).on("popstate", function(e, data) {
43
- if ( null !== e.originalEvent.state && 'string' == typeof e.originalEvent.state.module && '' !== e.originalEvent.state.module.replace( /[^\w-]/g, '' ) ) {
44
- jQuery( '#itsec-module-card-' + e.originalEvent.state.module.replace( /[^\w-]/g, '' ) + ' button.itsec-toggle-settings' ).trigger( 'itsec-popstate' );
45
- } else {
46
- itsecSettingsPage.closeGridSettingsModal( e );
47
- }
48
-
49
- if ( null !== e.originalEvent.state && 'string' == typeof e.originalEvent.state.module_type && '' !== e.originalEvent.state.module_type.replace( /[^\w-]/g, '' ) ) {
50
- jQuery( '#itsec-module-filter-' + e.originalEvent.state.module_type.replace( /[^\w-]/g, '' ) + ' a' ).trigger( 'itsec-popstate' );
51
- }
52
- });
53
-
54
- var $container = jQuery( '#wpcontent' );
55
-
56
- $container.on( 'click', '.itsec-module-filter a', this.filterView );
57
- $container.on( 'itsec-popstate', '.itsec-module-filter a', this.filterView );
58
- $container.on( 'click', '.itsec-settings-view-toggle a', this.toggleView );
59
- // $container.on( 'click', '.itsec-toggle-settings, .itsec-module-card-content h2', this.toggleSettings );
60
- $container.on( 'click', 'a[data-module-link]', this.openModuleFromLink );
61
- $container.on( 'click', '.list .itsec-module-card:not(.itsec-module-pro-upsell) .itsec-module-card-content, .itsec-toggle-settings, .itsec-module-settings-cancel', this.toggleSettings );
62
- $container.on( 'itsec-popstate', '.list .itsec-module-card-content, .itsec-toggle-settings', this.toggleSettings );
63
- $container.on( 'click', '.itsec-close-modal, .itsec-modal-background', this.closeGridSettingsModal );
64
- $container.on( 'keyup', this.closeGridSettingsModal );
65
- $container.on( 'click', '.itsec-toggle-activation', this.toggleModuleActivation );
66
- $container.on( 'click', '.itsec-module-settings-save', this.saveSettings );
67
- $container.on( 'click', '.itsec-reload-module', this.reloadModule );
68
- $container.on( 'click', '.itsec-details-toggle-container a[href="#"]', this.toggleDetails );
69
-
70
- $container.on( 'change', '#itsec-filter', this.logPageChangeFilter );
71
-
72
- // For use by module content to show/hide settings sections based upon an input.
73
- $container.on( 'change', '.itsec-settings-toggle', this.toggleModuleContent );
74
- $container.on( 'click', '.itsec-copy-trigger', this.handleCopy );
75
-
76
- itsecSettingsPage.bindEvents.bound = true;
77
- },
78
-
79
- toggleDetails: function( e ) {
80
- e.preventDefault();
81
-
82
- var $details = jQuery(this).parent().find( '.itsec-details-toggle-details' ).toggleClass( 'hide-if-js' );
83
-
84
- if ( $details.hasClass( 'hide-if-js' ) ) {
85
- jQuery(this).html( itsec_page.translations.show_information );
86
- } else {
87
- jQuery(this).html( itsec_page.translations.hide_description );
88
- }
89
- },
90
-
91
- logPageChangeFilter: function( e ) {
92
- var filter = jQuery( this ).val();
93
- var url = itsec_page.logs_page_url + '&filter=' + filter;
94
- window.location.href = url;
95
- },
96
-
97
- toggleModuleContent: function( e ) {
98
- if ( 'checkbox' === jQuery(this).attr( 'type' ) ) {
99
- var show = jQuery(this).prop( 'checked' );
100
- } else {
101
- var show = ( jQuery(this).val() ) ? true : false;
102
- }
103
-
104
- var $content = jQuery( '.' + jQuery(this).attr( 'id' ) + '-content' );
105
-
106
- if ( show ) {
107
- $content.show();
108
-
109
-
110
- var $container = jQuery( '.itsec-module-cards-container' );
111
-
112
- if ( $container.hasClass( 'grid' ) ) {
113
- var $modal = jQuery(this).parents( '.itsec-module-settings-content-container' );
114
- var scrollOffset = $modal.scrollTop() + jQuery(this).parent().position().top;
115
-
116
- $modal.animate( {'scrollTop': scrollOffset}, 'slow' );
117
- }
118
- } else {
119
- $content.hide();
120
- }
121
- },
122
-
123
- handleCopy: function( e ) {
124
-
125
- e.preventDefault();
126
-
127
- var $trigger = jQuery( e.currentTarget );
128
- var fromId = $trigger.data( 'copy-from' );
129
-
130
- if ( ! fromId.length ) {
131
- return;
132
- }
133
-
134
- var el = document.getElementById( fromId );
135
-
136
- var removeSelect = itsecSettingsPage.selectText( el );
137
-
138
- try {
139
-
140
- document.execCommand( 'copy' );
141
- removeSelect();
142
- $trigger.text( itsec_page.translations.copied );
143
-
144
- } catch ( e ) {
145
- var $p = jQuery( '<p></p>' ).text( itsec_page.translations.copy_instruction ),
146
- $notice = jQuery( '<div class="notice notice-alt notice-info"></div>' ).append( $p ),
147
- $el = jQuery( el );
148
-
149
- $trigger.after( $notice );
150
-
151
- var removeNotice = function () {
152
- $notice.fadeOut( function () {
153
- $notice.remove();
154
- } );
155
- };
156
- var copy = function () {
157
-
158
- setTimeout( function () {
159
- removeNotice();
160
- removeSelect();
161
- }, 100 );
162
-
163
- $el.off( 'copy', copy );
164
-
165
- return true;
166
- };
167
-
168
- $el.on( 'copy', copy );
169
-
170
- setTimeout( removeNotice, 5000 );
171
- }
172
- },
173
-
174
- // https://stackoverflow.com/a/987376
175
- selectText: function( element ) {
176
- var doc = document, text = element, range, selection;
177
-
178
- if ( doc.body.createTextRange ) { // ie
179
- range = document.body.createTextRange();
180
- range.moveToElementText( text );
181
- range.select();
182
- } else if ( window.getSelection ) {
183
- selection = window.getSelection();
184
- range = document.createRange();
185
- range.selectNodeContents( text );
186
- selection.removeAllRanges();
187
- selection.addRange( range );
188
- }
189
-
190
- return function() {
191
- if ( selection ) {
192
- selection.removeAllRanges();
193
- } else {
194
- range.collapse();
195
- }
196
- };
197
- },
198
-
199
- saveSettings: function( e ) {
200
- e.preventDefault();
201
-
202
- var $button = jQuery(this);
203
-
204
- if ( $button.hasClass( 'itsec-module-settings-save' ) ) {
205
- var module = $button.parents( '.itsec-module-card' ).attr( 'id' ).replace( 'itsec-module-card-', '' );
206
- } else {
207
- var module = '';
208
- }
209
-
210
- $button.prop( 'disabled', true );
211
-
212
- var data = {
213
- '--itsec-form-serialized-data': jQuery( '#itsec-module-settings-form' ).serialize()
214
- };
215
-
216
- itsecUtil.sendAJAXRequest( module, 'save', data, itsecSettingsPage.saveSettingsCallback );
217
- },
218
-
219
- scrollTop: function() {
220
- var $container = jQuery( '.itsec-module-cards-container' );
221
- var view = $container.hasClass( 'grid' ) ? 'grid' : 'list';
222
-
223
- if ( 'grid' === view ) {
224
- $container.find( '.itsec-module-settings-content-container:visible' ).animate( { 'scrollTop': 0 }, 'fast' );
225
- }
226
-
227
- if ( 'list' === view ) {
228
- jQuery( document ).scrollTop( 0 );
229
- }
230
- },
231
-
232
- saveSettingsCallback: function( results ) {
233
- if ( '' === results.module ) {
234
- jQuery( '#itsec-save' ).prop( 'disabled', false );
235
- } else {
236
- jQuery( '#itsec-module-card-' + results.module + ' button.itsec-module-settings-save' ).prop( 'disabled', false );
237
- }
238
-
239
- var $container = jQuery( '.itsec-module-cards-container' );
240
-
241
- if ( $container.hasClass( 'grid' ) ) {
242
- var view = 'grid';
243
- } else {
244
- var view = 'list';
245
- }
246
-
247
- itsecSettingsPage.clearMessages();
248
-
249
- if ( results.errors.length > 0 || results.warnings.length > 0 || ! results.closeModal ) {
250
- itsecSettingsPage.showErrors( results.errors, results.module, 'open' );
251
- itsecSettingsPage.showErrors( results.warnings, results.module, 'open', 'warning' );
252
- itsecSettingsPage.showMessages( results.messages, results.module, 'open' );
253
- itsecSettingsPage.showMessages( results.infos, results.module, 'open', 'info' );
254
-
255
- if ( 'grid' === view ) {
256
- $container.find( '.itsec-module-settings-content-container:visible' ).animate( {'scrollTop': 0}, 'fast' );
257
- }
258
-
259
- if ( 'list' === view ) {
260
- jQuery(document).scrollTop( 0 );
261
- }
262
- } else {
263
- itsecSettingsPage.showMessages( results.messages, results.module, 'closed' );
264
- itsecSettingsPage.showMessages( results.infos, results.module, 'closed', 'info' );
265
-
266
- if ( 'grid' === view ) {
267
- $container.find( '.itsec-module-settings-content-container:visible' ).scrollTop( 0 );
268
- itsecSettingsPage.closeGridSettingsModal();
269
- }
270
- }
271
- },
272
-
273
- clearMessages: function() {
274
- jQuery( '#itsec-settings-messages-container, .itsec-module-messages-container' ).empty();
275
- },
276
-
277
- showErrors: function( errors, module, containerStatus, type ) {
278
- jQuery.each( errors, function( index, error ) {
279
- itsecSettingsPage.showError( error, module, containerStatus, type );
280
- } );
281
- },
282
-
283
- showError: function( error, module, containerStatus, type ) {
284
-
285
- type = type || 'error';
286
-
287
- if ( jQuery( '.itsec-module-cards-container' ).hasClass( 'grid' ) ) {
288
- var view = 'grid';
289
- } else {
290
- var view = 'list';
291
- }
292
-
293
- if ( 'closed' !== containerStatus && 'open' !== containerStatus ) {
294
- containerStatus = 'closed';
295
- }
296
-
297
- if ( 'string' !== typeof module ) {
298
- module = '';
299
- }
300
-
301
-
302
- if ( 'closed' === containerStatus || '' === module ) {
303
- var container = jQuery( '#itsec-settings-messages-container' );
304
-
305
- if ( '' === module ) {
306
- container.addClass( 'no-module' );
307
- }
308
- } else {
309
- var container = jQuery( '#itsec-module-card-' + module + ' .itsec-module-messages-container' );
310
- }
311
-
312
- var $notice = jQuery( '<div class="notice"><p><strong>' + error + '</strong></p></div>' );
313
- $notice.addClass( 'notice-' + type );
314
-
315
- if ( containerStatus === 'open' || module.length ) {
316
- $notice.addClass( 'notice-alt' );
317
- }
318
-
319
- container.append( $notice ).addClass( 'visible' );
320
- },
321
-
322
- showMessages: function( messages, module, containerStatus, type ) {
323
- jQuery.each( messages, function( index, message ) {
324
- itsecSettingsPage.showMessage( message, module, containerStatus, type );
325
- } );
326
- },
327
-
328
- showMessage: function( message, module, containerStatus, type ) {
329
-
330
- type = type || 'success';
331
-
332
- if ( jQuery( '.itsec-module-cards-container' ).hasClass( 'grid' ) ) {
333
- var view = 'grid';
334
- } else {
335
- var view = 'list';
336
- }
337
-
338
- if ( 'closed' !== containerStatus && 'open' !== containerStatus ) {
339
- containerStatus = 'closed';
340
- }
341
-
342
- if ( 'string' !== typeof module ) {
343
- module = '';
344
- }
345
-
346
-
347
- if ( 'closed' === containerStatus || '' === module ) {
348
- var container = jQuery( '#itsec-settings-messages-container' );
349
-
350
- var dismiss = function () {
351
-
352
- if ( container.is( ':hover' ) ) {
353
- return setTimeout( dismiss, 2000 );
354
- }
355
-
356
- container.removeClass( 'visible' );
357
- setTimeout( function () {
358
- container.find( 'div' ).remove();
359
- }, 500 );
360
- };
361
-
362
- setTimeout( dismiss, 4000 );
363
- } else {
364
- var container = jQuery( '#itsec-module-card-' + module + ' .itsec-module-messages-container' );
365
- }
366
-
367
- var $notice = jQuery( '<div class="notice fade"><p><strong>' + message + '</strong></p></div>' );
368
- $notice.addClass( 'notice-' + type );
369
-
370
- if ( containerStatus === 'open' || module.length ) {
371
- $notice.addClass( 'notice-alt' );
372
- }
373
-
374
- container.append( $notice ).addClass( 'visible' );
375
- },
376
-
377
- filterView: function( e ) {
378
- e.preventDefault();
379
-
380
- var $activeLink = jQuery(this),
381
- $oldLink = $activeLink.parents( '.itsec-feature-tabs' ).find( '.current' ),
382
- type = $activeLink.parent().attr( 'id' ).substr( 20 );
383
-
384
- $oldLink.removeClass( 'current' );
385
- $activeLink.addClass( 'current' );
386
-
387
- if ( 'all' === type ) {
388
- jQuery( '.itsec-module-card' ).show();
389
- } else {
390
- jQuery( '.itsec-module-type-' + type ).show();
391
- jQuery( '.itsec-module-card' ).not( '.itsec-module-type-' + type ).hide();
392
- }
393
-
394
- // We use this to avoid pushing a new state when we're trying to handle a popstate
395
- if ( 'itsec-popstate' !== e.type ) {
396
- var url = '?page=itsec&module_type=' + type;
397
- var module = itsecUtil.getUrlParameter( 'module' );
398
- if ( 'string' === typeof module ) {
399
- url += '&module=' + module;
400
- }
401
-
402
- window.history.pushState( {'module_type':type}, type, url );
403
- }
404
- },
405
-
406
- toggleView: function( e ) {
407
- e.preventDefault();
408
-
409
- var $self = jQuery(this);
410
-
411
- if ( $self.hasClass( 'itsec-selected' ) ) {
412
- // Do nothing if already selected.
413
- return;
414
- }
415
-
416
- var $view = $self.attr( 'class' ).replace( 'itsec-', '' );
417
-
418
- $self.addClass( 'itsec-selected' ).siblings().removeClass( 'itsec-selected' );
419
- jQuery( '.itsec-module-settings-container' ).hide();
420
-
421
- jQuery( '.itsec-toggle-settings' ).each(function( index ) {
422
- var $button = jQuery( this );
423
-
424
- if ( $button.parents( '.itsec-module-card' ).hasClass( 'itsec-module-type-enabled' ) && ! $button.hasClass( 'information-only' ) ) {
425
- $button.html( itsec_page.translations.show_settings );
426
- } else if ( $button.hasClass( 'information-only' ) ) {
427
- $button.html( itsec_page.translations.information_only );
428
- } else {
429
- $button.html( itsec_page.translations.show_description );
430
- }
431
- });
432
-
433
- var $cardContainer = jQuery( '.itsec-module-cards-container' );
434
- jQuery.post( ajaxurl, {
435
- 'action': 'itsec-set-user-setting',
436
- 'itsec-user-setting-nonce': $self.parent().data( 'nonce' ),
437
- 'setting': 'itsec-settings-view',
438
- 'value': $view
439
- } );
440
-
441
- $cardContainer.fadeOut( 100, function() {
442
- $cardContainer.removeClass( 'grid list' ).addClass( $view );
443
- } );
444
- $cardContainer.fadeIn( 100 );
445
- },
446
-
447
- openModuleFromLink: function( e ) {
448
-
449
- var $link = jQuery( this ), module = $link.data( 'module-link' ),
450
- $module = jQuery( '.itsec-module-card[data-module-id="' + module + '"]' ),
451
- highlight = $link.data( 'highlight-setting-id' );
452
-
453
- if ( ! $module.length ) {
454
- return; // safety check
455
- }
456
-
457
- e.preventDefault();
458
-
459
- jQuery( '.itsec-module-settings-container:visible' ).hide();
460
-
461
- var $listClassElement = $module.parents( '.itsec-module-cards-container' ),
462
- $toggleButton = $module.find( '.itsec-toggle-settings' );
463
-
464
- if ( highlight && highlight.length ) {
465
- jQuery( 'label[for="' + highlight + '"]', $module ).parents( 'tr' ).addClass( 'itsec-highlighted-setting' );
466
- }
467
-
468
- if ( $listClassElement.hasClass( 'list' ) ) {
469
- itsecSettingsPage.toggleListSettingsCard.call( $toggleButton, e );
470
- } else if ( $listClassElement.hasClass( 'grid' ) ) {
471
- itsecSettingsPage.showGridSettingsModal.call( $toggleButton, e );
472
- }
473
-
474
- var type = $module.hasClass( 'itsec-module-type-advanced' ) ? 'advanced' : 'recommended';
475
-
476
- window.history.pushState( {module: module}, module, '?page=itsec&module=' + module + '&module_type=' + type );
477
-
478
- var href = $link.attr( 'href' );
479
-
480
- if ( href && href.length > 1 && href.charAt( 0 ) === '#' ) {
481
- setTimeout( function () {
482
- jQuery( '.itsec-module-settings-content-container', '#itsec-module-card-notification-center' ).scrollTo( jQuery( href ), 'swing', { offset: -30 } );
483
- }, 350 );
484
- }
485
- },
486
-
487
- toggleSettings: function( e ) {
488
- e.stopPropagation();
489
-
490
- var $listClassElement = jQuery(e.currentTarget).parents( '.itsec-module-cards-container' );
491
-
492
- if ( $listClassElement.hasClass( 'list') ) {
493
- itsecSettingsPage.toggleListSettingsCard.call( this, e );
494
- } else if ( $listClassElement.hasClass( 'grid' ) ) {
495
- itsecSettingsPage.showGridSettingsModal.call( this, e );
496
- }
497
-
498
- // We use this to avoid pushing a new state when we're trying to handle a popstate
499
- if ( 'itsec-popstate' !== e.type ) {
500
- var module_id = jQuery(this).closest('.itsec-module-card').data( 'module-id' );
501
-
502
- var module_type = itsecUtil.getUrlParameter( 'module_type' );
503
- if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
504
- module_type = 'recommended';
505
- }
506
- window.history.pushState( {'module':module_id}, module_id, '?page=itsec&module=' + module_id + '&module_type=' + module_type );
507
- }
508
- },
509
-
510
- toggleListSettingsCard: function( e ) {
511
- e.preventDefault();
512
-
513
- var $container = jQuery(this);
514
-
515
- if ( ! $container.hasClass( 'itsec-module-card-content' ) ) {
516
- $container = $container.parents( '.itsec-module-card' ).find( '.itsec-module-card-content' );
517
- }
518
-
519
- var $settings = $container.siblings( '.itsec-module-settings-container' ),
520
- isVisible = $settings.is( ':visible' );
521
- $settings.stop().slideToggle( 300 );
522
-
523
- if ( ! isVisible ) {
524
- var $highlighted = jQuery( '.itsec-highlighted-setting', $settings );
525
-
526
- if ( $highlighted.length ) {
527
- setTimeout( function () {
528
- jQuery.scrollTo( $highlighted.first(), 'swing', {
529
- offset: { top: -30 },
530
- onAfter: function() {
531
- var $el = jQuery( 'input[type!="button"], textarea, select', $highlighted ).not( ':hidden' ).first();
532
- itsecUtil.focus( $el, $highlighted );
533
- }
534
- } );
535
- }, 50 );
536
- } else {
537
- var $el = jQuery( 'input[type!="button"], textarea, select', $settings ).not( ':hidden' ).first();
538
- itsecUtil.focus( $el, $settings );
539
- }
540
- }
541
-
542
- var $button = $container.find( '.itsec-toggle-settings' );
543
-
544
- if ( $container.parent().hasClass( 'itsec-module-type-enabled' ) ) {
545
- if ( $button.html() == itsec_page.translations.show_settings ) {
546
- $button.html( itsec_page.translations.hide_settings );
547
- } else {
548
- $button.html( itsec_page.translations.show_settings );
549
- }
550
- } else {
551
- if ( $button.hasClass( 'information-only' ) ) {
552
- if ( $button.html() == itsec_page.translations.show_information ) {
553
- $button.html( itsec_page.translations.hide_description );
554
- } else {
555
- $button.html( itsec_page.translations.show_information );
556
- }
557
- } else {
558
- if ( $button.html() == itsec_page.translations.show_description ) {
559
- $button.html( itsec_page.translations.hide_description );
560
- } else {
561
- $button.html( itsec_page.translations.show_description );
562
- }
563
- }
564
- }
565
- },
566
-
567
- showGridSettingsModal: function( e ) {
568
- e.preventDefault();
569
-
570
- var $module = jQuery(this).parents( '.itsec-module-card' ),
571
- $settingsContainer = $module.find( '.itsec-module-settings-container' ),
572
- $modalBackground = jQuery( '.itsec-modal-background' );
573
-
574
- $module.show();
575
-
576
- $modalBackground.fadeIn();
577
- $settingsContainer.fadeIn( 200 );
578
-
579
- jQuery( 'body' ).addClass( 'itsec-modal-open' );
580
-
581
- var $highlighted = jQuery( '.itsec-highlighted-setting', $module ).first();
582
-
583
- if ( $highlighted.length ) {
584
- jQuery( '.itsec-module-settings-content-container', $module ).scrollTo( $highlighted, 'swing', {
585
- offset: { top: -20 },
586
- onAfter: function() {
587
- var $el = jQuery( 'input[type!="button"], textarea, select', $highlighted ).not( ':hidden' ).first();
588
- itsecUtil.focus( $el, $highlighted );
589
- }
590
- } );
591
- } else {
592
- var $el = jQuery( 'input[type!="button"], textarea, select', $settingsContainer ).not( ':hidden' ).first();
593
- itsecUtil.focus( $el, $settingsContainer );
594
- }
595
- },
596
-
597
- closeGridSettingsModal: function( e ) {
598
- if ( 'undefined' !== typeof e ) {
599
- e.preventDefault();
600
-
601
- // For keyup events, only process esc
602
- if ( 'keyup' === e.type && 27 !== e.which ) {
603
- return;
604
- }
605
- }
606
-
607
- jQuery( '.itsec-modal-background' ).fadeOut();
608
- jQuery( '.itsec-module-settings-container' ).fadeOut( 200 );
609
- jQuery( 'body' ).removeClass( 'itsec-modal-open' );
610
-
611
- if ( 'undefined' === typeof e || 'popstate' !== e.type ) {
612
- var module_type = itsecUtil.getUrlParameter( 'module_type' );
613
- if ( false === module_type || 0 === jQuery( '#itsec-module-filter-' + module_type.replace( /[^\w-]/g, '' ) ).length ) {
614
- module_type = 'recommended';
615
- }
616
- window.history.pushState( {'module':'', 'module_type':module_type}, module_type, '?page=itsec&module_type=' + module_type );
617
- }
618
-
619
- if ( jQuery( '#search' ).val().length ) {
620
- jQuery( '#search' ).focus();
621
- }
622
- },
623
-
624
- toggleModuleActivation: function( e ) {
625
- e.preventDefault();
626
- e.stopPropagation();
627
-
628
- var $button = jQuery(this),
629
- $card = $button.parents( '.itsec-module-card' ),
630
- $buttons = $card.find( '.itsec-toggle-activation' ),
631
- module = $card.attr( 'id' ).replace( 'itsec-module-card-', '' );
632
-
633
- $buttons.prop( 'disabled', true );
634
-
635
- if ( $button.html() == itsec_page.translations.activate ) {
636
- var method = 'activate';
637
- } else {
638
- var method = 'deactivate';
639
- }
640
-
641
- itsecUtil.sendAJAXRequest( module, method, {}, itsecSettingsPage.toggleModuleActivationCallback );
642
- },
643
-
644
- setModuleToActive: function( module ) {
645
- var args = {
646
- 'module': module,
647
- 'method': 'activate',
648
- 'errors': []
649
- };
650
-
651
- itsecSettingsPage.toggleModuleActivationCallback( args );
652
- },
653
-
654
- setModuleToInactive: function( module ) {
655
- var args = {
656
- 'module': module,
657
- 'method': 'deactivate',
658
- 'errors': []
659
- };
660
-
661
- itsecSettingsPage.toggleModuleActivationCallback( args );
662
- },
663
-
664
- toggleModuleActivationCallback: function( results ) {
665
- var module = results.module;
666
- var method = results.method;
667
-
668
- var $card = jQuery( '#itsec-module-card-' + module ),
669
- $buttons = $card.find( '.itsec-toggle-activation' )
670
-
671
- if ( results.errors.length > 0 ) {
672
- $buttons
673
- .html( itsec_page.translations.error )
674
- .addClass( 'button-secondary' )
675
- .removeClass( 'button-primary' );
676
-
677
- setTimeout( function() {
678
- itsecSettingsPage.isModuleActive( module );
679
- }, 1000 );
680
-
681
- itsecSettingsPage.showErrors( results.errors, results.module, 'closed', 'error' );
682
-
683
- return;
684
- }
685
-
686
- if ( 'activate' === method ) {
687
- $buttons
688
- .html( itsec_page.translations.deactivate )
689
- .addClass( 'button-secondary' )
690
- .removeClass( 'button-primary' )
691
- .prop( 'disabled', false );
692
-
693
- $card
694
- .addClass( 'itsec-module-type-enabled' )
695
- .removeClass( 'itsec-module-type-disabled' );
696
-
697
- var newToggleSettingsLabel = itsec_page.translations.show_settings;
698
- } else {
699
- $buttons
700
- .html( itsec_page.translations.activate )
701
- .addClass( 'button-primary' )
702
- .removeClass( 'button-secondary' )
703
- .prop( 'disabled', false );
704
-
705
- $card
706
- .addClass( 'itsec-module-type-disabled' )
707
- .removeClass( 'itsec-module-type-enabled' );
708
-
709
- var newToggleSettingsLabel = itsec_page.translations.show_description;
710
- }
711
-
712
- $card.find( '.itsec-toggle-settings' ).html( newToggleSettingsLabel );
713
-
714
- var enabledCount = jQuery( '.itsec-module-type-enabled' ).length,
715
- disabledCount = jQuery( '.itsec-module-type-disabled' ).length;
716
-
717
- jQuery( '#itsec-module-filter-enabled .count' ).html( '(' + enabledCount + ')' );
718
- jQuery( '#itsec-module-filter-disabled .count' ).html( '(' + disabledCount + ')' );
719
-
720
-
721
- itsecSettingsPage.showErrors( results.warnings, results.module, 'closed', 'warning' );
722
- itsecSettingsPage.showMessages( results.messages, results.module, 'closed' );
723
- itsecSettingsPage.showMessages( results.infos, results.module, 'closed', 'info' );
724
- },
725
-
726
- isModuleActive: function( module ) {
727
- var data = {
728
- 'module': module,
729
- 'method': 'is_active'
730
- };
731
-
732
- itsecUtil.sendAJAXRequest( module, 'is_active', {}, itsecSettingsPage.isModuleActiveCallback );
733
- },
734
-
735
- isModuleActiveCallback: function( results ) {
736
- if ( true === results.response ) {
737
- results.method = 'activate';
738
- } else if ( false === results.response ) {
739
- results.method = 'deactivate';
740
- } else {
741
- return;
742
- }
743
-
744
- itsecSettingsPage.toggleModuleActivationCallback( results );
745
- },
746
-
747
- reloadModule: function( module ) {
748
- if ( module.preventDefault ) {
749
- module.preventDefault();
750
-
751
- module = jQuery(this).parents( '.itsec-module-card' ).attr( 'id' ).replace( 'itsec-module-card-', '' );
752
- }
753
-
754
- var method = 'get_refreshed_module_settings';
755
- var data = {};
756
-
757
- itsecUtil.sendAJAXRequest( module, method, data, function( results ) {
758
- if ( results.success && results.response ) {
759
- var $card = jQuery( '#itsec-module-card-' + module );
760
- var isHidden = $card.is( ':hidden' );
761
-
762
- jQuery( '.itsec-module-settings-content-main', $card ).html( results.response );
763
-
764
- if ( isHidden ) {
765
- $card.hide();
766
- } else {
767
- jQuery( '.itsec-settings-toggle' ).trigger( 'change' );
768
- }
769
- } else if ( results.errors && results.errors.length > 0 ) {
770
- itsecSettingsPage.showErrors( results.errors, results.module, 'open' );
771
- } else if ( results.warnings && results.warnings.length > 0 ) {
772
- itsecSettingsPage.showErrors( results.warnings, results.module, 'open', 'warning' );
773
- }
774
-
775
- itsecSettingsPage.events.trigger( 'moduleReloaded', module );
776
-
777
- itsecSettingsPage.makeNoticesDismissible();
778
- } );
779
- },
780
-
781
- reloadAllModules: function( _, initialResponse) {
782
- itsecUtil.sendAJAXRequest( '#', 'get_refreshed_module_form', null, function ( response ) {
783
-
784
- if ( ! response.success || response.errors.length ) {
785
- return;
786
- }
787
-
788
- var $open;
789
-
790
- if ( jQuery( 'body' ).hasClass( 'itsec-modal-open' ) ) {
791
- var $newModules = jQuery( response.response ), $cardsList = jQuery( '.itsec-module-cards' );
792
- $open = jQuery( '.itsec-module-settings-container:visible' ).parents( '.itsec-module-card' );
793
-
794
- jQuery( '.itsec-module-card', $newModules ).each( function () {
795
- var $new = jQuery( this ), $current = jQuery( '#' + $new.attr( 'id' ), $cardsList );
796
-
797
- if ( $new.attr( 'id' ).length && $new.attr( 'id' ) === $open.attr( 'id' ) ) {
798
- jQuery( '.itsec-module-settings-content-main', $current ).html( jQuery( '.itsec-module-settings-content-main', $new ).html() );
799
- } else {
800
- jQuery( '.itsec-module-settings-container', $new ).hide();
801
- $current.replaceWith( $new );
802
- }
803
- } );
804
-
805
- } else {
806
- jQuery( '.itsec-module-cards-container' ).html( response.response );
807
- }
808
-
809
- itsecSettingsPage.initFilters();
810
-
811
- if ( ! $open ) {
812
- jQuery( '.itsec-module-settings-container' ).hide();
813
- }
814
-
815
- if ( initialResponse ) {
816
- itsecSettingsPage.showMessages( initialResponse.messages, initialResponse.module, $open ? 'open' : 'closed' );
817
- itsecSettingsPage.showMessages( initialResponse.infos, initialResponse.module, $open ? 'open' : 'closed', 'info' );
818
- itsecSettingsPage.showErrors( initialResponse.errors, initialResponse.module, $open ? 'open' : 'closed' );
819
- itsecSettingsPage.showErrors( initialResponse.warnings, initialResponse.module, $open ? 'open' : 'closed', 'warning' );
820
- }
821
-
822
- itsecSettingsPage.makeNoticesDismissible();
823
- itsecSettingsPage.events.trigger( 'modulesReloaded', initialResponse );
824
- } );
825
- },
826
-
827
- reloadWidget: function( widget ) {
828
- var method = 'get_refreshed_widget_settings';
829
- var data = {};
830
-
831
- itsecUtil.sendAJAXRequest( module, method, data, function( results ) {
832
- if ( results.success && results.response ) {
833
- jQuery( '#itsec-sidebar-widget-' + module + ' .inside' ).html( results.response );
834
- } else {
835
- itsecSettingsPage.showErrors( results.errors, results.module, 'closed' );
836
- itsecSettingsPage.showErrors( results.warnings, results.module, 'closed', 'warning' );
837
- }
838
- } );
839
- },
840
-
841
- // Make notices dismissible
842
- makeNoticesDismissible: function() {
843
- jQuery( '.notice.itsec-is-dismissible' ).each( function() {
844
- var $el = jQuery( this ),
845
- $button = jQuery( '<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>' ),
846
- btnText = itsec_page.translations.dismiss || '';
847
-
848
- // Don't rebind twice
849
- if ( jQuery( '.notice-dismiss', $el ).length ) {
850
- return;
851
- }
852
-
853
- // Ensure plain text
854
- $button.find( '.screen-reader-text' ).text( btnText );
855
- $button.on( 'click.wp-dismiss-notice', function( event ) {
856
- event.preventDefault();
857
-
858
- $el.trigger( 'itsec-dismiss-notice' );
859
-
860
- $el.fadeTo( 100, 0, function() {
861
- $el.slideUp( 100, function() {
862
- $el.remove();
863
- });
864
- });
865
- });
866
-
867
- $el.append( $button );
868
- });
869
- },
870
-
871
- refreshPage: function() {
872
- location.reload( true );
873
- }
874
- };
875
-
876
- jQuery(document).ready(function( $ ) {
877
- itsecSettingsPage.init();
878
-
879
- if ( itsec_page.show_security_check ) {
880
- jQuery( '.itsec-settings-view-toggle a.itsec-grid' ).trigger( 'click' );
881
- jQuery( '#itsec-module-card-security-check .itsec-toggle-settings' ).trigger( 'click' );
882
- }
883
-
884
-
885
- jQuery( '.dialog' ).click( function ( event ) {
886
- event.preventDefault();
887
-
888
- var target = jQuery( this ).attr( 'href' );
889
- var title = jQuery( this ).parents( '.inside' ).siblings( 'h3.hndle' ).children( 'span' ).text();
890
-
891
- jQuery( '#' + target ).dialog( {
892
- dialogClass : 'wp-dialog itsec-dialog itsec-dialog-logs',
893
- modal : true,
894
- closeOnEscape: true,
895
- title : title,
896
- height : ( jQuery( window ).height() * 0.8 ),
897
- width : ( jQuery( window ).width() * 0.8 ),
898
- open : function ( event, ui ) {
899
- jQuery( '.ui-widget-overlay' ).bind( 'click', function () {
900
- jQuery( this ).siblings( '.ui-dialog' ).find( '.ui-dialog-content' ).dialog( 'close' );
901
- } );
902
- }
903
- } );
904
-
905
- jQuery( '.ui-dialog :button' ).blur();
906
- } );
907
-
908
- var regex = /[^\w]/ig;
909
-
910
- var $search = $( '#search' ), $cardsContainer = $( '.itsec-module-cards' ),
911
- $cards = $( '.itsec-module-card', $cardsContainer ),
912
- $searchFilter = $( '#itsec-module-filter-search' ),
913
- $currentFilter = $( '.itsec-feature-tabs .current' ).parent();
914
-
915
- itsecSettingsPage.events.on( 'modulesReloaded', function() {
916
- $cardsContainer = $( '.itsec-module-cards' );
917
- $cards = $( '.itsec-module-card', $cardsContainer );
918
- } );
919
-
920
- $search.on( 'input', _.debounce( function () {
921
- var query = $search.val().trim().replace( regex, ' ' );
922
-
923
- var $maybeCurrent = $( '.itsec-feature-tabs .current' ).parent();
924
-
925
- if ( $maybeCurrent && $maybeCurrent.prop( 'id' ) !== 'itsec-module-filter-search' ) {
926
- $currentFilter = $maybeCurrent;
927
- }
928
-
929
- $( '.itsec-highlighted-setting', $cards ).removeClass( 'itsec-highlighted-setting' );
930
-
931
- if ( !query.length ) {
932
- $searchFilter.addClass( 'hide-if-js' );
933
- $( 'a', $searchFilter ).removeClass( 'current' );
934
- $( 'a', $currentFilter ).addClass( 'current' );
935
-
936
- var type = $currentFilter.prop( 'id' ).substr( 20 );
937
-
938
- if ( 'all' === type ) {
939
- $cards.show();
940
- } else {
941
- $( '.itsec-module-type-' + type ).show();
942
- $( '.itsec-module-card' ).not( '.itsec-module-type-' + type ).hide();
943
- }
944
-
945
- return;
946
- }
947
-
948
- var $titleMatches = $( ".itsec-module-card-content > h2:itsecContains('" + query + "')", $cards ),
949
- $titleMatchesCards = $titleMatches.parents( '.itsec-module-card' );
950
-
951
- var $descriptionMatches = $( ".itsec-module-card-content > p:itsecContains('" + query + "')", $cards ),
952
- $descriptionMatchesCards = $descriptionMatches.parents( '.itsec-module-card' );
953
-
954
- var $settingMatches = $( ".itsec-module-settings-container .form-table tr > th > label:itsecContains('" + query + "')", $cards ),
955
- $settingMatchesCards = $settingMatches.parents( '.itsec-module-card' );
956
-
957
-
958
- var $matches = $titleMatchesCards.add( $descriptionMatchesCards ).add( $settingMatchesCards );
959
-
960
- $searchFilter.removeClass( 'hide-if-js' );
961
- $( 'a', $currentFilter ).removeClass( 'current' );
962
- $( 'a', $searchFilter ).addClass( 'current' );
963
- $( '.count', $searchFilter ).text( '(' + $matches.length + ')' );
964
-
965
- $cards.hide();
966
- $matches.show();
967
-
968
- $settingMatches.parents( 'tr' ).addClass( 'itsec-highlighted-setting' );
969
-
970
- if ( $matches.length === 1 ) {
971
- $( '.itsec-toggle-settings', $matches.first() ).click();
972
- }
973
- }, 250 ) );
974
-
975
- $.expr[":"].itsecContains = $.expr.createPseudo( function ( arg ) {
976
- return function ( elem ) {
977
- var candidate = $( elem ).text().toUpperCase().replace( regex, ' ' ), term = arg.toUpperCase();
978
- var index = candidate.indexOf( term );
979
-
980
- if ( index === -1 ) {
981
- return false;
982
- }
983
-
984
- if ( index === 0 ) {
985
- return true;
986
- }
987
-
988
- var prior = candidate.charAt( index - 1 ), next = candidate.charAt( term.length + index );
989
-
990
- // full word
991
- return prior === ' ' && ( next === ' ' || next === '' );
992
- };
993
- } );
994
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/module-settings.php DELETED
@@ -1,339 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * The iThemes Security Module Settings Page API parent class.
5
- *
6
- * @property-read string $id
7
- * @property-read string $title
8
- * @property-read string $description
9
- * @property-read string $type
10
- * @property-read string $pro
11
- * @property-read bool $can_save
12
- * @property-read bool $redraw_on_save
13
- * @property-read bool $upsell
14
- * @property-read string $upsell_url
15
- * @property-read bool $information_only
16
- * @property-read string $status
17
- * @property-read string $documentation
18
- */
19
- class ITSEC_Module_Settings_Page {
20
- /**
21
- * Unique ID for the module.
22
- *
23
- * This is used to store the module's data and generate form inputs.
24
- *
25
- * @access protected
26
- * @var string
27
- */
28
- protected $id = '';
29
-
30
- /**
31
- * User-friendly display title for the module.
32
- *
33
- * @access protected
34
- * @var string
35
- */
36
- protected $title = '';
37
-
38
- /**
39
- * User-friendly display description for the module.
40
- *
41
- * @access protected
42
- * @var string
43
- */
44
- protected $description = '';
45
-
46
- /**
47
- * Whether the module is categorized as additional or recommended.
48
- *
49
- * @access protected
50
- * @var string
51
- */
52
- protected $type = 'recommended'; // "advanced" or "recommended"
53
-
54
- /**
55
- * Whether the settings require resaving after activation in order to fully-activate the module.
56
- *
57
- * @access protected
58
- * @var boolean
59
- */
60
- protected $requires_resave_after_activation = false;
61
-
62
- /**
63
- * Whether the module is part of iThemes Security Pro or not.
64
- *
65
- * @access protected
66
- * @var bool
67
- */
68
- protected $pro = false;
69
-
70
- /**
71
- * Whether the module settings can be saved.
72
- *
73
- * @access protected
74
- * @var bool
75
- */
76
- protected $can_save = true;
77
-
78
- /**
79
- * Whether the module settings should be redrawn on save.
80
- *
81
- * @access protected
82
- * @var bool
83
- */
84
- protected $redraw_on_save = false;
85
-
86
- /**
87
- * Whether the module is an iThemes Security Pro module being shown as an upsell or not.
88
- *
89
- * @access protected
90
- * @var bool
91
- */
92
- protected $upsell = false;
93
-
94
- /**
95
- * URL to use for upsell if this is one.
96
- *
97
- * @access protected
98
- * @var string
99
- */
100
- protected $upsell_url = 'https://ithemes.com/security/';
101
-
102
- /**
103
- * Whether the module is for informational purposes only - no settings, no actions
104
- *
105
- * @access protected
106
- * @var bool
107
- */
108
- protected $information_only = false;
109
-
110
- /**
111
- * Set the module status to 'warning' to signal to the user it needs attention.
112
- *
113
- * @var string
114
- */
115
- protected $status = '';
116
-
117
- /**
118
- * Link to documentation for this module.
119
- *
120
- * @var string
121
- */
122
- protected $documentation = '';
123
-
124
- /**
125
- * Constructor.
126
- *
127
- * Register the module settings to register themselves on init. Each subclass should use the constructor to set the
128
- * id, title, description, type, pro, can_save, and redraw_on_save properties to values specific to that module and then call
129
- * parent::__construct().
130
- *
131
- * @access public
132
- */
133
- public function __construct() {
134
- add_action( 'itsec-settings-page-register-modules', array( $this, 'register' ) );
135
- }
136
-
137
- /**
138
- * Make protected properties public read-only.
139
- *
140
- * This function should be left as-is in subclasses.
141
- *
142
- * @access public
143
- *
144
- * @param string $name Property to get.
145
- *
146
- * @return mixed Property.
147
- */
148
- public function __get( $name ) {
149
- if ( in_array( $name, array( 'id', 'title', 'description', 'type', 'pro', 'can_save', 'redraw_on_save', 'upsell', 'upsell_url', 'information_only', 'status', 'documentation' ) ) ) {
150
- return $this->$name;
151
- }
152
-
153
- trigger_error( 'Attempted to check invalid property: ' . get_class( $this ) . "->$name", E_USER_ERROR );
154
- }
155
-
156
- /**
157
- * Register the module's settings with the settings page.
158
- *
159
- * This function should be left as-is in subclasses.
160
- *
161
- * @access public
162
- */
163
- public function register() {
164
- foreach ( array( 'id', 'title', 'description' ) as $name ) {
165
- if ( empty( $this->$name ) ) {
166
- trigger_error( get_class( $this ) . " has not set the $name variable.", E_USER_ERROR );
167
- }
168
- }
169
-
170
- do_action( 'itsec-settings-page-register-module', $this );
171
- }
172
-
173
- /**
174
- * Allow the module to enqueue module-specific scripts and styles.
175
- *
176
- * @access public
177
- */
178
- public function enqueue_scripts_and_styles() {}
179
-
180
- /**
181
- * Allow a module to process an AJAX request.
182
- *
183
- * The module's implementation of this function can either handle all input manually or return a data structure to
184
- * be returned by the module API. The module's Javascript can make use of the itsec_module_send_ajax_request()
185
- * Javascript function in order to make the AJAX request. It has a request format of:
186
- * itsecSettingsPage.sendModuleAJAXRequest( module, data, callback );
187
- *
188
- * @access public
189
- *
190
- * @param array $data Array of data sent by the AJAX request.
191
- */
192
- public function handle_ajax_request( $data ) {}
193
-
194
- /**
195
- * Return the settings for the module.
196
- *
197
- * @access public
198
- *
199
- * @return array List of settings.
200
- */
201
- public function get_settings() {
202
- return ITSEC_Modules::get_settings( $this->id );
203
- }
204
-
205
- /**
206
- * Render the module's settings content.
207
- *
208
- * This function should be left as-is in subclasses.
209
- *
210
- * @access public
211
- *
212
- * @param ITSEC_Form $form ITSEC_Form object used to create inputs.
213
- */
214
- public function render( $form ) {
215
-
216
- $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'module' => $this->id ) );
217
-
218
- ?>
219
- <div class="itsec-settings-module-description">
220
- <?php $this->render_description( $form ); ?>
221
- </div>
222
- <?php if ( $messages ) : ?>
223
- <div class="itsec-settings-module-service-status">
224
- <?php foreach ( $messages as $message ): ?>
225
- <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
226
- <p><?php echo $message['message']; ?></p>
227
- </div>
228
- <?php endforeach; ?>
229
- </div>
230
- <?php endif; ?>
231
- <div class="itsec-settings-module-settings">
232
- <?php $this->render_settings( $form ); ?>
233
- </div>
234
- <?php
235
-
236
- }
237
-
238
- /**
239
- * Render the module description.
240
- *
241
- * The description is shown whether the module is active or not. Ensure that the description adequately informs the
242
- * user of the value of the module without requiring them to see the actual settings.
243
- *
244
- * @access protected
245
- *
246
- * @param object $form ITSEC_Form object used to create inputs.
247
- */
248
- protected function render_description( $form ) {
249
-
250
- ?>
251
- <p>Example module description.</p>
252
- <?php
253
-
254
- }
255
-
256
- /**
257
- * Render the module settings.
258
- *
259
- * The inputs and input descriptions should be output in this function. This output is hidden when a module is
260
- * deactivated.
261
- *
262
- * @access protected
263
- *
264
- * @param ITSEC_Form $form ITSEC_Form object used to create inputs.
265
- */
266
- protected function render_settings( $form ) {
267
-
268
- ?>
269
- <table class="form-table itsec-settings-section">
270
- <tbody>
271
- <tr>
272
- <th><label>Setting 1</label></th>
273
- <td>
274
- <?php $form->add_text( 'setting_1' ); ?>
275
- </td>
276
- </tr>
277
- <tr>
278
- <th><label>Setting 2</label></th>
279
- <td>
280
- <?php $form->add_text( 'setting_2' ); ?>
281
- </td>
282
- </tr>
283
- </tbody>
284
- </table>
285
- <?php
286
-
287
- }
288
-
289
- /**
290
- * Process form input.
291
- *
292
- * This function should be left as-is in subclasses unless specific processing is required.
293
- *
294
- * @access public
295
- *
296
- * @param array $data Array of form inputs to be processed and stored.
297
- */
298
- public function handle_form_post( $data ) {
299
- ITSEC_Modules::set_settings( $this->id, $data );
300
- }
301
-
302
- /**
303
- * Returns the errors array.
304
- *
305
- * This function should be left as-is in subclasses.
306
- *
307
- * @access public
308
- *
309
- * @return array Array of WP_Error objects.
310
- */
311
- public function get_errors() {
312
- $validator = ITSEC_Modules::get_validator( $this->id );
313
-
314
- if ( is_null( $validator ) ) {
315
- return array();
316
- }
317
-
318
- return $validator->get_errors();
319
- }
320
-
321
- /**
322
- * Returns the messages array.
323
- *
324
- * This function should be left as-is in subclasses.
325
- *
326
- * @access public
327
- *
328
- * @return array Array of status or update messages.
329
- */
330
- public function get_messages() {
331
- $validator = ITSEC_Modules::get_validator( $this->id );
332
-
333
- if ( is_null( $validator ) ) {
334
- return array();
335
- }
336
-
337
- return $validator->get_messages();
338
- }
339
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/page-dashboard.php ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ add_action( 'admin_enqueue_scripts', function () {
4
+ $primary_id = ITSEC_Dashboard_Util::get_primary_dashboard_id();
5
+
6
+ $preload_requests = [
7
+ '/?_fields=name,description,url,home' => [
8
+ 'route' => '/',
9
+ 'query' => [ '_fields' => 'name,description,url,home' ],
10
+ ],
11
+ '/ithemes-security/v1?context=help' => [
12
+ 'route' => '/ithemes-security/v1',
13
+ 'query' => [ 'context' => 'help' ],
14
+ ],
15
+ '/ithemes-security/v1/dashboard-static',
16
+ '/ithemes-security/v1/dashboards?_embed=1' => [
17
+ 'route' => '/ithemes-security/v1/dashboards',
18
+ 'embed' => true,
19
+ ],
20
+ '/ithemes-security/v1/dashboard-available-cards' => [
21
+ 'route' => '/ithemes-security/v1/dashboard-available-cards',
22
+ ],
23
+ '/ithemes-security/v1/actors?_embed=1' => [
24
+ 'route' => '/ithemes-security/v1/actors',
25
+ 'embed' => true,
26
+ ],
27
+ '/wp/v2/users/me?context=edit' => [
28
+ 'route' => '/wp/v2/users/me',
29
+ 'query' => [ 'context' => 'edit' ],
30
+ ],
31
+ ];
32
+
33
+ if ( ! $primary_id ) {
34
+ $create = new WP_REST_Request( 'POST', '/ithemes-security/v1/dashboards' );
35
+ $create->set_body_params( [
36
+ 'preset' => 'default',
37
+ 'label' => __( 'Security Dashboard', 'better-wp-security' ),
38
+ ] );
39
+ $created = rest_do_request( $create );
40
+
41
+ if ( ! $created->is_error() ) {
42
+ $primary_id = $created->get_data()['id'];
43
+ update_user_meta( get_current_user_id(), ITSEC_Dashboard::META_PRIMARY, $primary_id );
44
+ ITSEC_Dashboard_Util::flush_cache();
45
+ }
46
+ }
47
+
48
+ if ( $primary_id ) {
49
+ $key = "/ithemes-security/v1/dashboards/{$primary_id}?_embed=1";
50
+ $query = array();
51
+
52
+ if ( current_user_can( 'itsec_edit_dashboard', $primary_id ) ) {
53
+ $key .= '&context=edit';
54
+
55
+ $query['context'] = 'edit';
56
+ }
57
+
58
+ $preload_requests[ $key ] = array(
59
+ 'route' => "/ithemes-security/v1/dashboards/{$primary_id}",
60
+ 'embed' => true,
61
+ 'query' => $query,
62
+ );
63
+
64
+ $preload_requests["/ithemes-security/v1/dashboards/{$primary_id}/cards?_embed=1"] = array(
65
+ 'route' => "/ithemes-security/v1/dashboards/{$primary_id}/cards",
66
+ 'embed' => true,
67
+ );
68
+
69
+ $preload_requests[] = "/ithemes-security/v1/dashboards/{$primary_id}/layout";
70
+ }
71
+
72
+ $preload = ITSEC_Lib::preload_rest_requests( $preload_requests );
73
+
74
+ wp_enqueue_style( 'itsec-dashboard-dashboard' );
75
+ wp_enqueue_script( 'itsec-dashboard-dashboard' );
76
+ wp_add_inline_script(
77
+ 'itsec-dashboard-dashboard',
78
+ sprintf( 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', wp_json_encode( $preload ) )
79
+ );
80
+
81
+ foreach ( ITSEC_Modules::get_available_modules() as $module ) {
82
+ if ( ! ITSEC_Modules::is_active( $module ) ) {
83
+ continue;
84
+ }
85
+
86
+ $handle = "itsec-{$module}-dashboard";
87
+
88
+ if ( wp_script_is( $handle, 'registered' ) ) {
89
+ wp_enqueue_script( $handle );
90
+ }
91
+
92
+ if ( wp_style_is( $handle, 'registered' ) ) {
93
+ wp_enqueue_style( $handle );
94
+ }
95
+ }
96
+
97
+ do_action( 'itsec_dashboard_enqueue_scripts' );
98
+ } );
99
+
100
+ add_action( 'itsec-page-show', function () {
101
+ printf(
102
+ '<div id="itsec-dashboard-root" data-can-manage="%s" data-install-type="%s"></div>',
103
+ ITSEC_Core::current_user_can_manage(),
104
+ ITSEC_Core::get_install_type()
105
+ );
106
+ } );
107
+
108
+ remove_all_actions( 'all_admin_notices' );
109
+ remove_all_actions( 'network_admin_notices' );
110
+ remove_all_actions( 'admin_notices' );
core/admin-pages/page-debug.php CHANGED
@@ -312,6 +312,9 @@ final class ITSEC_Debug_Page {
312
  'ITSEC_DISABLE_PASSWORD_STRENGTH',
313
  'ITSEC_DISABLE_INACTIVE_USER_CHECK',
314
  'ITSEC_SHOW_FEATURE_FLAGS',
 
 
 
315
  );
316
 
317
  ITSEC_Lib::load( 'feature-flags' );
312
  'ITSEC_DISABLE_PASSWORD_STRENGTH',
313
  'ITSEC_DISABLE_INACTIVE_USER_CHECK',
314
  'ITSEC_SHOW_FEATURE_FLAGS',
315
+ 'ITSEC_ENABLE_BACKUPS',
316
+ 'ITSEC_FORCE_UNINSTALL',
317
+ 'ITSEC_IGNORE_MODULE_REQUIREMENTS',
318
  );
319
 
320
  ITSEC_Lib::load( 'feature-flags' );
core/admin-pages/page-logs.php CHANGED
@@ -3,14 +3,10 @@
3
 
4
  final class ITSEC_Logs_Page {
5
  private $self_url = '';
6
- private $modules = array();
7
  private $widgets = array();
8
  private $translations = array();
9
 
10
-
11
  public function __construct() {
12
- add_action( 'itsec-logs-page-register-widget', array( $this, 'register_widget' ) );
13
-
14
  add_action( 'itsec-page-show', array( $this, 'handle_page_load' ) );
15
  add_action( 'itsec-page-ajax', array( $this, 'handle_ajax_request' ) );
16
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
@@ -20,13 +16,9 @@ final class ITSEC_Logs_Page {
20
 
21
  $this->set_translation_strings();
22
 
23
-
24
- require( dirname( __FILE__ ) . '/module-settings.php' );
25
  require( dirname( __FILE__ ) . '/sidebar-widget.php' );
26
-
27
  require_once( ITSEC_Core::get_core_dir() . '/lib/form.php' );
28
 
29
-
30
  do_action( 'itsec-logs-page-init' );
31
  do_action( 'itsec-logs-page-register-widgets' );
32
 
@@ -39,14 +31,6 @@ final class ITSEC_Logs_Page {
39
  public function add_scripts() {
40
  ITSEC_Lib::enqueue_util();
41
 
42
- foreach ( $this->modules as $id => $module ) {
43
- $module->enqueue_scripts_and_styles();
44
- }
45
-
46
- foreach ( $this->widgets as $id => $widget ) {
47
- $widget->enqueue_scripts_and_styles();
48
- }
49
-
50
  $vars = array(
51
  'ajax_action' => 'itsec_logs_page',
52
  'ajax_nonce' => wp_create_nonce( 'itsec-logs-nonce' ),
@@ -148,23 +132,6 @@ final class ITSEC_Logs_Page {
148
  $widget->handle_form_post( $post_data[$id] );
149
  }
150
  } else {
151
- if ( ! empty( $_POST['module'] ) ) {
152
- if ( isset( $this->modules[$_POST['module']] ) ) {
153
- $modules = array( $_POST['module'] => $this->modules[$_POST['module']] );
154
- } else {
155
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-save-unrecognized-module', sprintf( __( 'The supplied module (%s) is not recognized. The module settings could not be saved.', 'better-wp-security' ), $_POST['module'] ) ) );
156
- $modules = array();
157
- }
158
- } else {
159
- $modules = $this->modules;
160
- }
161
-
162
- foreach ( $modules as $id => $module ) {
163
- if ( isset( $post_data[$id] ) ) {
164
- $results = $module->handle_form_post( $post_data[$id] );
165
- }
166
- }
167
-
168
  if ( ITSEC_Response::is_success() ) {
169
  if ( ITSEC_Response::get_show_default_success_message() ) {
170
  ITSEC_Response::add_message( __( 'The settings saved successfully.', 'better-wp-security' ) );
@@ -481,68 +448,41 @@ final class ITSEC_Logs_Page {
481
  ?>
482
  </div>
483
 
484
- <div id="poststuff">
485
- <div id="post-body" class="metabox-holder columns-2 hide-if-no-js">
486
- <div id="postbox-container-2" class="postbox-container">
487
- <?php $this->show_old_logs_migration(); ?>
488
-
489
- <?php if ( 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' ) ) : ?>
490
- <p><?php _e( 'To view logs within the plugin you must enable database logging in the Global Settings. File logging is not available for access within the plugin itself.', 'better-wp-security' ); ?></p>
491
- <p><?php printf( wp_kses( __( 'The log file can be found at: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), ITSEC_Log::get_log_file_path() ); ?></p>
492
- <?php else : ?>
493
- <div class="itsec-module-cards-container list">
494
- <?php
495
- $list = new ITSEC_Logs_List_Table();
496
-
497
- $list->prepare_items();
498
- $list->views();
499
- $form->start_form( array( 'method' => 'GET' ) );
500
- $form->add_hidden( 'page', 'itsec-logs' );
501
- $list->display();
502
- $form->end_form();
503
- ?>
504
- </div>
505
- <?php endif; ?>
506
  </div>
507
- <div class="itsec-modal-background"></div>
508
- <div id="itsec-log-details-container" class="grid">
509
- <div class="itsec-module-settings-container">
510
- <div class="itsec-modal-navigation">
511
- <button class="dashicons itsec-close-modal"></button>
512
- </div>
513
- <div class="itsec-module-settings-content-container">
514
- <div class="itsec-module-settings-content">
515
- <div class="itsec-module-messages-container"></div>
516
- <div class="itsec-module-settings-content-main"></div>
517
- </div>
518
- </div>
519
- </div>
520
  </div>
521
-
522
- <div id="postbox-container-1" class="postbox-container">
523
- <?php foreach ( $this->widgets as $id => $widget ) : ?>
524
- <?php $form->start_form( "itsec-sidebar-widget-form-$id" ); ?>
525
- <?php $form->add_nonce( 'itsec-logs-page' ); ?>
526
- <?php $form->add_hidden( 'widget-id', $id ); ?>
527
- <div id="itsec-sidebar-widget-<?php echo $id; ?>" class="postbox itsec-sidebar-widget">
528
- <div class="postbox-header">
529
- <h2 class="hndle ui-sortable-handle"><span><?php echo esc_html( $widget->title ); ?></span></h2>
530
- </div>
531
- <div class="inside">
532
- <?php $this->get_widget_settings( $id, $form, true ); ?>
533
- </div>
534
- </div>
535
- <?php $form->end_form(); ?>
536
- <?php endforeach; ?>
537
  </div>
538
  </div>
539
-
540
- <div class="hide-if-js">
541
- <p class="itsec-warning-message"><?php _e( 'iThemes Security requires Javascript in order for the settings to be modified. Please enable Javascript to configure the settings.', 'better-wp-security' ); ?></p>
542
- </div>
543
-
544
- <div class="hidden" id="itsec-logs-cache">
545
- </div>
546
  </div>
547
  </div>
548
  <?php
@@ -550,56 +490,7 @@ final class ITSEC_Logs_Page {
550
  }
551
 
552
  public function register_widget( $widget ) {
553
- if ( ! is_object( $widget ) || ! is_a( $widget, 'ITSEC_Settings_Page_Sidebar_Widget' ) ) {
554
- trigger_error( 'An invalid widget was registered.', E_USER_ERROR );
555
- return;
556
- }
557
-
558
- if ( isset( $this->modules[$widget->id] ) ) {
559
- trigger_error( "A widget with the id of {$widget->id} is registered. Widget id's must be unique from any other module or widget." );
560
- return;
561
- }
562
-
563
- if ( isset( $this->widgets[$widget->id] ) ) {
564
- trigger_error( "A widget with the id of {$widget->id} is already registered. Widget id's must be unique from any other module or widget." );
565
- return;
566
- }
567
-
568
-
569
- $this->widgets[$widget->id] = $widget;
570
- }
571
-
572
- private function get_widget_settings( $id, $form = false, $echo = false ) {
573
- if ( ! isset( $this->widgets[$id] ) ) {
574
- $error = new WP_Error( 'itsec-settings-page-get-widget-settings-invalid-id', sprintf( __( 'The requested widget (%s) does not exist. Logs for it cannot be rendered.', 'better-wp-security' ), $id ) );
575
-
576
- if ( $echo ) {
577
- ITSEC_Lib::show_error_message( $error );
578
- } else {
579
- return $error;
580
- }
581
- }
582
-
583
- if ( false === $form ) {
584
- $form = new ITSEC_Form();
585
- }
586
-
587
- $widget = $this->widgets[$id];
588
-
589
- $form->add_input_group( $id );
590
- $form->set_defaults( $widget->get_defaults() );
591
-
592
- if ( ! $echo ) {
593
- ob_start();
594
- }
595
-
596
- $widget->render( $form );
597
-
598
- $form->remove_all_input_groups();
599
-
600
- if ( ! $echo ) {
601
- return ob_get_clean();
602
- }
603
  }
604
 
605
  private function show_old_logs_migration() {
3
 
4
  final class ITSEC_Logs_Page {
5
  private $self_url = '';
 
6
  private $widgets = array();
7
  private $translations = array();
8
 
 
9
  public function __construct() {
 
 
10
  add_action( 'itsec-page-show', array( $this, 'handle_page_load' ) );
11
  add_action( 'itsec-page-ajax', array( $this, 'handle_ajax_request' ) );
12
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
16
 
17
  $this->set_translation_strings();
18
 
 
 
19
  require( dirname( __FILE__ ) . '/sidebar-widget.php' );
 
20
  require_once( ITSEC_Core::get_core_dir() . '/lib/form.php' );
21
 
 
22
  do_action( 'itsec-logs-page-init' );
23
  do_action( 'itsec-logs-page-register-widgets' );
24
 
31
  public function add_scripts() {
32
  ITSEC_Lib::enqueue_util();
33
 
 
 
 
 
 
 
 
 
34
  $vars = array(
35
  'ajax_action' => 'itsec_logs_page',
36
  'ajax_nonce' => wp_create_nonce( 'itsec-logs-nonce' ),
132
  $widget->handle_form_post( $post_data[$id] );
133
  }
134
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  if ( ITSEC_Response::is_success() ) {
136
  if ( ITSEC_Response::get_show_default_success_message() ) {
137
  ITSEC_Response::add_message( __( 'The settings saved successfully.', 'better-wp-security' ) );
448
  ?>
449
  </div>
450
 
451
+ <div>
452
+ <?php $this->show_old_logs_migration(); ?>
453
+
454
+ <?php if ( 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' ) ) : ?>
455
+ <p><?php _e( 'To view logs within the plugin you must enable database logging in the Global Settings. File logging is not available for access within the plugin itself.', 'better-wp-security' ); ?></p>
456
+ <p><?php printf( wp_kses( __( 'The log file can be found at: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), ITSEC_Log::get_log_file_path() ); ?></p>
457
+ <?php else : ?>
458
+ <div class="itsec-module-cards-container list">
459
+ <?php
460
+ $list = new ITSEC_Logs_List_Table();
461
+
462
+ $list->prepare_items();
463
+ $list->views();
464
+ $form->start_form( array( 'method' => 'GET' ) );
465
+ $form->add_hidden( 'page', 'itsec-logs' );
466
+ $list->display();
467
+ $form->end_form();
468
+ ?>
 
 
 
 
469
  </div>
470
+ <?php endif; ?>
471
+ </div>
472
+ <div class="itsec-modal-background"></div>
473
+ <div id="itsec-log-details-container" class="grid">
474
+ <div class="itsec-module-settings-container">
475
+ <div class="itsec-modal-navigation">
476
+ <button class="dashicons itsec-close-modal"></button>
 
 
 
 
 
 
477
  </div>
478
+ <div class="itsec-module-settings-content-container">
479
+ <div class="itsec-module-settings-content">
480
+ <div class="itsec-module-messages-container"></div>
481
+ <div class="itsec-module-settings-content-main"></div>
482
+ </div>
 
 
 
 
 
 
 
 
 
 
 
483
  </div>
484
  </div>
485
+ </div>
 
 
 
 
 
 
486
  </div>
487
  </div>
488
  <?php
490
  }
491
 
492
  public function register_widget( $widget ) {
493
+ _deprecated_function( __METHOD__, '7.0.1' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  }
495
 
496
  private function show_old_logs_migration() {
core/admin-pages/page-security-check.php DELETED
@@ -1,4 +0,0 @@
1
- <?php
2
-
3
- wp_redirect( ITSEC_Core::get_security_check_page_url() );
4
- exit;
 
 
 
 
core/admin-pages/page-settings.php CHANGED
@@ -1,676 +1,66 @@
1
  <?php
2
-
3
-
4
- final class ITSEC_Settings_Page {
5
- private static $instance;
6
-
7
- private $self_url = '';
8
-
9
- /** @var ITSEC_Module_Settings_Page[] */
10
- private $modules = array();
11
-
12
- /** @var ITSEC_Settings_Page_Sidebar_Widget[] */
13
- private $widgets = array();
14
- private $translations = array();
15
-
16
-
17
- private function __construct() {
18
- add_action( 'itsec-settings-page-register-module', array( $this, 'register_module' ) );
19
- add_action( 'itsec-settings-page-register-widget', array( $this, 'register_widget' ) );
20
-
21
- add_action( 'itsec-page-show', array( $this, 'handle_page_load' ) );
22
- add_action( 'itsec-page-ajax', array( $this, 'handle_ajax_request' ) );
23
- add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
24
- add_action( 'admin_print_styles', array( $this, 'add_styles' ) );
25
-
26
- add_filter( 'admin_body_class', array( $this, 'add_settings_classes' ) );
27
-
28
- $this->set_translation_strings();
29
-
30
- if ( ! empty( $_GET['enable'] ) && ! empty( $_GET['itsec-enable-nonce'] ) && wp_verify_nonce( $_GET['itsec-enable-nonce'], 'itsec-enable-' . $_GET['enable'] ) ) {
31
- ITSEC_Modules::activate( $_GET['enable'] );
32
- }
33
-
34
- require( dirname( __FILE__ ) . '/module-settings.php' );
35
- require( dirname( __FILE__ ) . '/sidebar-widget.php' );
36
-
37
- require_once( ITSEC_Core::get_core_dir() . '/lib/form.php' );
38
-
39
-
40
- do_action( 'itsec-settings-page-init' );
41
- do_action( 'itsec-settings-page-register-modules' );
42
- do_action( 'itsec-settings-page-register-widgets' );
43
-
44
-
45
- if ( ! empty( $_POST ) && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) ) {
46
- $this->handle_post();
47
- }
48
- }
49
-
50
- public static function get_instance() {
51
- if ( ! self::$instance ) {
52
- self::$instance = new self;
53
- }
54
-
55
- return self::$instance;
56
- }
57
-
58
- public function add_settings_classes( $classes ) {
59
- if ( ITSEC_Modules::get_setting( 'global', 'show_error_codes' ) ) {
60
- $classes .= ' itsec-show-error-codes';
61
- }
62
-
63
- if ( ITSEC_Modules::get_setting( 'global', 'write_files' ) ) {
64
- $classes .= ' itsec-write-files-enabled';
65
- } else {
66
- $classes .= ' itsec-write-files-disabled';
67
- }
68
-
69
- $classes = trim( $classes );
70
-
71
- return $classes;
72
- }
73
-
74
- public function add_scripts() {
75
- ITSEC_Lib::enqueue_util();
76
-
77
- foreach ( $this->modules as $id => $module ) {
78
- $module->enqueue_scripts_and_styles();
79
- }
80
-
81
- foreach ( $this->widgets as $id => $widget ) {
82
- $widget->enqueue_scripts_and_styles();
83
- }
84
-
85
- $vars = array(
86
- 'ajax_action' => 'itsec_settings_page',
87
- 'ajax_nonce' => wp_create_nonce( 'itsec-settings-nonce' ),
88
- 'show_security_check' => ITSEC_Modules::get_setting( 'global', 'show_security_check' ),
89
- 'translations' => $this->translations,
90
- );
91
-
92
- if ( $vars['show_security_check'] ) {
93
- ITSEC_Modules::set_setting( 'global', 'show_security_check', false );
94
-
95
- if ( ! empty( $_GET['module'] ) && 'security-check' === $_GET['module'] ) {
96
- $vars['show_security_check'] = false;
97
- }
98
- }
99
-
100
- wp_enqueue_script( 'itsec-scrollTo', plugins_url( 'js/scrollTo.js', dirname( __FILE__ ) ), array( 'jquery' ) );
101
- wp_enqueue_script( 'itsec-settings-page-script', plugins_url( 'js/settings.js', __FILE__ ), array( 'underscore' ), ITSEC_Core::get_plugin_build(), true );
102
- wp_localize_script( 'itsec-settings-page-script', 'itsec_page', $vars );
103
- }
104
-
105
- public function add_styles() {
106
- wp_enqueue_style( 'itsec-settings-page-style', plugins_url( 'css/style.css', __FILE__ ), array(), ITSEC_Core::get_plugin_build() );
107
- }
108
-
109
- private function set_translation_strings() {
110
- $this->translations = array(
111
- 'save_settings' => __( 'Save Settings', 'better-wp-security' ),
112
- 'close_settings' => __( 'Close', 'better-wp-security' ),
113
- 'show_settings' => __( 'Configure Settings', 'better-wp-security' ),
114
- 'hide_settings' => __( 'Hide Settings', 'better-wp-security' ),
115
- 'show_description' => __( 'Learn More', 'better-wp-security' ),
116
- 'hide_description' => __( 'Hide Details', 'better-wp-security' ),
117
- 'show_information' => __( 'Show Details', 'better-wp-security' ),
118
- 'activate' => __( 'Enable', 'better-wp-security' ),
119
- 'deactivate' => __( 'Disable', 'better-wp-security' ),
120
- 'error' => __( 'Error', 'better-wp-security' ),
121
- 'dismiss' => __( 'Dismiss Notice', 'better-wp-security' ), // Screen reader text for dismissible notices
122
- 'copied' => __( 'Copied!', 'better-wp-security' ),
123
- 'copy_instruction' => __( 'Please press Ctrl/Cmd+C to copy.', 'better-wp-security' ),
124
-
125
- /* translators: 1: module name */
126
- 'successful_save' => __( 'Settings saved successfully for %1$s.', 'better-wp-security' ),
127
- );
128
-
129
- foreach ( $this->translations as $key => $message ) {
130
- if ( is_wp_error( $message ) ) {
131
- $messages = ITSEC_Response::get_error_strings( $message );
132
- $this->translations[$key] = $messages[0];
133
- }
134
- }
135
- }
136
-
137
- public function handle_ajax_request() {
138
- if ( WP_DEBUG ) {
139
- ini_set( 'display_errors', 1 );
140
- }
141
-
142
-
143
- ITSEC_Core::set_interactive( true );
144
-
145
- $method = ( isset( $_POST['method'] ) && is_string( $_POST['method'] ) ) ? $_POST['method'] : '';
146
- $module = ( isset( $_POST['module'] ) && is_string( $_POST['module'] ) ) ? $_POST['module'] : '';
147
-
148
- if ( empty( $GLOBALS['hook_suffix'] ) ) {
149
- $GLOBALS['hook_suffix'] = 'toplevel_page_itsec';
150
- }
151
-
152
-
153
- if ( false === check_ajax_referer( 'itsec-settings-nonce', 'nonce', false ) ) {
154
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-failed-nonce', __( 'A nonce security check failed, preventing the request from completing as expected. Please try reloading the page and trying again.', 'better-wp-security' ) ) );
155
- } else if ( ! ITSEC_Core::current_user_can_manage() ) {
156
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-insufficient-privileges', __( 'A permissions security check failed, preventing the request from completing as expected. The currently logged in user does not have sufficient permissions to make this request. Please try reloading the page and trying again.', 'better-wp-security' ) ) );
157
- } else if ( empty( $method ) ) {
158
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-missing-method', __( 'The server did not receive a valid request. The required "method" argument is missing. Please try again.', 'better-wp-security' ) ) );
159
- } else if ( 'save' === $method ) {
160
- $this->handle_post();
161
- ITSEC_Response::maybe_flag_new_notifications_available();
162
- } else if ( empty( $module ) ) {
163
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-missing-module', __( 'The server did not receive a valid request. The required "module" argument is missing. Please try again.', 'better-wp-security' ) ) );
164
- } else if ( 'activate' === $method ) {
165
- $was_active = ITSEC_Modules::activate( $module );
166
- ITSEC_Response::set_response( $was_active );
167
-
168
- if ( ! $was_active ) {
169
- ITSEC_Modules::load_module_file( 'active.php', $module );
170
- }
171
-
172
- ITSEC_Response::add_store_dispatch( 'ithemes-security/user-groups', 'fetchGroupsSettings' );
173
- ITSEC_Response::add_store_dispatch( 'ithemes-security/core', 'fetchIndex', [ true ] );
174
- ITSEC_Response::maybe_flag_new_notifications_available();
175
- } else if ( 'deactivate' === $method ) {
176
- ITSEC_Response::set_response( ITSEC_Modules::deactivate( $module ) );
177
- ITSEC_Response::add_store_dispatch( 'ithemes-security/user-groups', 'fetchGroupsSettings' );
178
- ITSEC_Response::add_store_dispatch( 'ithemes-security/core', 'fetchIndex', [ true ] );
179
- ITSEC_Response::maybe_flag_new_notifications_available();
180
- } else if ( 'is_active' === $method ) {
181
- ITSEC_Response::set_response( ITSEC_Modules::is_active( $module ) );
182
- } else if ( 'get_refreshed_module_settings' === $method ) {
183
- ITSEC_Response::set_response( $this->get_module_settings( $module ) );
184
- } else if ( 'get_refreshed_widget_settings' === $method ) {
185
- ITSEC_Response::set_response( $this->get_widget_settings( $module ) );
186
- } else if ( 'get_refreshed_module_form' === $method ) {
187
- $form = new ITSEC_Form();
188
- $this->prepare_modules_and_calculate_filters();
189
- ob_start();
190
- $this->print_modules_form( $form );
191
- ITSEC_Response::set_response( ob_get_clean() );
192
- } else if ( 'handle_module_request' === $method ) {
193
- if ( isset( $this->modules[$module] ) ) {
194
- if ( isset( $_POST['data'] ) ) {
195
- $returned_value = $this->modules[$module]->handle_ajax_request( $_POST['data'] );
196
-
197
- if ( ! is_null( $returned_value ) ) {
198
- ITSEC_Response::set_response( $returned_value );
199
- }
200
- } else {
201
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-module-request-missing-data', __( 'The server did not receive a valid request. The required "data" argument for the module is missing. Please try again.', 'better-wp-security' ) ) );
202
- }
203
- } else {
204
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-module-request-invalid-module', __( "The server did not receive a valid request. The supplied module, \"$module\", does not exist. Please try again.", 'better-wp-security' ) ) );
205
- }
206
- } else if ( 'handle_widget_request' === $method ) {
207
- if ( isset( $this->widgets[$module] ) ) {
208
- if ( isset( $_POST['data'] ) ) {
209
- $this->widgets[$module]->handle_ajax_request( $_POST['data'] );
210
- } else {
211
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-widget-request-missing-data', __( 'The server did not receive a valid request. The required "data" argument for the widget is missing. Please try again.', 'better-wp-security' ) ) );
212
- }
213
- } else {
214
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-widget-request-invalid-widget', __( "The server did not receive a valid request. The supplied widget, \"$module\", does not exist. Please try again.", 'better-wp-security' ) ) );
215
- }
216
- } else {
217
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-page-unknown-method', __( 'The server did not receive a valid request. An unknown "method" argument was supplied. Please try again.', 'better-wp-security' ) ) );
218
- }
219
-
220
-
221
- ITSEC_Response::send_json();
222
- }
223
-
224
- public function register_module( $module ) {
225
- if ( ! is_object( $module ) || ! is_a( $module, 'ITSEC_Module_Settings_Page' ) ) {
226
- trigger_error( 'An invalid module was registered.', E_USER_ERROR );
227
- return;
228
- }
229
-
230
- if ( isset( $this->modules[$module->id] ) ) {
231
- trigger_error( "A module with the id of {$module->id} is already registered. Module id's must be unique." );
232
- return;
233
- }
234
-
235
- $this->modules[$module->id] = $module;
236
- }
237
-
238
- public function register_widget( $widget ) {
239
- if ( ! is_object( $widget ) || ! is_a( $widget, 'ITSEC_Settings_Page_Sidebar_Widget' ) ) {
240
- trigger_error( 'An invalid widget was registered.', E_USER_ERROR );
241
- return;
242
- }
243
-
244
- if ( isset( $this->modules[$widget->id] ) ) {
245
- trigger_error( "A widget with the id of {$widget->id} is registered. Widget id's must be unique from any other module or widget." );
246
- return;
247
- }
248
-
249
- if ( isset( $this->widgets[$widget->id] ) ) {
250
- trigger_error( "A widget with the id of {$widget->id} is already registered. Widget id's must be unique from any other module or widget." );
251
- return;
252
- }
253
-
254
-
255
- $this->widgets[$widget->id] = $widget;
256
- }
257
-
258
- private function handle_post() {
259
- if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
260
- // Only process the nonce when the request is not an AJAX request as the AJAX handler has its own nonce check.
261
- ITSEC_Form::check_nonce( 'itsec-settings-page' );
262
- }
263
-
264
-
265
- $post_data = ITSEC_Form::get_post_data();
266
- $saved = true;
267
- $js_function_calls = array();
268
-
269
- if ( ! empty( $_POST['widget-id'] ) ) {
270
- $id = $_POST['widget-id'];
271
-
272
- if ( isset( $post_data[$id] ) && isset( $this->widgets[$id] ) ) {
273
- $widget = $this->widgets[$id];
274
-
275
- $widget->handle_form_post( $post_data[$id] );
276
- }
277
- } else {
278
- if ( ! empty( $_POST['module'] ) ) {
279
- if ( isset( $this->modules[$_POST['module']] ) ) {
280
- $modules = array( $_POST['module'] => $this->modules[$_POST['module']] );
281
- } else {
282
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-save-unrecognized-module', sprintf( __( 'The supplied module (%s) is not recognized. The module settings could not be saved.', 'better-wp-security' ), $_POST['module'] ) ) );
283
- $modules = array();
284
- }
285
- } else {
286
- $modules = $this->modules;
287
- }
288
-
289
- foreach ( $modules as $id => $module ) {
290
- if ( isset( $post_data[$id] ) ) {
291
- $results = $module->handle_form_post( $post_data[$id] );
292
- }
293
- }
294
-
295
- if ( ITSEC_Response::is_success() ) {
296
- if ( ITSEC_Response::get_show_default_success_message() ) {
297
- ITSEC_Response::add_message( __( 'The settings saved successfully.', 'better-wp-security' ) );
298
- }
299
- } else {
300
- if ( ITSEC_Response::get_show_default_error_message() ) {
301
- $error_count = ITSEC_Response::get_error_count();
302
-
303
- if ( $error_count > 0 ) {
304
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-data-not-saved', _n( 'The settings could not be saved. Please correct the error above and try again.', 'The settings could not be saved. Please correct the errors above and try again.', $error_count, 'better-wp-security' ) ) );
305
- } else {
306
- ITSEC_Response::add_error( new WP_Error( 'itsec-settings-data-not-saved-missing-error', __( 'The settings could not be saved. Due to an unknown error. Please try refreshing the page and trying again.', 'better-wp-security' ) ) );
307
- }
308
- }
309
- }
310
- }
311
-
312
-
313
- if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
314
- return;
315
- }
316
-
317
- ITSEC_Response::maybe_flag_new_notifications_available();
318
- ITSEC_Response::maybe_regenerate_wp_config();
319
- ITSEC_Response::maybe_regenerate_server_config();
320
- ITSEC_Response::maybe_do_force_logout();
321
- ITSEC_Response::maybe_do_redirect();
322
- }
323
-
324
- public function handle_page_load( $self_url ) {
325
- $this->self_url = $self_url;
326
-
327
- $this->show_settings_page();
328
- }
329
-
330
- private function get_module_settings( $id, $form = false, $echo = false ) {
331
- if ( ! isset( $this->modules[$id] ) ) {
332
- $error = new WP_Error( 'itsec-settings-page-get-module-settings-invalid-id', sprintf( __( 'The requested module (%s) does not exist. Settings for it cannot be rendered.', 'better-wp-security' ), $id ) );
333
-
334
- if ( $echo ) {
335
- ITSEC_Lib::show_error_message( $error );
336
- } else {
337
- return $error;
338
- }
339
- }
340
-
341
- if ( false === $form ) {
342
- $form = new ITSEC_Form();
343
- }
344
-
345
- $module = $this->modules[$id];
346
-
347
- $form->add_input_group( $id );
348
- $form->set_defaults( $module->get_settings() );
349
-
350
- if ( ! $echo ) {
351
- ob_start();
352
- }
353
-
354
- $module->render( $form );
355
-
356
- $form->remove_all_input_groups();
357
-
358
- if ( ! $echo ) {
359
- return ob_get_clean();
360
- }
361
- }
362
-
363
- private function get_widget_settings( $id, $form = false, $echo = false ) {
364
- if ( ! isset( $this->widgets[$id] ) ) {
365
- $error = new WP_Error( 'itsec-settings-page-get-widget-settings-invalid-id', sprintf( __( 'The requested widget (%s) does not exist. Settings for it cannot be rendered.', 'better-wp-security' ), $id ) );
366
-
367
- if ( $echo ) {
368
- ITSEC_Lib::show_error_message( $error );
369
- } else {
370
- return $error;
371
- }
372
- }
373
-
374
- if ( false === $form ) {
375
- $form = new ITSEC_Form();
376
- }
377
-
378
- $widget = $this->widgets[$id];
379
-
380
- $form->add_input_group( $id );
381
- $form->set_defaults( $widget->get_defaults() );
382
-
383
- if ( ! $echo ) {
384
- ob_start();
385
- }
386
-
387
- $widget->render( $form );
388
-
389
- $form->remove_all_input_groups();
390
-
391
- if ( ! $echo ) {
392
- return ob_get_clean();
393
- }
394
- }
395
-
396
- private function prepare_modules_and_calculate_filters() {
397
- $module_filters = array(
398
- 'all' => array(
399
- _x( 'All', 'List all modules', 'better-wp-security' ),
400
- 0,
401
- ),
402
- 'recommended' => array(
403
- _x( 'Recommended', 'List recommended modules', 'better-wp-security' ),
404
- 0,
405
- ),
406
- 'advanced' => array(
407
- _x( 'Advanced', 'List advanced modules', 'better-wp-security' ),
408
- 0,
409
- ),
410
- );
411
-
412
- foreach ( $this->modules as $id => $module ) {
413
- $module_filters['all'][1]++;
414
-
415
- if ( isset( $module_filters[$module->type] ) ) {
416
- $module_filters[$module->type][1]++;
417
- }
418
-
419
- $module->enabled = ITSEC_Modules::is_active( $id );
420
- $module->always_active = ITSEC_Modules::is_always_active( $id );
421
- }
422
-
423
- return $module_filters;
424
- }
425
-
426
- private function show_settings_page() {
427
- $form = new ITSEC_Form();
428
-
429
- $module_filters = $this->prepare_modules_and_calculate_filters();
430
-
431
- $current_type = isset( $_REQUEST['module_type'] ) ? $_REQUEST['module_type'] : 'recommended';
432
-
433
- $feature_tabs = array();
434
-
435
- foreach ( $module_filters as $type => $data ) {
436
- if ( $current_type === $type ) {
437
- $class = 'current';
438
- } else {
439
- $class = '';
440
- }
441
-
442
- $feature_tabs[] = "<li class='itsec-module-filter' id='itsec-module-filter-$type'><a href='" . esc_url( add_query_arg( 'module_type', $type, $this->self_url ) ) . "' class='$class'>{$data[0]} <span class='count'>({$data[1]})</span></a>";
443
- }
444
-
445
- // Get user's view preference
446
- $view = get_user_meta( get_current_user_id(), 'itsec-settings-view', true );
447
-
448
- // Default to grid view for users that have an invalid or unspecified view
449
- if ( ! in_array( $view, array( 'grid', 'list' ) ) ) {
450
- $view = 'grid';
451
- }
452
-
453
- ?>
454
- <div class="wrap">
455
- <h1>
456
- <?php _e( 'iThemes Security', 'better-wp-security' ); ?>
457
- <a href="<?php echo esc_url( ITSEC_Core::get_logs_page_url() ); ?>" class="page-title-action"><?php _e( 'View Logs', 'better-wp-security' ); ?></a>
458
- <a href="<?php echo esc_url( apply_filters( 'itsec_support_url', 'https://wordpress.org/support/plugin/better-wp-security' ) ); ?>" target="_blank" rel="noopener noreferrer" class="page-title-action"><?php _e( 'Support', 'better-wp-security' ); ?></a>
459
- </h1>
460
-
461
- <div id="itsec-settings-messages-container">
462
- <?php
463
- foreach ( ITSEC_Response::get_errors() as $error ) {
464
- ITSEC_Lib::show_error_message( $error );
465
- }
466
-
467
- foreach ( ITSEC_Response::get_messages() as $message ) {
468
- ITSEC_Lib::show_status_message( $message );
469
- }
470
- ?>
471
- </div>
472
-
473
- <div id="poststuff">
474
- <div id="post-body" class="metabox-holder columns-2 hide-if-no-js">
475
- <div id="postbox-container-2" class="postbox-container">
476
- <div class="itsec-module-section-heading">
477
- <div class="itsec-settings-view-toggle hide-if-no-js" data-nonce="<?php echo esc_attr( wp_create_nonce( 'set-user-setting-itsec-settings-view' ) ); ?>">
478
- <a class="itsec-grid<?php if ( 'grid' === $view ) { echo ' itsec-selected'; } ?>"><span class="dashicons dashicons-grid-view"></span></a>
479
- <a class="itsec-list<?php if ( 'list' === $view ) { echo ' itsec-selected'; } ?>"><span class="dashicons dashicons-list-view"></span></a>
480
- </div>
481
- <div class="itsec-module-search">
482
- <input type="search" placeholder="<?php esc_attr_e( 'Search Modules', 'better-wp-security' ); ?>" id="search" spellcheck="false" autocomplete="off" autofill="off" x-autocomplete="false">
483
- </div>
484
- <ul class="subsubsub itsec-feature-tabs hide-if-no-js">
485
- <?php echo implode( " |</li>\n", $feature_tabs ) . "</li>\n"; ?>
486
- <li class="itsec-module-filter hide-if-js" id="itsec-module-filter-search">| <a><?php esc_html_e( 'Search', 'better-wp-security' ); ?></a> <span class="count"></span></li>
487
- </ul>
488
- </div>
489
- <div class="itsec-module-cards-container <?php echo $view; ?> hide-if-js">
490
- <?php $this->print_modules_form( $form ); ?>
491
- </div>
492
- </div>
493
- <div class="itsec-modal-background"></div>
494
-
495
- <div id="postbox-container-1" class="postbox-container">
496
- <?php foreach ( $this->widgets as $id => $widget ) : ?>
497
- <?php if ( $widget->settings_form ) : ?>
498
- <?php $form->start_form( "itsec-sidebar-widget-form-$id" ); ?>
499
- <?php $form->add_nonce( 'itsec-settings-page' ); ?>
500
- <?php $form->add_hidden( 'widget-id', $id ); ?>
501
- <?php endif; ?>
502
- <div id="itsec-sidebar-widget-<?php echo $id; ?>" class="postbox itsec-sidebar-widget">
503
- <div class="postbox-header">
504
- <h2 class="hndle ui-sortable-handle"><span><?php echo esc_html( $widget->title ); ?></span></h2>
505
- </div>
506
- <div class="inside">
507
- <?php if ( $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'widget' => $id ) ) ) : ?>
508
- <div class="itsec-widgets-service-status">
509
- <?php foreach ( $messages as $message ): ?>
510
- <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
511
- <p><?php echo $message['message']; ?></p>
512
- </div>
513
- <?php endforeach; ?>
514
- </div>
515
- <?php endif; ?>
516
-
517
- <?php $this->get_widget_settings( $id, $form, true ); ?>
518
- </div>
519
- </div>
520
- <?php
521
- if ( $widget->settings_form ) {
522
- $form->end_form();
523
- }
524
- ?>
525
- <?php endforeach; ?>
526
- </div>
527
- </div>
528
-
529
- <div class="hide-if-js">
530
- <p class="itsec-warning-message"><?php _e( 'iThemes Security requires Javascript in order for the settings to be modified. Please enable Javascript to configure the settings.', 'better-wp-security' ); ?></p>
531
- </div>
532
- </div>
533
- </div>
534
- <?php
535
-
536
- }
537
-
538
- /**
539
- * Print the modules form element.
540
- *
541
- * @param ITSEC_Form $form
542
- */
543
- private function print_modules_form( $form ) {
544
- ?>
545
- <?php $form->start_form( 'itsec-module-settings-form' ); ?>
546
- <?php $form->add_nonce( 'itsec-settings-page' ); ?>
547
- <ul class="itsec-module-cards">
548
- <?php foreach ( $this->modules as $id => $module ) : ?>
549
- <?php
550
-
551
- $classes = array(
552
- 'itsec-module-type-' . $module->type,
553
- 'itsec-module-type-' . ( $module->enabled ? 'enabled' : 'disabled' ),
554
- );
555
-
556
- if ( $module->upsell ) {
557
- $classes[] = 'itsec-module-pro-upsell';
558
- }
559
-
560
- if ( $module->pro ) {
561
- $classes[] = 'itsec-module-type-pro';
562
- }
563
-
564
- if ( 'warning' === $module->status ) {
565
- $classes[] = 'itsec-module-status--warning';
566
- }
567
- ?>
568
- <li id="itsec-module-card-<?php echo $id; ?>" class="itsec-module-card <?php echo implode( ' ', $classes ); ?>" data-module-id="<?php echo $id; ?>">
569
- <div class="itsec-module-card-content">
570
- <?php if ( $module->upsell ) : ?>
571
- <a href="<?php echo esc_url( $module->upsell_url ); ?>" target="_blank" rel="noopener noreferrer" class="itsec-pro-upsell">&nbsp;</a>
572
- <?php endif; ?>
573
- <h2><?php echo esc_html( $module->title ); ?></h2>
574
- <?php if ( $module->pro ) : ?>
575
- <div class="itsec-pro-label"><?php _e( 'Pro', 'better-wp-security' ); ?></div>
576
- <?php endif; ?>
577
- <p class="module-description"><?php echo $module->description; ?></p>
578
- <?php if ( ! $module->upsell ) : ?>
579
- <div class="module-actions hide-if-no-js">
580
- <?php if ( $module->information_only ) : ?>
581
- <button class="button button-secondary itsec-toggle-settings information-only"><?php echo $this->translations['show_information']; ?></button>
582
- <?php elseif ( $module->enabled || $module->always_active ) : ?>
583
- <button class="button button-secondary itsec-toggle-settings"><?php echo $this->translations['show_settings']; ?></button>
584
- <?php if ( ! $module->always_active ) : ?>
585
- <button class="button button-secondary itsec-toggle-activation"><?php echo $this->translations['deactivate']; ?></button>
586
- <?php endif; ?>
587
- <?php else : ?>
588
- <button class="button button-secondary itsec-toggle-settings"><?php echo $this->translations['show_description']; ?></button>
589
- <button class="button button-primary itsec-toggle-activation"><?php echo $this->translations['activate']; ?></button>
590
- <?php endif; ?>
591
- </div>
592
- <?php endif; ?>
593
- </div>
594
- <?php if ( ! $module->upsell ) : ?>
595
- <div class="itsec-module-settings-container">
596
- <div class="itsec-modal-navigation">
597
- <button class="dashicons itsec-close-modal"></button>
598
- <button class="itsec-right dashicons hidden"><span class="screen-reader-text"><?php _e( 'Configure next iThemes Security setting', 'better-wp-security' ); ?></span></button>
599
- <button class="itsec-left dashicons hidden"><span class="screen-reader-text"><?php _e( 'Configure previous iThemes Security setting', 'better-wp-security' ); ?></span></button>
600
- </div>
601
- <div class="itsec-module-settings-content-container">
602
- <div class="itsec-module-settings-content">
603
- <h3 class="itsec-modal-header">
604
- <?php echo esc_html( $module->title ); ?>
605
- <?php do_action( 'itsec_module_settings_after_title', $id ); ?>
606
- </h3>
607
- <div class="itsec-module-messages-container" id="itsec-module-messages-container-<?php echo esc_attr( $id ); ?>"></div>
608
- <div class="itsec-module-settings-content-main">
609
- <?php $this->get_module_settings( $id, $form, true ); ?>
610
- </div>
611
- </div>
612
- </div>
613
- <div class="itsec-list-content-footer hide-if-no-js">
614
- <div class="itsec-module-footer-actions itsec-module-footer-actions--left">
615
- <?php if ( $module->can_save ) : ?>
616
- <button class="button button-primary itsec-module-settings-save"><?php echo $this->translations['save_settings']; ?></button>
617
- <?php endif; ?>
618
- <button class="button button-secondary itsec-module-settings-cancel"><?php _e( 'Cancel', 'better-wp-security' ); ?></button>
619
- </div>
620
-
621
- <div class="itsec-module-footer-actions itsec-module-footer-actions--right">
622
- <?php if ( $module->documentation ): ?>
623
- <a href="<?php echo esc_url( $module->documentation ); ?>" class="itsec-module-documentation"><?php esc_html_e( 'Documentation', 'better-wp-security' ); ?></a>
624
- <?php endif; ?>
625
- </div>
626
- </div>
627
- <div class="itsec-modal-content-footer">
628
- <div class="itsec-module-footer-actions itsec-module-footer-actions--left">
629
- <?php if ( $module->can_save ) : ?>
630
- <button class="button button-primary itsec-module-settings-save"><?php echo $this->translations['save_settings']; ?></button>
631
- <?php else : ?>
632
- <button class="button button-primary itsec-close-modal"><?php echo $this->translations['close_settings']; ?></button>
633
- <?php endif; ?>
634
- </div>
635
-
636
- <div class="itsec-module-footer-actions itsec-module-footer-actions--right">
637
- <?php if ( $module->documentation ): ?>
638
- <a href="<?php echo esc_url( $module->documentation ); ?>" class="itsec-module-documentation"><?php esc_html_e( 'Documentation', 'better-wp-security' ); ?></a>
639
- <?php endif; ?>
640
-
641
- <?php if ( $module->enabled || $module->always_active || $module->information_only ) : ?>
642
- <?php if ( ! $module->always_active && ! $module->information_only ) : ?>
643
- <button class="button button-secondary itsec-toggle-activation"><?php echo $this->translations['deactivate']; ?></button>
644
- <?php endif; ?>
645
- <?php else : ?>
646
- <button class="button button-primary itsec-toggle-activation"><?php echo $this->translations['activate']; ?></button>
647
- <?php endif; ?>
648
- </div>
649
- </div>
650
- </div>
651
- <?php endif; ?>
652
- </li>
653
- <?php endforeach; ?>
654
- <li class="itsec-module-card-filler"></li>
655
- </ul>
656
-
657
- <?php $form->end_form(); ?>
658
-
659
- <?php
660
-
661
- }
662
-
663
- public static function show_details_toggle( $description, $details ) {
664
- $self = self::get_instance();
665
-
666
- echo "<div class='itsec-warning-message itsec-details-toggle-container'>\n";
667
- echo "$description<br />\n";
668
- echo '<a href="#" class="hide-if-no-js">' . esc_html( $self->translations['show_information'] ) . '</a>';
669
- echo "<div class='itsec-details-toggle-details hide-if-js'>\n";
670
- echo $details;
671
- echo "</div>\n";
672
- echo "</div>\n";
673
- }
674
- }
675
-
676
- ITSEC_Settings_Page::get_instance();
1
  <?php
2
+ add_action( 'admin_enqueue_scripts', function () {
3
+ $preload = \ITSEC_Lib::preload_rest_requests( [
4
+ '/ithemes-security/v1/site-types' => [
5
+ 'route' => '/ithemes-security/v1/site-types',
6
+ ],
7
+ '/ithemes-security/v1?context=help' => [
8
+ 'route' => '/ithemes-security/v1',
9
+ 'query' => [ 'context' => 'help' ],
10
+ ],
11
+ '/ithemes-security/v1/user-matchables?_embed=1' => [
12
+ 'route' => '/ithemes-security/v1/user-matchables',
13
+ 'embed' => true,
14
+ ],
15
+ '/ithemes-security/v1/modules?context=edit&_embed=1' => [
16
+ 'route' => '/ithemes-security/v1/modules',
17
+ 'query' => [ 'context' => 'edit' ],
18
+ 'embed' => true,
19
+ ],
20
+ '/ithemes-security/v1/tools' => [
21
+ 'route' => '/ithemes-security/v1/tools',
22
+ ],
23
+ ] );
24
+ wp_enqueue_script( 'itsec-pages-settings' );
25
+ wp_enqueue_style( 'itsec-pages-settings' );
26
+ wp_add_inline_script(
27
+ 'itsec-pages-settings',
28
+ sprintf( 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', wp_json_encode( $preload ) )
29
+ );
30
+
31
+ foreach ( ITSEC_Modules::get_available_modules() as $module ) {
32
+ $handle = "itsec-{$module}-settings";
33
+
34
+ if ( wp_script_is( $handle, 'registered' ) ) {
35
+ wp_enqueue_script( $handle );
36
+ }
37
+
38
+ if ( wp_style_is( $handle, 'registered' ) ) {
39
+ wp_enqueue_style( $handle );
40
+ }
41
+ }
42
+
43
+ remove_action( 'admin_head', 'wp_admin_canonical_url' );
44
+ } );
45
+
46
+ add_filter( 'admin_viewport_meta', function ( $meta ) {
47
+ $meta .= ',maximum-scale=1';
48
+
49
+ return $meta;
50
+ } );
51
+
52
+ add_action( 'itsec-page-show', function () {
53
+ require_once ITSEC_Core::get_core_dir() . 'admin-pages/sidebar-widget.php';
54
+ require_once ITSEC_Core::get_core_dir() . 'deprecated/module-settings.php';
55
+ do_action( 'itsec-settings-page-init' );
56
+
57
+ $server_type = ITSEC_Lib::get_server();
58
+ $onboard = ITSEC_Modules::get_setting( 'global', 'onboard_complete' );
59
+
60
+ printf(
61
+ '<div id="itsec-settings-root" data-server-type="%s" data-install-type="%s" data-onboard="%s"></div>',
62
+ esc_attr( $server_type ),
63
+ esc_attr( ITSEC_Core::get_install_type() ),
64
+ esc_attr( $onboard )
65
+ );
66
+ } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/admin-pages/sidebar-widget.php CHANGED
@@ -13,7 +13,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
13
  * @var string
14
  */
15
  protected $id = '';
16
-
17
  /**
18
  * User-friendly display title for the widget.
19
  *
@@ -21,7 +21,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
21
  * @var string
22
  */
23
  protected $title = '';
24
-
25
  /**
26
  * Array of default values for the form inputs.
27
  *
@@ -29,7 +29,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
29
  * @var array
30
  */
31
  protected $defaults = array();
32
-
33
  /**
34
  * Array of WP_Error objects.
35
  *
@@ -39,7 +39,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
39
  * @var array
40
  */
41
  protected $errors = array();
42
-
43
  /**
44
  * Array or status or update messages.
45
  *
@@ -76,6 +76,8 @@ class ITSEC_Settings_Page_Sidebar_Widget {
76
  * @access public
77
  */
78
  public function __construct() {
 
 
79
  add_action( 'itsec-settings-page-register-widgets', array( $this, 'register' ), $this->priority );
80
  add_action( 'itsec-logs-page-register-widgets', array( $this, 'register' ), $this->priority );
81
  }
@@ -111,18 +113,18 @@ class ITSEC_Settings_Page_Sidebar_Widget {
111
  trigger_error( get_class( $this ) . " has not set the $name variable.", E_USER_ERROR );
112
  }
113
  }
114
-
115
  do_action( 'itsec-settings-page-register-widget', $this );
116
  do_action( 'itsec-logs-page-register-widget', $this );
117
  }
118
-
119
  /**
120
  * Allow the widget to enqueue widget-specific scripts and styles.
121
  *
122
  * @access public
123
  */
124
  public function enqueue_scripts_and_styles() {}
125
-
126
  /**
127
  * Allow a widget to process an AJAX request.
128
  *
@@ -136,7 +138,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
136
  * @param array $data Array of data sent by the AJAX request.
137
  */
138
  public function handle_ajax_request( $data ) {}
139
-
140
  /**
141
  * Return the default settings for the widget.
142
  *
@@ -150,7 +152,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
150
  public function get_defaults() {
151
  return $this->defaults;
152
  }
153
-
154
  /**
155
  * Render the settings form.
156
  *
@@ -161,7 +163,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
161
  * @param object ITSEC_Form object used to create inputs.
162
  */
163
  public function render( $form ) {}
164
-
165
  /**
166
  * Process form input by calling validate and save member functions.
167
  *
@@ -174,12 +176,12 @@ class ITSEC_Settings_Page_Sidebar_Widget {
174
  */
175
  public function handle_form_post( $data ) {
176
  $data = $this->validate( $data );
177
-
178
  if ( ! is_null( $data ) ) {
179
  $this->save( $data );
180
  }
181
  }
182
-
183
  /**
184
  * Validate form input data.
185
  *
@@ -197,7 +199,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
197
  protected function validate( $data ) {
198
  return $data;
199
  }
200
-
201
  /**
202
  * Save the validated form input data.
203
  *
@@ -211,7 +213,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
211
  protected function save( $data ) {
212
  ITSEC_Storage::set( $this->id, $data );
213
  }
214
-
215
  /**
216
  * Returns the errors array.
217
  *
@@ -224,7 +226,7 @@ class ITSEC_Settings_Page_Sidebar_Widget {
224
  public function get_errors() {
225
  return $this->errors;
226
  }
227
-
228
  /**
229
  * Returns the messages array.
230
  *
13
  * @var string
14
  */
15
  protected $id = '';
16
+
17
  /**
18
  * User-friendly display title for the widget.
19
  *
21
  * @var string
22
  */
23
  protected $title = '';
24
+
25
  /**
26
  * Array of default values for the form inputs.
27
  *
29
  * @var array
30
  */
31
  protected $defaults = array();
32
+
33
  /**
34
  * Array of WP_Error objects.
35
  *
39
  * @var array
40
  */
41
  protected $errors = array();
42
+
43
  /**
44
  * Array or status or update messages.
45
  *
76
  * @access public
77
  */
78
  public function __construct() {
79
+ _deprecated_function( __METHOD__, '7.0.1' );
80
+
81
  add_action( 'itsec-settings-page-register-widgets', array( $this, 'register' ), $this->priority );
82
  add_action( 'itsec-logs-page-register-widgets', array( $this, 'register' ), $this->priority );
83
  }
113
  trigger_error( get_class( $this ) . " has not set the $name variable.", E_USER_ERROR );
114
  }
115
  }
116
+
117
  do_action( 'itsec-settings-page-register-widget', $this );
118
  do_action( 'itsec-logs-page-register-widget', $this );
119
  }
120
+
121
  /**
122
  * Allow the widget to enqueue widget-specific scripts and styles.
123
  *
124
  * @access public
125
  */
126
  public function enqueue_scripts_and_styles() {}
127
+
128
  /**
129
  * Allow a widget to process an AJAX request.
130
  *
138
  * @param array $data Array of data sent by the AJAX request.
139
  */
140
  public function handle_ajax_request( $data ) {}
141
+
142
  /**
143
  * Return the default settings for the widget.
144
  *
152
  public function get_defaults() {
153
  return $this->defaults;
154
  }
155
+
156
  /**
157
  * Render the settings form.
158
  *
163
  * @param object ITSEC_Form object used to create inputs.
164
  */
165
  public function render( $form ) {}
166
+
167
  /**
168
  * Process form input by calling validate and save member functions.
169
  *
176
  */
177
  public function handle_form_post( $data ) {
178
  $data = $this->validate( $data );
179
+
180
  if ( ! is_null( $data ) ) {
181
  $this->save( $data );
182
  }
183
  }
184
+
185
  /**
186
  * Validate form input data.
187
  *
199
  protected function validate( $data ) {
200
  return $data;
201
  }
202
+
203
  /**
204
  * Save the validated form input data.
205
  *
213
  protected function save( $data ) {
214
  ITSEC_Storage::set( $this->id, $data );
215
  }
216
+
217
  /**
218
  * Returns the errors array.
219
  *
226
  public function get_errors() {
227
  return $this->errors;
228
  }
229
+
230
  /**
231
  * Returns the messages array.
232
  *
core/container.php CHANGED
@@ -2,6 +2,8 @@
2
 
3
  namespace iThemesSecurity;
4
 
 
 
5
  use ITSEC_Lib_Upgrader;
6
  use Pimple\Container;
7
  use wpdb;
@@ -34,6 +36,10 @@ return static function ( Container $c ) {
34
  return [];
35
  };
36
 
 
 
 
 
37
  $c[ Ban_Hosts\Multi_Repository::class ] = static function ( Container $c ) {
38
  return new Ban_Hosts\Multi_Repository(
39
  ...array_map( [ $c, 'offsetGet' ], $c['ban-hosts.repositories'] )
@@ -54,4 +60,47 @@ return static function ( Container $c ) {
54
  );
55
  };
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  };
2
 
3
  namespace iThemesSecurity;
4
 
5
+ use iThemesSecurity\Lib\REST;
6
+ use iThemesSecurity\Lib\Site_Types;
7
  use ITSEC_Lib_Upgrader;
8
  use Pimple\Container;
9
  use wpdb;
36
  return [];
37
  };
38
 
39
+ $c['dashboard.cards'] = static function () {
40
+ return [];
41
+ };
42
+
43
  $c[ Ban_Hosts\Multi_Repository::class ] = static function ( Container $c ) {
44
  return new Ban_Hosts\Multi_Repository(
45
  ...array_map( [ $c, 'offsetGet' ], $c['ban-hosts.repositories'] )
60
  );
61
  };
62
 
63
+ $c[ Site_Types\Registry::class ] = static function () {
64
+ return ( new Site_Types\Registry() )
65
+ ->register( new Site_Types\Type\Ecommerce() )
66
+ ->register( new Site_Types\Type\Network() )
67
+ ->register( new Site_Types\Type\Non_Profit() )
68
+ ->register( new Site_Types\Type\Blog() )
69
+ ->register( new Site_Types\Type\Portfolio() )
70
+ ->register( new Site_Types\Type\Brochure() );
71
+ };
72
+
73
+ $c[ Site_Types\Defaults::class ] = static function () {
74
+ return new Site_Types\Defaults();
75
+ };
76
+
77
+ $c[ Lib\Tools\Tools_Registry::class ] = static function () {
78
+ return new Lib\Tools\Tools_Registry();
79
+ };
80
+
81
+ $c[ Lib\Tools\Tools_Runner::class ] = static function ( Container $c ) {
82
+ return new Lib\Tools\Tools_Runner( $c[ Lib\Tools\Tools_Registry::class ] );
83
+ };
84
+
85
+ $c[ REST\Modules_Controller::class ] = static function () {
86
+ return new REST\Modules_Controller();
87
+ };
88
+
89
+ $c[ REST\Settings_Controller::class ] = static function () {
90
+ return new REST\Settings_Controller();
91
+ };
92
+
93
+ $c[ REST\Site_Types_Controller::class ] = static function ( Container $c ) {
94
+ return new REST\Site_Types_Controller(
95
+ $c[ Site_Types\Registry::class ],
96
+ $c[ Site_Types\Defaults::class ]
97
+ );
98
+ };
99
+
100
+ $c[ REST\Tools_Controller::class ] = static function ( Container $c ) {
101
+ return new REST\Tools_Controller(
102
+ $c[ Lib\Tools\Tools_Registry::class ],
103
+ $c[ Lib\Tools\Tools_Runner::class ]
104
+ );
105
+ };
106
  };
core/core.php CHANGED
@@ -1,6 +1,7 @@
1
  <?php
2
 
3
  use iThemesSecurity\User_Groups;
 
4
 
5
  /**
6
  * iThemes Security Core.
@@ -26,7 +27,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
26
  *
27
  * @access private
28
  */
29
- private $plugin_build = 4122;
30
 
31
  /**
32
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
@@ -149,10 +150,8 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
149
  add_action( 'plugins_loaded', array( $this, 'handle_upgrade' ), - 100, 0 );
150
  add_action( 'plugins_loaded', array( $this, 'continue_init' ), - 90 );
151
 
152
- add_action( 'itsec_scheduler_register_events', array( $this, 'register_events' ) );
153
  add_action( 'itsec_scheduled_clear-locks', array( 'ITSEC_Lib', 'delete_expired_locks' ) );
154
  add_action( 'itsec_scheduled_clear-tokens', array( ITSEC_Lib_Opaque_Tokens::class, 'delete_expired_tokens' ) );
155
- add_action( 'itsec_scheduled_flush-files', array( 'ITSEC_Files', 'flush_files' ) );
156
  add_action( 'itsec_before_import', function () {
157
  $this->importing = true;
158
  } );
@@ -232,7 +231,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
232
  return;
233
  }
234
 
235
- add_action( 'admin_notices', function () {
236
  echo '<div class="notice notice-error">';
237
  echo '<p>';
238
  esc_html_e( 'Cannot run iThemes Security. Error encountered during setup. Please try deactivating and reactivating iThemes Security. Contact support if the error persists.', 'better-wp-security' );
@@ -263,6 +262,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
263
 
264
  $this->setup_scheduler();
265
  ITSEC_Modules::run_active_modules();
 
266
 
267
  $this->login_interstitial = new ITSEC_Lib_Login_Interstitial();
268
  $this->login_interstitial->run();
@@ -327,6 +327,15 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
327
  return current_user_can( self::get_required_cap() );
328
  }
329
 
 
 
 
 
 
 
 
 
 
330
  /**
331
  * Retrieve the global instance of the files utility.
332
  *
@@ -435,69 +444,51 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
435
  $sync_api->register( 'itsec-get-everything', 'Ithemes_Sync_Verb_ITSEC_Get_Everything', dirname( __FILE__ ) . '/sync-verbs/itsec-get-everything.php' );
436
  }
437
 
438
- /**
439
- * Register events.
440
- *
441
- * @param ITSEC_Scheduler $scheduler
442
- */
443
- public function register_events( $scheduler ) {
444
- $scheduler->schedule( ITSEC_Scheduler::S_DAILY, 'clear-locks' );
445
- $scheduler->schedule( ITSEC_Scheduler::S_DAILY, 'health-check' );
446
- $scheduler->schedule( ITSEC_Scheduler::S_DAILY, 'clear-tokens' );
447
- $scheduler->schedule( ITSEC_Scheduler::S_HOURLY, 'flush-files' );
448
- }
449
-
450
  /**
451
  * Register core modules.
452
  */
453
  public function register_modules() {
454
  $path = dirname( __FILE__ );
455
 
456
- ITSEC_Modules::register_module( 'feature-flags', "$path/modules/feature-flags", 'always-active' );
457
- ITSEC_Modules::register_module( 'security-check', "$path/modules/security-check", 'always-active' );
458
- ITSEC_Modules::register_module( 'global', "$path/modules/global", 'always-active' );
459
- ITSEC_Modules::register_module( 'notification-center', "$path/modules/notification-center", 'always-active' );
460
- ITSEC_Modules::register_module( 'user-groups', "$path/modules/user-groups", 'always-active' );
461
- ITSEC_Modules::register_module( 'privacy', "$path/modules/privacy", 'always-active' );
462
- ITSEC_Modules::register_module( '404-detection', "$path/modules/404-detection" );
463
- ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user", 'always-active' );
464
- ITSEC_Modules::register_module( 'away-mode', "$path/modules/away-mode" );
465
- ITSEC_Modules::register_module( 'ban-users', "$path/modules/ban-users", 'default-active' );
466
  include( "$path/modules/ban-users/init.php" ); // Provides the itsec_ban_users_handle_new_blacklisted_ip function which is always needed.
467
- ITSEC_Modules::register_module( 'content-directory', "$path/modules/content-directory", 'always-active' );
468
- ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix", 'always-active' );
469
- ITSEC_Modules::register_module( 'backup', "$path/modules/backup", 'default-active' );
470
- ITSEC_Modules::register_module( 'core', "$path/modules/core", 'always-active' );
471
- ITSEC_Modules::register_module( 'email-confirmation', "$path/modules/email-confirmation", 'always-active' );
472
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
473
- ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions", 'always-active' );
474
- ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend", 'always-active' );
475
- ITSEC_Modules::register_module( 'brute-force', "$path/modules/brute-force", 'default-active' );
 
476
 
477
- if ( is_multisite() ) {
478
- ITSEC_Modules::register_module( 'multisite-tweaks', "$path/modules/multisite-tweaks" );
479
  }
480
 
481
- ITSEC_Modules::register_module( 'network-brute-force', "$path/modules/ipcheck", 'default-active' );
482
 
483
- if ( ! defined( 'ITSEC_DISABLE_PASSWORD_REQUIREMENTS' ) || ! ITSEC_DISABLE_PASSWORD_REQUIREMENTS ) {
484
- ITSEC_Modules::register_module( 'password-requirements', "$path/modules/password-requirements/", 'always-active' );
485
  }
486
 
487
- ITSEC_Modules::register_module( 'ssl', "$path/modules/ssl" );
488
- ITSEC_Modules::register_module( 'strong-passwords', "$path/modules/strong-passwords", 'always-active' );
 
489
  ITSEC_Modules::register_module( 'system-tweaks', "$path/modules/system-tweaks" );
490
- ITSEC_Modules::register_module( 'wordpress-salts', "$path/modules/salts", 'always-active' );
491
- ITSEC_Modules::register_module( 'wordpress-tweaks', "$path/modules/wordpress-tweaks", 'default-active' );
492
- ITSEC_Modules::register_module( 'file-writing', "$path/modules/file-writing", 'always-active' );
493
- ITSEC_Modules::register_module( 'malware', "$path/modules/malware", 'always-active' );
494
- ITSEC_Modules::register_module( 'security-check-pro', "$path/modules/security-check-pro", self::is_pro() ? 'always-active' : 'default-inactive' );
495
- ITSEC_Modules::register_module( 'sync-connect', "$path/modules/sync-connect", 'always-active' );
496
- ITSEC_Modules::register_module( 'site-scanner', "$path/modules/site-scanner", 'always-active' );
497
-
498
- if ( ! ITSEC_Core::is_pro() ) {
499
- ITSEC_Modules::register_module( 'pro-module-upsells', "$path/modules/pro", 'always-active' );
500
- }
501
  }
502
 
503
  /**
@@ -575,27 +566,33 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
575
 
576
  $this->doing_data_upgrade = true;
577
 
578
- require_once( self::get_core_dir() . '/setup.php' );
579
  self::get_instance()->setup_error = ITSEC_Setup::handle_upgrade( $build );
580
 
581
  return self::get_instance()->setup_error;
582
  }
583
 
584
  public static function handle_activation() {
585
- require_once( self::get_core_dir() . '/setup.php' );
586
  self::get_instance()->setup_error = ITSEC_Setup::handle_activation();
587
  }
588
 
589
  public static function handle_deactivation() {
590
- require_once( self::get_core_dir() . '/setup.php' );
591
  ITSEC_Setup::handle_deactivation();
592
  }
593
 
594
  public static function handle_uninstall() {
595
- require_once( self::get_core_dir() . '/setup.php' );
596
  ITSEC_Setup::handle_uninstall();
597
  }
598
 
 
 
 
 
 
 
599
  /**
600
  * Register a notice to be displayed in the WordPress admin.
601
  *
@@ -604,27 +601,6 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
604
  */
605
  public static function add_notice( $callback, $all_pages = false ) {
606
  _deprecated_function( __METHOD__, '6.0.0', 'ITSEC_Lib_Admin_Notices::register' );
607
-
608
- global $pagenow, $plugin_page;
609
-
610
- if ( ! $all_pages && ! in_array( $pagenow, array( 'plugins.php', 'update-core.php' ) ) && ( ! isset( $plugin_page ) || ! in_array( $plugin_page, array( 'itsec', 'itsec-logs' ) ) ) ) {
611
- return;
612
- }
613
-
614
- $self = self::get_instance();
615
-
616
- if ( ! $self->notices_loaded ) {
617
- wp_enqueue_style( 'itsec-notice', plugins_url( 'core/css/itsec_notice.css', ITSEC_Core::get_core_dir() ), array(), '20160609' );
618
- wp_enqueue_script( 'itsec-notice', plugins_url( 'core/js/itsec-notice.js', ITSEC_Core::get_core_dir() ), array(), '20160512' );
619
-
620
- $self->notices_loaded = true;
621
- }
622
-
623
- if ( is_multisite() ) {
624
- add_action( 'network_admin_notices', $callback );
625
- } else {
626
- add_action( 'admin_notices', $callback );
627
- }
628
  }
629
 
630
  public static function get_plugin_file() {
@@ -702,6 +678,19 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
702
  return is_dir( self::get_plugin_dir() . 'pro' );
703
  }
704
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  /**
706
  * Is this an actively licensed Pro installation.
707
  *
@@ -831,19 +820,59 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
831
  }
832
 
833
  public static function get_backup_creation_page_url() {
834
- $url = network_admin_url( 'admin.php?page=itsec&module=backup' );
835
-
836
- $url = apply_filters( 'itsec-filter-backup-creation-page-url', $url );
837
 
838
- return $url;
839
  }
840
 
841
- public static function get_security_check_page_url() {
842
- return network_admin_url( 'admin.php?page=itsec&module=security-check' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  }
844
 
845
  public static function get_settings_module_url( $module ) {
846
- return network_admin_url( 'admin.php?page=itsec&module=' . $module );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
  }
848
 
849
  /**
@@ -867,37 +896,30 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
867
  return $self->interactive;
868
  }
869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  /**
871
  * Determine whether the current request is an Infinite WP API call.
872
  *
873
  * @return bool
874
  */
875
  public static function is_iwp_call() {
876
- $self = self::get_instance();
877
-
878
- if ( isset( $self->is_iwp_call ) ) {
879
- return $self->is_iwp_call;
880
- }
881
-
882
-
883
- $self->is_iwp_call = false;
884
-
885
- if ( false && ! ITSEC_Modules::get_setting( 'global', 'infinitewp_compatibility' ) ) {
886
- return false;
887
- }
888
-
889
-
890
- $post_data = @file_get_contents( 'php://input' );
891
-
892
- if ( ! empty( $post_data ) ) {
893
- $data = base64_decode( $post_data );
894
-
895
- if ( false !== strpos( $data, 's:10:"iwp_action";' ) ) {
896
- $self->is_iwp_call = true;
897
- }
898
- }
899
-
900
- return $self->is_iwp_call;
901
  }
902
 
903
  /**
@@ -1108,6 +1130,15 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
1108
  return false;
1109
  }
1110
 
 
 
 
 
 
 
 
 
 
1111
  /**
1112
  * Check to see if the define to disable all active modules is set.
1113
  *
1
  <?php
2
 
3
  use iThemesSecurity\User_Groups;
4
+ use iThemesSecurity\Lib;
5
 
6
  /**
7
  * iThemes Security Core.
27
  *
28
  * @access private
29
  */
30
+ private $plugin_build = 4124;
31
 
32
  /**
33
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
150
  add_action( 'plugins_loaded', array( $this, 'handle_upgrade' ), - 100, 0 );
151
  add_action( 'plugins_loaded', array( $this, 'continue_init' ), - 90 );
152
 
 
153
  add_action( 'itsec_scheduled_clear-locks', array( 'ITSEC_Lib', 'delete_expired_locks' ) );
154
  add_action( 'itsec_scheduled_clear-tokens', array( ITSEC_Lib_Opaque_Tokens::class, 'delete_expired_tokens' ) );
 
155
  add_action( 'itsec_before_import', function () {
156
  $this->importing = true;
157
  } );
231
  return;
232
  }
233
 
234
+ add_action( 'all_admin_notices', function () {
235
  echo '<div class="notice notice-error">';
236
  echo '<p>';
237
  esc_html_e( 'Cannot run iThemes Security. Error encountered during setup. Please try deactivating and reactivating iThemes Security. Contact support if the error persists.', 'better-wp-security' );
262
 
263
  $this->setup_scheduler();
264
  ITSEC_Modules::run_active_modules();
265
+ ITSEC_Modules::get_container()->get( Lib\Tools\Tools_Runner::class )->run();
266
 
267
  $this->login_interstitial = new ITSEC_Lib_Login_Interstitial();
268
  $this->login_interstitial->run();
327
  return current_user_can( self::get_required_cap() );
328
  }
329
 
330
+ /**
331
+ * Checks if the user has completed the onboarding process.
332
+ *
333
+ * @return bool
334
+ */
335
+ public static function is_onboarded() {
336
+ return ITSEC_Modules::get_setting( 'global', 'onboard_complete' );
337
+ }
338
+
339
  /**
340
  * Retrieve the global instance of the files utility.
341
  *
444
  $sync_api->register( 'itsec-get-everything', 'Ithemes_Sync_Verb_ITSEC_Get_Everything', dirname( __FILE__ ) . '/sync-verbs/itsec-get-everything.php' );
445
  }
446
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  /**
448
  * Register core modules.
449
  */
450
  public function register_modules() {
451
  $path = dirname( __FILE__ );
452
 
453
+ ITSEC_Modules::register_module( 'feature-flags', "$path/modules/feature-flags" );
454
+ ITSEC_Modules::register_module( 'user-groups', "$path/modules/user-groups" );
455
+ ITSEC_Modules::register_module( 'global', "$path/modules/global" );
456
+ ITSEC_Modules::register_module( 'notification-center', "$path/modules/notification-center" );
457
+ ITSEC_Modules::register_module( 'privacy', "$path/modules/privacy" );
458
+ ITSEC_Modules::register_module( 'dashboard', "$path/modules/dashboard" );
459
+ ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user" );
460
+ ITSEC_Modules::register_module( 'ban-users', "$path/modules/ban-users" );
 
 
461
  include( "$path/modules/ban-users/init.php" ); // Provides the itsec_ban_users_handle_new_blacklisted_ip function which is always needed.
462
+ ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix" );
463
+ ITSEC_Modules::register_module( 'core', "$path/modules/core" );
464
+ ITSEC_Modules::register_module( 'promos', "$path/modules/promos" );
465
+ ITSEC_Modules::register_module( 'email-confirmation', "$path/modules/email-confirmation" );
 
466
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
467
+ ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions" );
468
+ ITSEC_Modules::register_module( 'file-writing', "$path/modules/file-writing" );
469
+ ITSEC_Modules::register_module( 'brute-force', "$path/modules/brute-force" );
470
+ ITSEC_Modules::register_module( 'network-brute-force', "$path/modules/network-brute-force" );
471
 
472
+ if ( ! defined( 'ITSEC_DISABLE_PASSWORD_REQUIREMENTS' ) || ! ITSEC_DISABLE_PASSWORD_REQUIREMENTS ) {
473
+ ITSEC_Modules::register_module( 'password-requirements', "$path/modules/password-requirements/" );
474
  }
475
 
476
+ ITSEC_Modules::register_module( 'ssl', "$path/modules/ssl" );
477
 
478
+ if ( ! defined( 'BACKUPBUDDY_PLUGIN_FILE' ) || ( defined( 'ITSEC_ENABLE_BACKUPS' ) && ITSEC_ENABLE_BACKUPS ) ) {
479
+ ITSEC_Modules::register_module( 'backup', "$path/modules/backup" );
480
  }
481
 
482
+ ITSEC_Modules::register_module( 'two-factor', "$path/modules/two-factor" );
483
+ ITSEC_Modules::register_module( 'strong-passwords', "$path/modules/strong-passwords" );
484
+ ITSEC_Modules::register_module( 'hibp', "$path/modules/hibp" );
485
  ITSEC_Modules::register_module( 'system-tweaks', "$path/modules/system-tweaks" );
486
+ ITSEC_Modules::register_module( 'wordpress-salts', "$path/modules/salts" );
487
+ ITSEC_Modules::register_module( 'wordpress-tweaks', "$path/modules/wordpress-tweaks" );
488
+ ITSEC_Modules::register_module( 'security-check-pro', "$path/modules/security-check-pro" );
489
+ ITSEC_Modules::register_module( 'sync-connect', "$path/modules/sync-connect" );
490
+ ITSEC_Modules::register_module( 'site-scanner', "$path/modules/site-scanner" );
491
+ ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend" );
 
 
 
 
 
492
  }
493
 
494
  /**
566
 
567
  $this->doing_data_upgrade = true;
568
 
569
+ self::load_setup();
570
  self::get_instance()->setup_error = ITSEC_Setup::handle_upgrade( $build );
571
 
572
  return self::get_instance()->setup_error;
573
  }
574
 
575
  public static function handle_activation() {
576
+ self::load_setup();
577
  self::get_instance()->setup_error = ITSEC_Setup::handle_activation();
578
  }
579
 
580
  public static function handle_deactivation() {
581
+ self::load_setup();
582
  ITSEC_Setup::handle_deactivation();
583
  }
584
 
585
  public static function handle_uninstall() {
586
+ self::load_setup();
587
  ITSEC_Setup::handle_uninstall();
588
  }
589
 
590
+ private static function load_setup() {
591
+ if ( ! class_exists( 'ITSEC_Setup' ) ) {
592
+ require_once( self::get_core_dir() . 'setup.php' );
593
+ }
594
+ }
595
+
596
  /**
597
  * Register a notice to be displayed in the WordPress admin.
598
  *
601
  */
602
  public static function add_notice( $callback, $all_pages = false ) {
603
  _deprecated_function( __METHOD__, '6.0.0', 'ITSEC_Lib_Admin_Notices::register' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  }
605
 
606
  public static function get_plugin_file() {
678
  return is_dir( self::get_plugin_dir() . 'pro' );
679
  }
680
 
681
+ /**
682
+ * Gets the installation type.
683
+ *
684
+ * @return string
685
+ */
686
+ public static function get_install_type() {
687
+ if ( self::is_pro() ) {
688
+ return 'pro';
689
+ }
690
+
691
+ return 'free';
692
+ }
693
+
694
  /**
695
  * Is this an actively licensed Pro installation.
696
  *
820
  }
821
 
822
  public static function get_backup_creation_page_url() {
823
+ $url = self::get_settings_module_url( 'backup' );
 
 
824
 
825
+ return apply_filters( 'itsec-filter-backup-creation-page-url', $url );
826
  }
827
 
828
+ public static function get_settings_module_route( $module ) {
829
+ $path = '/settings/configure/';
830
+ $config = ITSEC_Modules::get_config( $module );
831
+
832
+ if ( $config ) {
833
+ $settings = ITSEC_Modules::get_settings_obj( $module );
834
+
835
+ if ( ! $settings || ! $settings->has_interactive_settings() ) {
836
+ return "/settings/modules/{$config->get_type()}#{$config->get_id()}";
837
+ }
838
+
839
+ if ( $config->get_type() !== 'recommended' ) {
840
+ $path .= $config->get_type() . '/';
841
+ }
842
+
843
+ $path .= $config->get_id();
844
+ }
845
+
846
+ return $path;
847
  }
848
 
849
  public static function get_settings_module_url( $module ) {
850
+ $path = self::get_settings_module_route( $module );
851
+
852
+ return network_admin_url( 'admin.php?page=itsec&path=' . urlencode( $path ) );
853
+ }
854
+
855
+ public static function get_url_for_settings_route( $path ) {
856
+ return network_admin_url( 'admin.php?page=itsec&path=' . urlencode( $path ) );
857
+ }
858
+
859
+ public static function get_link_for_settings_route( $path ) {
860
+ $url = self::get_url_for_settings_route( $path );
861
+
862
+ return sprintf( '<a href="%s" data-itsec-path="%s">', esc_attr( $url ), esc_attr( $path ) );
863
+ }
864
+
865
+ /**
866
+ * Gets the URL for the Security Check page.
867
+ *
868
+ * @deprecated 7.0.0
869
+ *
870
+ * @return string
871
+ */
872
+ public static function get_security_check_page_url() {
873
+ _deprecated_function( __METHOD__, '7.0.0' );
874
+
875
+ return self::get_settings_page_url();
876
  }
877
 
878
  /**
896
  return $self->interactive;
897
  }
898
 
899
+ /**
900
+ * Runs a callback with the given interactivity settings.
901
+ *
902
+ * @param bool $interactive Whether to process the callback in interactive mode.
903
+ * @param callable $callback The callback to execute.
904
+ *
905
+ * @return mixed The return value from callback.
906
+ */
907
+ public static function with_interactivity( bool $interactive, callable $callback ) {
908
+ $current = self::is_interactive();
909
+ self::set_interactive( $interactive );
910
+ $r = $callback();
911
+ self::set_interactive( $current );
912
+
913
+ return $r;
914
+ }
915
+
916
  /**
917
  * Determine whether the current request is an Infinite WP API call.
918
  *
919
  * @return bool
920
  */
921
  public static function is_iwp_call() {
922
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  }
924
 
925
  /**
1130
  return false;
1131
  }
1132
 
1133
+ /**
1134
+ * Checks if iThemes Security is in development mode.
1135
+ *
1136
+ * @return bool
1137
+ */
1138
+ public static function is_development() {
1139
+ return defined( 'ITSEC_DEVELOPMENT' ) && ITSEC_DEVELOPMENT;
1140
+ }
1141
+
1142
  /**
1143
  * Check to see if the define to disable all active modules is set.
1144
  *
core/css/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/css/ithemes.css DELETED
@@ -1,637 +0,0 @@
1
- #screen-meta-links #itsec-meta-link-wrap a::after {
2
- content : '';
3
- margin-right : 5px;
4
- }
5
-
6
- #itsec-meta-link-wrap a.show-settings {
7
- float : right;
8
- margin : 0 0 0 6px;
9
- }
10
-
11
- .wrap h2.nav-tab-wrapper {
12
- padding-bottom: 0;
13
- }
14
-
15
- div.inside ul {
16
- margin-left : 20px;
17
- }
18
-
19
- div.inside ul li {
20
- line-height : 16px;
21
- list-style : disc;
22
- }
23
-
24
- .form-table th, .form-table td label {
25
- font-weight : 600;
26
- }
27
-
28
- #poststuff h2.settings-section-header {
29
- padding : 1em 0 0;
30
- }
31
-
32
- .current-time-date {
33
- display : inline-block;
34
- font-size : 1.25em;
35
- background : #efefef;
36
- padding : .75em 1em;
37
- }
38
-
39
- .ui-datepicker {
40
- display : none;
41
- }
42
-
43
- a.itsec_return_to_top {
44
- position : fixed;
45
- bottom : 10px;
46
- right : 10px;
47
- z-index : 200;
48
- width : 41px;
49
- height : 41px;
50
- background : url(../img/return-to-top.png) center center no-repeat;
51
- display : block;
52
- text-indent : -9999em;
53
- opacity : .8;
54
- }
55
-
56
- a.itsec_return_to_top:hover {
57
- opacity : 1;
58
- }
59
-
60
- #global_table_of_contents div.inside {
61
- padding-top : 6px;
62
- }
63
-
64
- #global_table_of_contents .hndle,
65
- #global_table_of_contents .handlediv {
66
- display : none;
67
- }
68
-
69
- #global_table_of_contents.fixed {
70
- position : fixed;
71
- right : 0;
72
- bottom : 45%;
73
- z-index : 2;
74
- }
75
-
76
- div.inside .itsec_toc_label {
77
- font-weight : 600;
78
- font-size : 14px;
79
- }
80
-
81
- div.inside .itsec_toc {
82
- margin : 0;
83
- }
84
-
85
- div.inside .itsec_toc li {
86
- list-style : none;
87
- }
88
-
89
- #side-sortables.empty-container {
90
- border : none;
91
- }
92
-
93
- .ui-dialog {
94
- position : fixed !important;
95
- }
96
-
97
- /***************************************
98
- Warnings/Notices
99
- ****************************************/
100
- .itsec-warning-message {
101
- display : block;
102
- clear : both;
103
- font-size : 1.125em;
104
- margin : 1.5em 0;
105
- padding : 1em;
106
- border-left : 4px solid #ca8383;
107
- background : #ffd4d4;
108
- color : #9c3e3e;
109
- }
110
-
111
- .itsec-notice-message {
112
- display : block;
113
- clear : both;
114
- font-size : 1.125em;
115
- margin : 1.5em 0;
116
- padding : 1em;
117
- border-left : 4px solid #e8e4a7;
118
- background : #fff9ec;
119
- }
120
-
121
- .itsec-notice-message span,
122
- .itsec-warning-message span {
123
- font-weight : 700;
124
- }
125
-
126
- /***************************************
127
- Support Page List
128
- ****************************************/
129
- .inside .itsec-support {
130
- margin : 0;
131
- }
132
-
133
- .inside .itsec-support li {
134
- list-style : none;
135
- }
136
-
137
- .inside .itsec-support li h4 {
138
- background : #ebebeb;
139
- padding : 1em;
140
- margin-bottom : 0;
141
- }
142
-
143
- .inside .itsec-support li ul {
144
- border : 1px solid #ebebeb;
145
- margin : 0;
146
- }
147
-
148
- .inside .itsec-support li li {
149
- padding : .5em 1em .5em 2em;
150
- margin : 0;
151
- }
152
-
153
- .inside .itsec-support li li:nth-child(2n+1) {
154
- background : #fafafa;
155
- }
156
-
157
- /***************************************
158
- Dashboard information
159
- ****************************************/
160
- #itsec_get_started .inside {
161
- padding : 0;
162
- margin : 0;
163
- }
164
-
165
- #itsec_get_started .itsec-video {
166
- position : relative;
167
- padding-bottom : 48.5%;
168
- padding-top : 25px;
169
- height : 0;
170
- width : 85%;
171
- margin : 1em auto 0 auto;
172
- }
173
-
174
- #itsec_get_started .itsec-video iframe {
175
- position : absolute;
176
- top : 0;
177
- left : 0;
178
- width : 100%;
179
- height : 100%;
180
- }
181
-
182
- .itsec_getting_started {
183
- padding : 0 2em;
184
- overflow : hidden;
185
- }
186
-
187
- .itsec_getting_started .column {
188
- width : 100%;
189
- max-width : 45%;
190
- float : left;
191
- padding-right : 2.5%;
192
- border-right : 1px solid #ebebeb;
193
- padding-bottom : 2em;
194
- }
195
-
196
- .itsec_getting_started .column.two {
197
- margin-right : 0;
198
- padding-right : 0;
199
- padding-left : 2.5%;
200
- border : 0;
201
- max-width : 49%;
202
- }
203
-
204
- .itsec_getting_started .itsec-video-link {
205
- width : 100%;
206
- max-width : 200px;
207
- float : left;
208
- margin : 0 1.5em 1.5em 0;
209
- }
210
-
211
- .itsec_getting_started .itsec-video-link img {
212
- max-width : 100%;
213
- height : auto;
214
- }
215
-
216
- .itsec_getting_started .itsec-video-description {
217
-
218
- }
219
-
220
- .itsec_getting_started .itsec_video {
221
- display : none;
222
- }
223
-
224
- .itsec-video-dialog .itsec_video {
225
- padding : 0;
226
- margin : 0 0 -5px 0;
227
- }
228
-
229
- .itsec-video-dialog .ui-dialog-titlebar {
230
- line-height : 1;
231
- font-size : 1em;
232
- height : 35px;
233
- background : #222;
234
- border-bottom : none;
235
- }
236
-
237
- .itsec-video-dialog .ui-dialog-titlebar button {
238
- height : 30px;
239
- width : 30px;
240
- padding : 0;
241
- }
242
-
243
- #itsec_tabbed_dashboard_content {
244
- padding : 0 1em;
245
- }
246
-
247
- #itsec_tabbed_dashboard_content h2 {
248
- margin-bottom : 0;
249
- }
250
-
251
- #itsec_tabbed_dashboard_content .itsec-tabs {
252
- overflow : hidden;
253
- padding : 0;
254
- margin-top : 2em;
255
- margin-bottom : 2em;
256
- }
257
-
258
- #itsec_tabbed_dashboard_content .itsec-tabs li {
259
- display : block;
260
- float : left;
261
- padding : 0;
262
- border : 0;
263
- border-right : 1px solid #ebebeb;
264
- }
265
-
266
- #itsec_tabbed_dashboard_content .itsec-tabs li a {
267
- display : block;
268
- padding : 1em 2em;
269
- text-decoration : none;
270
- background : #fff;
271
- border-bottom : 3px solid transparent;
272
- }
273
-
274
- #itsec_tabbed_dashboard_content .itsec-tabs li.ui-state-active a,
275
- #itsec_tabbed_dashboard_content .itsec-tabs li.ui-state-hover a {
276
- background : #ebebeb;
277
- border-bottom : 3px solid #025680;
278
- }
279
-
280
- #itsec_tabbed_dashboard_content .statuslist {
281
- margin-bottom : 4em;
282
- }
283
-
284
- #itsec_tabbed_dashboard_content .statuslist li {
285
- padding : .75em 1em;
286
- }
287
-
288
- #itsec_tabbed_dashboard_content .statuslist.high-priority {
289
- border : 1px solid #ffd4d4;
290
- }
291
-
292
- #itsec_tabbed_dashboard_content .statuslist.high-priority li {
293
- background : #ffeaea url(../img/flag16-red.png) 12px center no-repeat;
294
- padding-left : 40px;
295
- border-bottom : 1px solid #ffd4d4;
296
- }
297
-
298
- #itsec_tabbed_dashboard_content .statuslist.medium-priority {
299
- border : 1px solid #dadaa4;
300
- }
301
-
302
- #itsec_tabbed_dashboard_content .statuslist.medium-priority li {
303
- background : #ffffcb url(../img/flag16-yellow.png) 12px center no-repeat;
304
- padding-left : 40px;
305
- border-left : 4px solid #dadaa4;
306
- border-bottom : 1px solid #dadaa4;
307
- }
308
-
309
- #itsec_tabbed_dashboard_content .statuslist.low-priority {
310
- border : 1px solid #a6cbdb;
311
- }
312
-
313
- #itsec_tabbed_dashboard_content .statuslist.low-priority li {
314
- background : #daf4ff url(../img/flag16-blue.png) 12px center no-repeat;
315
- padding-left : 40px;
316
- border-left : 4px solid #a6cbdb;
317
- border-bottom : 1px solid #a6cbdb;
318
- }
319
-
320
- #itsec_tabbed_dashboard_content .statuslist.completed li {
321
- background : #fafafa url(../img/check16.png) 12px center no-repeat;
322
- padding-left : 40px;
323
- }
324
-
325
- #itsec_tabbed_dashboard_content .statuslist.high-priority li:last-child,
326
- #itsec_tabbed_dashboard_content .statuslist.medium-priority li:last-child,
327
- #itsec_tabbed_dashboard_content .statuslist.low-priority li:last-child {
328
- border-bottom : 0;
329
- }
330
-
331
- div.itsec_rewrite_rules {
332
- height : 500px;
333
- overflow : auto;
334
- width : 100%;
335
- line-height : 1em;
336
- }
337
-
338
- div.itsec_rewrite_rules code {
339
- overflow-x : auto; /* Use horizontal scroller if needed; for Firefox 2, not needed in Firefox 3 */
340
- overflow-y : hidden;
341
- background-color : transparent;
342
- white-space : pre-wrap; /* css-3 */
343
- white-space : -moz-pre-wrap !important; /* Mozilla, since 1999 */
344
- white-space : -pre-wrap; /* Opera 4-6 */
345
- white-space : -o-pre-wrap; /* Opera 7 */
346
- /* width: 99%; */
347
- word-wrap : break-word; /* Internet Explorer 5.5+ */
348
- }
349
-
350
- /***************************************
351
- Dashboard Security Status Lists
352
- ****************************************/
353
- #itsec_status ul {
354
- list-style-position : inside;
355
- margin : 1em 0;
356
- border : 1px solid #ebebeb;
357
- padding : 0;
358
- }
359
-
360
- #itsec_status ul li {
361
- padding : .5em 1em;
362
- margin : 0;
363
- border-bottom : 1px solid #ebebeb;
364
- overflow : hidden;
365
- }
366
-
367
- #itsec_status ul li:last-child {
368
- border-bottom : none;
369
- }
370
-
371
- #itsec_status ul.completed li {
372
- border-left : 5px solid #c9c9c9;
373
- }
374
-
375
- #itsec_status ul.high-priority li {
376
- border-left : 5px solid #ffd4d4;
377
- }
378
-
379
- #itsec_status ul.medium-priority li {
380
- border-left : 5px solid #d9f4ff;
381
- }
382
-
383
- #itsec_status ul.low-priority li {
384
- border-left : 5px solid #d9f4ff;
385
- }
386
-
387
- #itsec_status ul li p {
388
- float : left;
389
- width : 75%;
390
- margin : 0;
391
- padding : .3em 0;
392
- }
393
-
394
- #itsec_status ul li .itsec_status_action {
395
- float : right;
396
- width : 20%;
397
- margin-left : 5%;
398
- text-align : right;
399
- }
400
-
401
- #normal-sortables #itsec_release_lockout_form p.submit {
402
- float : none;
403
- padding-left : 0;
404
- margin : 1.5em 0 0;
405
- padding-top : 1.5em;
406
- }
407
-
408
- /***************************************
409
- iThemes Security Dialogs
410
- ****************************************/
411
-
412
- /* Logs Modal */
413
- .itsec-dialog-logs .ui-dialog-content {
414
- padding : 1em 2em 2em 2em;
415
- }
416
-
417
- .itsec-dialog-logs .ui-dialog-titlebar span {
418
- padding : .25em 1em;
419
- text-align : left;
420
- }
421
-
422
- /* All Logs Modal */
423
- .itsec-all-log-dialog ul li h3 {
424
- border-bottom : 1px solid #ebebeb;
425
- text-transform : capitalize;
426
- }
427
-
428
- .itsec-all-log-dialog ul ul li h3 {
429
- border : none;
430
- text-transform : none;
431
- font-size : 1.2em;
432
- margin : 0;
433
- }
434
-
435
- .itsec-all-log-dialog ul ul li {
436
- padding : .5em 1em;
437
- }
438
-
439
- .itsec-all-log-dialog ul ul ul li,
440
- .itsec-all-log-dialog ul ul ul ul li {
441
- padding : 0;
442
- }
443
-
444
- .itsec-all-log-dialog ul ul li:nth-child(2n) {
445
- background : #ebebeb;
446
- }
447
-
448
- .itsec-all-log-dialog ul ul ul li:nth-child(2n),
449
- .itsec-all-log-dialog ul ul ul ul li:nth-child(2n) {
450
- background : transparent;
451
- }
452
-
453
- /* Initial setup Modal */
454
- .itsec-setup-dialog .ui-dialog-titlebar {
455
- background : #2ea2cc;
456
- color : #fff;
457
- font-size : 1.5em;
458
- font-weight : normal;
459
- height : auto;
460
- line-height : 2.5;
461
- padding : 0 1em;
462
- }
463
-
464
- .itsec-setup-dialog .ui-dialog-titlebar span {
465
- text-align : left;
466
- }
467
-
468
- .itsec-setup-dialog .ui-dialog-titlebar button {
469
- width : 30px;
470
- height : 30px;
471
- color : #5bbdd5;
472
- top : 8px;
473
- right : 15px;
474
- margin : 0;
475
- padding : 0;
476
- }
477
-
478
- .itsec-setup-dialog .ui-dialog-titlebar button:before {
479
- color : #5bbdd5;
480
- }
481
-
482
- .itsec-setup-dialog .ui-dialog-titlebar button:hover:before {
483
- color : #fff;
484
- }
485
-
486
- .itsec-setup-dialog .ui-icon,
487
- .itsec-setup-dialog .ui-icon:hover {
488
- background : none !important;
489
- }
490
-
491
- #itsec_intro_modal {
492
- padding : 0;
493
- margin : 0;
494
- }
495
-
496
- #itsec_intro_modal li {
497
- border-bottom : 1px solid #ebebeb;
498
- padding : 1em;
499
- overflow : hidden;
500
- list-style : none;
501
- list-style-position : inside;
502
- }
503
-
504
- #itsec_intro_modal li p {
505
- margin : 0;
506
- }
507
-
508
- #itsec_intro_modal li h4 {
509
- margin : 0;
510
- }
511
-
512
- #itsec_intro_modal .itsec-intro-close {
513
- position : relative;
514
- display : block;
515
- width : 98%;
516
- line-height : 3;
517
- text-align : right;
518
- color : #2ea2cc;
519
- }
520
-
521
- #itsec_intro_modal .itsec_tooltip_success {
522
- display : block;
523
- padding : .45em 1em;
524
- margin-top : .25em;
525
- border : 1px solid #c9c9c9;
526
- background : #fafafa url(../img/check16.png) 12px center no-repeat;
527
- padding-left : 40px;
528
- }
529
-
530
- #itsec_intro_modal .itsec_tooltip_failure {
531
- display : block;
532
- padding : .45em 1em;
533
- margin-top : .25em;
534
- border : 1px solid #dadaa4;
535
- background : #ffffcb url(../img/flag16-yellow.png) 12px center no-repeat;
536
- padding-left : 40px;
537
- }
538
-
539
- /* Malware & File Change status message */
540
- #itsec_malware_scan_status,
541
- #itsec_file_change_status {
542
- display : none;
543
- }
544
-
545
- #itsec_malware_scan_status p,
546
- #itsec_file_change_status p {
547
- padding : 5px 10px 5px 30px;
548
- border : 1px solid #a8d657;
549
- background : #e2edce url(../img/green-check16.png) 10px center no-repeat;
550
- color : #435523;
551
- }
552
-
553
- /***************************************
554
- Media Queries
555
- ****************************************/
556
-
557
- @media only screen and (max-width : 1280px) {
558
- #itsec_get_started .itsec-video {
559
- width : 100%;
560
- padding-bottom : 56.25%;
561
- }
562
-
563
- }
564
-
565
- @media only screen and (max-width : 850px) {
566
- #post-body.columns-2 #postbox-container-1 {
567
- margin-left : 0;
568
- max-width : 100%;
569
- }
570
-
571
- #global_table_of_contents.fixed {
572
- bottom : 0;
573
- margin : 0;
574
- width : 100%;
575
- }
576
-
577
- }
578
-
579
- @media only screen and (max-width : 600px) {
580
- .itsec_getting_started {
581
- padding : 0;
582
- }
583
-
584
- .itsec_getting_started .button-primary {
585
- margin-bottom : 1em;
586
- }
587
-
588
- .itsec_getting_started .column {
589
- max-width : 100%;
590
- float : none;
591
- padding-right : 2em;
592
- padding-left : 2em;
593
- padding-bottom : 2em;
594
- padding-left : 2em;
595
- border-bottom : 1px solid #ebebeb;
596
- border-right : 0;
597
- box-sizing : border-box;
598
- overflow : hidden;
599
- }
600
-
601
- .itsec_getting_started .column.two {
602
- padding-right : 2em;
603
- padding-left : 2em;
604
- border : 0;
605
- max-width : 100%;
606
- }
607
-
608
- h2.nav-tab-wrapper {
609
- border-bottom : 0;
610
- padding : 10px 0 3px 0;
611
- }
612
-
613
- h2.nav-tab-wrapper .nav-tab.nav-tab-active,
614
- h2.nav-tab-wrapper .nav-tab {
615
- border-bottom : 1px solid #ccc;
616
- margin : 2px;
617
- }
618
- }
619
-
620
- @media only screen and (max-width : 400px) {
621
- #itsec_tabbed_dashboard_content .itsec-tabs li {
622
- float : none;
623
- width : 100%;
624
- }
625
-
626
- #itsec_tabbed_dashboard_content .itsec-tabs li a {
627
- border-left : 3px solid transparent;
628
- border-bottom : 0;
629
- }
630
-
631
- #itsec_tabbed_dashboard_content .itsec-tabs li.ui-state-active a,
632
- #itsec_tabbed_dashboard_content .itsec-tabs li.ui-state-hover a {
633
- border-left : 3px solid #025680;
634
- border-bottom : 0;
635
- }
636
- }
637
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/css/itsec_notice.css DELETED
@@ -1,94 +0,0 @@
1
- .updated.itsec-notice {
2
- margin: 1em 0;
3
- position: relative;
4
- font-size: 14px;
5
- line-height: 1.6;
6
- -webkit-border-radius: 3px;
7
- -moz-border-radius: 3px;
8
- border-radius: 3px;
9
- color: #3e85b5;
10
- border: 1px solid #9fd0f1;
11
- border-bottom-width: 3px;
12
- background: #e3eef6;
13
- -webkit-box-shadow: none;
14
- -moz-box-shadow: none;
15
- box-shadow: none;
16
- padding: 10px 60px 10px 10px !important;
17
- }
18
-
19
- .updated.itsec-notice p {
20
- font-size: 14px;
21
- }
22
-
23
- .updated.itsec-notice span.it-icon-itsec {
24
- font-size: 2em;
25
- line-height: .1;
26
- vertical-align: middle;
27
- margin: 0;
28
- margin-right: .5em;
29
- padding: 0;
30
- display: inline;
31
- }
32
-
33
- .updated.itsec-notice .itsec-notice-button {
34
- background: #9fd0f1;
35
- border: 1px solid #79afd3;
36
- border-bottom-width: 2px;
37
- border-radius: 3px;
38
- display: inline-block;
39
- margin: 4px 10px;
40
- padding: 10px 18px;
41
- -webkit-transition: all .1s linear;
42
- -moz-transition: all .1s linear;
43
- transition: all .1s linear;
44
- color: #0071bc;
45
- font-weight: bold;
46
- text-decoration: none;
47
- position: inherit;
48
- }
49
-
50
- .itsec-two-factor-notice .itsec-notice-button.itsec-notice-hide {
51
- background-color: #C9DEEC;
52
- color: #7F9DB1;
53
- border-color: #9FBCD0;
54
- }
55
-
56
- .itsec_notice_text {
57
- display: block;
58
- margin: 10px 0 10px 0;
59
- }
60
-
61
- .itsec-notice .itsec-notice-hide {
62
- border: 1px solid transparent;
63
- border-radius: 3px;
64
- padding: 7px 14px;
65
- -webkit-transition: all .1s linear;
66
- -moz-transition: all .1s linear;
67
- transition: all .1s linear;
68
- background: none;
69
- font-weight: bold;
70
- text-decoration: none;
71
- position: absolute;
72
- top: 50%;
73
- margin-top: -17px;
74
- right: 10px;
75
- }
76
-
77
- .itsec-notice .itsec-notice-hide:hover {
78
- background: #9fd0f1;
79
- border: 1px solid #79afd3;
80
- border-bottom-width: 2px;
81
- border-radius: 3px;
82
- cursor: pointer;
83
- }
84
-
85
- /* Specified after '.itsec-notice .itsec-notice-hide:hover' for preference on buttons with both .itsec-notice-hide and .itsec-notice-button */
86
- .itsec-notice .itsec-notice-button:hover {
87
- background: #eff7fd;
88
- cursor: pointer;
89
- }
90
-
91
- .itsec-notice a.itsec-notice-button {
92
- line-height: 15px;
93
- font-size: 14px;
94
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/deprecated/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/deprecated/module-settings.php ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * The iThemes Security Module Settings Page API parent class.
5
+ *
6
+ * @property-read string $id
7
+ * @property-read string $title
8
+ * @property-read string $description
9
+ * @property-read string $type
10
+ * @property-read string $pro
11
+ * @property-read bool $can_save
12
+ * @property-read bool $redraw_on_save
13
+ * @property-read bool $upsell
14
+ * @property-read string $upsell_url
15
+ * @property-read bool $information_only
16
+ * @property-read string $status
17
+ * @property-read string $documentation
18
+ */
19
+ class ITSEC_Module_Settings_Page {
20
+ /**
21
+ * Unique ID for the module.
22
+ *
23
+ * This is used to store the module's data and generate form inputs.
24
+ *
25
+ * @access protected
26
+ * @var string
27
+ */
28
+ protected $id = '';
29
+
30
+ /**
31
+ * User-friendly display title for the module.
32
+ *
33
+ * @access protected
34
+ * @var string
35
+ */
36
+ protected $title = '';
37
+
38
+ /**
39
+ * User-friendly display description for the module.
40
+ *
41
+ * @access protected
42
+ * @var string
43
+ */
44
+ protected $description = '';
45
+
46
+ /**
47
+ * Whether the module is categorized as additional or recommended.
48
+ *
49
+ * @access protected
50
+ * @var string
51
+ */
52
+ protected $type = 'recommended'; // "advanced" or "recommended"
53
+
54
+ /**
55
+ * Whether the settings require resaving after activation in order to fully-activate the module.
56
+ *
57
+ * @access protected
58
+ * @var boolean
59
+ */
60
+ protected $requires_resave_after_activation = false;
61
+
62
+ /**
63
+ * Whether the module is part of iThemes Security Pro or not.
64
+ *
65
+ * @access protected
66
+ * @var bool
67
+ */
68
+ protected $pro = false;
69
+
70
+ /**
71
+ * Whether the module settings can be saved.
72
+ *
73
+ * @access protected
74
+ * @var bool
75
+ */
76
+ protected $can_save = true;
77
+
78
+ /**
79
+ * Whether the module settings should be redrawn on save.
80
+ *
81
+ * @access protected
82
+ * @var bool
83
+ */
84
+ protected $redraw_on_save = false;
85
+
86
+ /**
87
+ * Whether the module is an iThemes Security Pro module being shown as an upsell or not.
88
+ *
89
+ * @access protected
90
+ * @var bool
91
+ */
92
+ protected $upsell = false;
93
+
94
+ /**
95
+ * URL to use for upsell if this is one.
96
+ *
97
+ * @access protected
98
+ * @var string
99
+ */
100
+ protected $upsell_url = 'https://ithemes.com/security/';
101
+
102
+ /**
103
+ * Whether the module is for informational purposes only - no settings, no actions
104
+ *
105
+ * @access protected
106
+ * @var bool
107
+ */
108
+ protected $information_only = false;
109
+
110
+ /**
111
+ * Set the module status to 'warning' to signal to the user it needs attention.
112
+ *
113
+ * @var string
114
+ */
115
+ protected $status = '';
116
+
117
+ /**
118
+ * Link to documentation for this module.
119
+ *
120
+ * @var string
121
+ */
122
+ protected $documentation = '';
123
+
124
+ /**
125
+ * Constructor.
126
+ *
127
+ * Register the module settings to register themselves on init. Each subclass should use the constructor to set the
128
+ * id, title, description, type, pro, can_save, and redraw_on_save properties to values specific to that module and then call
129
+ * parent::__construct().
130
+ *
131
+ * @access public
132
+ */
133
+ public function __construct() {
134
+ _deprecated_function( static::class . '::__construct', '7.0.0' );
135
+
136
+ add_action( 'itsec-settings-page-register-modules', array( $this, 'register' ) );
137
+ }
138
+
139
+ /**
140
+ * Make protected properties public read-only.
141
+ *
142
+ * This function should be left as-is in subclasses.
143
+ *
144
+ * @access public
145
+ *
146
+ * @param string $name Property to get.
147
+ *
148
+ * @return mixed Property.
149
+ */
150
+ public function __get( $name ) {
151
+ if ( in_array( $name, array( 'id', 'title', 'description', 'type', 'pro', 'can_save', 'redraw_on_save', 'upsell', 'upsell_url', 'information_only', 'status', 'documentation' ) ) ) {
152
+ return $this->$name;
153
+ }
154
+
155
+ trigger_error( 'Attempted to check invalid property: ' . get_class( $this ) . "->$name", E_USER_ERROR );
156
+ }
157
+
158
+ /**
159
+ * Register the module's settings with the settings page.
160
+ *
161
+ * This function should be left as-is in subclasses.
162
+ *
163
+ * @access public
164
+ */
165
+ public function register() {
166
+ foreach ( array( 'id', 'title', 'description' ) as $name ) {
167
+ if ( empty( $this->$name ) ) {
168
+ trigger_error( get_class( $this ) . " has not set the $name variable.", E_USER_ERROR );
169
+ }
170
+ }
171
+
172
+ do_action( 'itsec-settings-page-register-module', $this );
173
+ }
174
+
175
+ /**
176
+ * Allow the module to enqueue module-specific scripts and styles.
177
+ *
178
+ * @access public
179
+ */
180
+ public function enqueue_scripts_and_styles() {}
181
+
182
+ /**
183
+ * Allow a module to process an AJAX request.
184
+ *
185
+ * The module's implementation of this function can either handle all input manually or return a data structure to
186
+ * be returned by the module API. The module's Javascript can make use of the itsec_module_send_ajax_request()
187
+ * Javascript function in order to make the AJAX request. It has a request format of:
188
+ * itsecSettingsPage.sendModuleAJAXRequest( module, data, callback );
189
+ *
190
+ * @access public
191
+ *
192
+ * @param array $data Array of data sent by the AJAX request.
193
+ */
194
+ public function handle_ajax_request( $data ) {}
195
+
196
+ /**
197
+ * Return the settings for the module.
198
+ *
199
+ * @access public
200
+ *
201
+ * @return array List of settings.
202
+ */
203
+ public function get_settings() {
204
+ return ITSEC_Modules::get_settings( $this->id );
205
+ }
206
+
207
+ /**
208
+ * Render the module's settings content.
209
+ *
210
+ * This function should be left as-is in subclasses.
211
+ *
212
+ * @access public
213
+ *
214
+ * @param ITSEC_Form $form ITSEC_Form object used to create inputs.
215
+ */
216
+ public function render( $form ) {
217
+
218
+ $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'module' => $this->id ) );
219
+
220
+ ?>
221
+ <div class="itsec-settings-module-description">
222
+ <?php $this->render_description( $form ); ?>
223
+ </div>
224
+ <?php if ( $messages ) : ?>
225
+ <div class="itsec-settings-module-service-status">
226
+ <?php foreach ( $messages as $message ): ?>
227
+ <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
228
+ <p><?php echo $message['message']; ?></p>
229
+ </div>
230
+ <?php endforeach; ?>
231
+ </div>
232
+ <?php endif; ?>
233
+ <div class="itsec-settings-module-settings">
234
+ <?php $this->render_settings( $form ); ?>
235
+ </div>
236
+ <?php
237
+
238
+ }
239
+
240
+ /**
241
+ * Render the module description.
242
+ *
243
+ * The description is shown whether the module is active or not. Ensure that the description adequately informs the
244
+ * user of the value of the module without requiring them to see the actual settings.
245
+ *
246
+ * @access protected
247
+ *
248
+ * @param object $form ITSEC_Form object used to create inputs.
249
+ */
250
+ protected function render_description( $form ) {
251
+
252
+ ?>
253
+ <p>Example module description.</p>
254
+ <?php
255
+
256
+ }
257
+
258
+ /**
259
+ * Render the module settings.
260
+ *
261
+ * The inputs and input descriptions should be output in this function. This output is hidden when a module is
262
+ * deactivated.
263
+ *
264
+ * @access protected
265
+ *
266
+ * @param ITSEC_Form $form ITSEC_Form object used to create inputs.
267
+ */
268
+ protected function render_settings( $form ) {
269
+
270
+ ?>
271
+ <table class="form-table itsec-settings-section">
272
+ <tbody>
273
+ <tr>
274
+ <th><label>Setting 1</label></th>
275
+ <td>
276
+ <?php $form->add_text( 'setting_1' ); ?>
277
+ </td>
278
+ </tr>
279
+ <tr>
280
+ <th><label>Setting 2</label></th>
281
+ <td>
282
+ <?php $form->add_text( 'setting_2' ); ?>
283
+ </td>
284
+ </tr>
285
+ </tbody>
286
+ </table>
287
+ <?php
288
+
289
+ }
290
+
291
+ /**
292
+ * Process form input.
293
+ *
294
+ * This function should be left as-is in subclasses unless specific processing is required.
295
+ *
296
+ * @access public
297
+ *
298
+ * @param array $data Array of form inputs to be processed and stored.
299
+ */
300
+ public function handle_form_post( $data ) {
301
+ ITSEC_Modules::set_settings( $this->id, $data );
302
+ }
303
+
304
+ /**
305
+ * Returns the errors array.
306
+ *
307
+ * This function should be left as-is in subclasses.
308
+ *
309
+ * @access public
310
+ *
311
+ * @return array Array of WP_Error objects.
312
+ */
313
+ public function get_errors() {
314
+ $validator = ITSEC_Modules::get_validator( $this->id );
315
+
316
+ if ( is_null( $validator ) ) {
317
+ return array();
318
+ }
319
+
320
+ return $validator->get_errors();
321
+ }
322
+
323
+ /**
324
+ * Returns the messages array.
325
+ *
326
+ * This function should be left as-is in subclasses.
327
+ *
328
+ * @access public
329
+ *
330
+ * @return array Array of status or update messages.
331
+ */
332
+ public function get_messages() {
333
+ $validator = ITSEC_Modules::get_validator( $this->id );
334
+
335
+ if ( is_null( $validator ) ) {
336
+ return array();
337
+ }
338
+
339
+ return $validator->get_messages();
340
+ }
341
+ }
core/files.php CHANGED
@@ -42,8 +42,13 @@ final class ITSEC_Files {
42
  $result = ITSEC_Lib_Config_File::update_wp_config();
43
  $success = ! is_wp_error( $result );
44
 
45
- if ( $add_responses && is_wp_error( $result ) ) {
46
- ITSEC_Response::add_error( $result );
 
 
 
 
 
47
  }
48
 
49
  return $success;
@@ -58,11 +63,15 @@ final class ITSEC_Files {
58
 
59
  if ( $add_responses ) {
60
  if ( is_wp_error( $result ) ) {
 
61
  ITSEC_Response::add_error( $result );
 
 
 
62
 
63
- $file = ITSEC_Lib_Config_File::get_server_config_file_path();
64
- } else if ( 'nginx' === $server ) {
65
- ITSEC_Response::add_message( __( 'You must restart your NGINX server for the changes to take effect.', 'better-wp-security' ) );
66
  }
67
  }
68
 
42
  $result = ITSEC_Lib_Config_File::update_wp_config();
43
  $success = ! is_wp_error( $result );
44
 
45
+ if ( $add_responses ) {
46
+ if ( is_wp_error( $result ) ) {
47
+ ITSEC_Response::set_success( false );
48
+ ITSEC_Response::add_error( $result );
49
+ } else {
50
+ ITSEC_Response::set_success();
51
+ }
52
  }
53
 
54
  return $success;
63
 
64
  if ( $add_responses ) {
65
  if ( is_wp_error( $result ) ) {
66
+ ITSEC_Response::set_success( false );
67
  ITSEC_Response::add_error( $result );
68
+ ITSEC_Lib_Config_File::get_server_config_file_path();
69
+ } else {
70
+ ITSEC_Response::set_success();
71
 
72
+ if ( 'nginx' === $server ) {
73
+ ITSEC_Response::add_info( __( 'You must restart your NGINX server for the changes to take effect.', 'better-wp-security' ) );
74
+ }
75
  }
76
  }
77
 
core/img/alert16.png DELETED
Binary file
core/img/check16.png DELETED
Binary file
core/img/flag16-blue.png DELETED
Binary file
core/img/flag16-red.png DELETED
Binary file
core/img/flag16-yellow.png DELETED
Binary file
core/img/green-check16.png DELETED
Binary file
core/img/index.php CHANGED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/img/mail/index.php CHANGED
@@ -1 +1 @@
1
- <?php // Silence is golden
1
+ <?php // Silence is golden.
core/img/return-to-top.png DELETED
Binary file
core/js/admin-dashboard-footer.js DELETED
@@ -1,3 +0,0 @@
1
- if ( typeof postboxes !== 'undefined' ) {
2
- postboxes.add_postbox_toggles( pagenow );
3
- }
 
 
 
core/js/admin-dashboard.js DELETED
@@ -1,187 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( '#screen-meta-links' ).append(
4
- '<div id="itsec-meta-link-wrap" class="hide-if-no-js screen-meta-toggle">' +
5
- '<a href="' + itsec_dashboard.url + '" class="show-settings">' + itsec_dashboard.text + '</a>' +
6
- '</div>'
7
- );
8
-
9
- jQuery( '.itsec_toc_item_link' ).click( function ( event ) {
10
-
11
- event.preventDefault();
12
-
13
- var goto = jQuery( this ).attr( 'href' );
14
-
15
- jQuery( 'html, body' ).animate(
16
- {
17
- scrollTop: jQuery( goto ).offset().top
18
- },
19
- 1000
20
- );
21
-
22
- } );
23
-
24
- jQuery( '.dialog' ).click( function ( event ) {
25
-
26
- event.preventDefault();
27
-
28
- var target = jQuery( this ).attr( 'href' );
29
- var title = jQuery( this ).parents( '.inside' ).siblings( 'h3.hndle' ).children( 'span' ).text();
30
-
31
- jQuery( '#' + target ).dialog( {
32
- dialogClass : 'wp-dialog itsec-dialog itsec-dialog-logs',
33
- modal : true,
34
- closeOnEscape: true,
35
- title : title,
36
- height : ( jQuery( window ).height() * 0.8 ),
37
- width : ( jQuery( window ).width() * 0.8 ),
38
- open : function ( event, ui ) {
39
-
40
- jQuery( '.ui-widget-overlay' ).bind( 'click', function () {
41
- jQuery( this ).siblings( '.ui-dialog' ).find( '.ui-dialog-content' ).dialog( 'close' );
42
- } );
43
-
44
- }
45
-
46
- } );
47
-
48
- jQuery( '.ui-dialog :button' ).blur();
49
-
50
- } );
51
-
52
- jQuery( '.itsec-video-link' ).click( function ( event ) {
53
-
54
- event.preventDefault();
55
-
56
- var target = jQuery( this ).data( 'video-id' );
57
-
58
- jQuery( '.' + target ).dialog( {
59
- dialogClass : 'wp-dialog itsec-dialog itsec-video-dialog',
60
- modal : true,
61
- closeOnEscape: true,
62
- width : 'auto',
63
- resizable : false,
64
- draggable : false,
65
- create : function ( event, ui ) {
66
- jQuery( this ).css( "maxWidth", "853px" );
67
- },
68
- open : function ( event, ui ) {
69
-
70
- jQuery( '.ui-widget-overlay' ).bind( 'click', function () {
71
- jQuery( this ).siblings( '.ui-dialog' ).find( '.ui-dialog-content' ).dialog( 'close' );
72
- } );
73
-
74
- }
75
-
76
- } );
77
-
78
- jQuery( '.ui-dialog :button' ).blur();
79
-
80
- } );
81
-
82
- jQuery( '.itsec_return_to_top' ).click( function ( event ) {
83
-
84
- event.preventDefault();
85
-
86
- jQuery( 'html, body' ).animate( {
87
- scrollTop: jQuery( 'html, body' ).offset().top
88
- },
89
- 500
90
- );
91
-
92
- } );
93
-
94
- jQuery( function () {
95
- jQuery( "#itsec_tabbed_dashboard_content" ).tabs();
96
- } );
97
-
98
- var toc_fixed = false;
99
-
100
- jQuery( window ).scroll( function () {
101
-
102
- if ( jQuery( this ).scrollTop() >= 550 ) {
103
-
104
- if ( ! toc_fixed ) {
105
-
106
- toc_fixed = true;
107
- jQuery( '#global_table_of_contents' ).addClass( 'fixed' );
108
-
109
- }
110
-
111
- } else {
112
-
113
- if ( toc_fixed ) {
114
-
115
- toc_fixed = false;
116
- jQuery( '#global_table_of_contents' ).removeClass( 'fixed' );
117
-
118
- }
119
-
120
- }
121
-
122
- } );
123
-
124
- } );
125
-
126
- function itsec_toc_select( value ) {
127
-
128
- if ( value ) {
129
-
130
- if ( jQuery( value ).hasClass( 'closed' ) ) {
131
- jQuery( value ).removeClass( 'closed' );
132
- }
133
-
134
- jQuery( 'html, body' ).animate(
135
- {
136
- scrollTop: jQuery( value ).offset().top - 50
137
- },
138
- 500
139
- );
140
- }
141
-
142
- }
143
-
144
- if ( window.location.hash ) {
145
-
146
- var id = window.location.hash.substring( 1 );
147
-
148
- jQuery( window ).load( function () {
149
-
150
- var target_offset = jQuery( "#" + id ).offset();
151
- var target_top = target_offset.top;
152
- var scroll_target = jQuery( '#' + id ).parent().parent();
153
- var toggle_target = scroll_target.parents( '.postbox' );
154
-
155
- // open metabox if needed
156
- if ( toggle_target.hasClass( 'closed' ) ) {
157
- toggle_target.removeClass( 'closed' );
158
- }
159
-
160
- //scroll to setting and highlight it
161
- scroll_to_setting( scroll_target );
162
-
163
- } );
164
-
165
- function scroll_to_setting( scroll_target ) {
166
-
167
- var id = window.location.hash.substring( 1 );
168
- var target_offset = jQuery( "#" + id ).offset();
169
- var target_top = target_offset.top;
170
-
171
- jQuery( 'html, body' ).animate( { scrollTop: target_top - 100 }, 500 );
172
-
173
- jQuery( scroll_target ).animate( {
174
- backgroundColor: '#ffffcb'
175
- }, 1000 );
176
-
177
- setTimeout( function () {
178
-
179
- jQuery( scroll_target ).animate( {
180
- backgroundColor: '#fff'
181
- }, 1000 )
182
-
183
- }, 6000 );
184
-
185
- }
186
-
187
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/js/admin-global-settings.js DELETED
@@ -1,19 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( '#itsec_reset_log_location' ).click( function ( event ) {
4
-
5
- event.preventDefault();
6
-
7
- jQuery( '#itsec_global_log_location' ).val( itsec_global_settings.location );
8
-
9
- } );
10
-
11
- jQuery( '.itsec_add_ip_to_whitelist' ).click( function ( event ) {
12
-
13
- event.preventDefault();
14
-
15
- jQuery( '#itsec_global_lockout_white_list' ).val( jQuery( '#itsec_global_lockout_white_list' ).val() + jQuery( '.itsec_add_ip_to_whitelist' ).attr( 'href' ) );
16
-
17
- } );
18
-
19
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/js/admin-logs.js DELETED
@@ -1,22 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- var $_GET = {};
4
-
5
- document.location.search.replace( /\??(?:([^=]+)=([^&]*)&?)/g, function () {
6
- function decode( s ) {
7
- return decodeURIComponent( s.split( "+" ).join( " " ) );
8
- }
9
-
10
- $_GET[decode( arguments[1] )] = decode( arguments[2] );
11
- } );
12
-
13
- var uri = URI( window.location.href )
14
-
15
- jQuery( '#itsec_log_filter' ).on( 'change', function () {
16
-
17
- uri.removeSearch( 'itsec_log_filter' ).removeSearch( 'orderby' ).removeSearch( 'order' ).removeSearch( 'paged' ).addSearch( { itsec_log_filter: [this.value] } );
18
- window.location.replace( uri );
19
-
20
- } );
21
-
22
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/js/admin-modal.js DELETED
@@ -1,75 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- //setup the tooltip
4
- jQuery( '#itsec_intro_modal' ).dialog(
5
- {
6
- dialogClass : 'wp-dialog itsec-setup-dialog',
7
- modal : true,
8
- closeOnEscape: false,
9
- title : itsec_tooltip_text.title,
10
- width : '75%',
11
- resizable : false,
12
- draggable : false,
13
- close : function ( event, ui ) {
14
-
15
- var data = {
16
- action: 'itsec_tooltip_ajax',
17
- module: 'close',
18
- nonce : itsec_tooltip_text.nonce
19
- };
20
-
21
- //call the ajax
22
- jQuery.post( ajaxurl, data, function () {
23
-
24
- var url = window.location.href;
25
- url = url.substring( 0, url.lastIndexOf( "&" ) );
26
-
27
- window.location.replace( url );
28
-
29
- } );
30
-
31
- }
32
-
33
- }
34
- );
35
-
36
- jQuery( '.ui-dialog a' ).blur();
37
-
38
- jQuery( '.itsec-intro-close' ).click( function ( event ) {
39
- jQuery( '#itsec_intro_modal' ).dialog( 'close' );
40
- } );
41
-
42
- //process tooltip actions
43
- jQuery( '.itsec_tooltip_ajax' ).click( function ( event ) {
44
-
45
- event.preventDefault();
46
-
47
- var module = jQuery( this ).attr( 'href' );
48
- var caller = this;
49
-
50
- var data = {
51
- action: 'itsec_tooltip_ajax',
52
- module: module,
53
- nonce : itsec_tooltip_text.nonce
54
- };
55
-
56
- //let user know we're working
57
- jQuery( caller ).removeClass( 'itsec_tooltip_ajax button-primary' ).addClass( 'button-secondary' ).html( 'Working...' );
58
-
59
- //call the ajax
60
- jQuery.post( ajaxurl, data, function ( response ) {
61
-
62
- if ( response == 'true' ) {
63
-
64
- jQuery( caller ).replaceWith( '<span class="itsec_tooltip_success">' + itsec_tooltip_text.messages[module].success + '</span>' );
65
-
66
- } else {
67
-
68
- jQuery( caller ).replaceWith( '<span class="itsec_tooltip_failure">' + itsec_tooltip_text.messages[module].failure + '</span>' );
69
- }
70
-
71
- } );
72
-
73
- } );
74
-
75
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/js/itsec-notice.js DELETED
@@ -1,16 +0,0 @@
1
- jQuery(document).ready( function() {
2
- jQuery( '.itsec-notice' ).on( 'click', '.itsec-notice-hide', function( e ) {
3
- e.preventDefault();
4
-
5
- var $container = jQuery(this).parents( '.itsec-notice' );
6
-
7
- jQuery.post( ajaxurl, {
8
- 'action': 'itsec-dismiss-notice-' + jQuery(this).data( 'source' ),
9
- 'notice_nonce': jQuery(this).data( 'nonce' )
10
- }, function( response ) {
11
- if ( response.success ) {
12
- $container.hide();
13
- }
14
- } );
15
- } );
16
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/js/scrollTo.js DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * Copyright (c) 2007-2015 Ariel Flesler - aflesler ○ gmail • com | http://flesler.blogspot.com
3
- * Licensed under MIT
4
- * @author Ariel Flesler
5
- * @version 2.1.3
6
- */
7
- ;(function(f){"use strict";"function"===typeof define&&define.amd?define(["jquery"],f):"undefined"!==typeof module&&module.exports?module.exports=f(require("jquery")):f(jQuery)})(function($){"use strict";function n(a){return!a.nodeName||-1!==$.inArray(a.nodeName.toLowerCase(),["iframe","#document","html","body"])}function h(a){return $.isFunction(a)||$.isPlainObject(a)?a:{top:a,left:a}}var p=$.scrollTo=function(a,d,b){return $(window).scrollTo(a,d,b)};p.defaults={axis:"xy",duration:0,limit:!0};$.fn.scrollTo=function(a,d,b){"object"=== typeof d&&(b=d,d=0);"function"===typeof b&&(b={onAfter:b});"max"===a&&(a=9E9);b=$.extend({},p.defaults,b);d=d||b.duration;var u=b.queue&&1<b.axis.length;u&&(d/=2);b.offset=h(b.offset);b.over=h(b.over);return this.each(function(){function k(a){var k=$.extend({},b,{queue:!0,duration:d,complete:a&&function(){a.call(q,e,b)}});r.animate(f,k)}if(null!==a){var l=n(this),q=l?this.contentWindow||window:this,r=$(q),e=a,f={},t;switch(typeof e){case "number":case "string":if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(e)){e= h(e);break}e=l?$(e):$(e,q);case "object":if(e.length===0)return;if(e.is||e.style)t=(e=$(e)).offset()}var v=$.isFunction(b.offset)&&b.offset(q,e)||b.offset;$.each(b.axis.split(""),function(a,c){var d="x"===c?"Left":"Top",m=d.toLowerCase(),g="scroll"+d,h=r[g](),n=p.max(q,c);t?(f[g]=t[m]+(l?0:h-r.offset()[m]),b.margin&&(f[g]-=parseInt(e.css("margin"+d),10)||0,f[g]-=parseInt(e.css("border"+d+"Width"),10)||0),f[g]+=v[m]||0,b.over[m]&&(f[g]+=e["x"===c?"width":"height"]()*b.over[m])):(d=e[m],f[g]=d.slice&& "%"===d.slice(-1)?parseFloat(d)/100*n:d);b.limit&&/^\d+$/.test(f[g])&&(f[g]=0>=f[g]?0:Math.min(f[g],n));!a&&1<b.axis.length&&(h===f[g]?f={}:u&&(k(b.onAfterFirst),f={}))});k(b.onAfter)}})};p.max=function(a,d){var b="x"===d?"Width":"Height",h="scroll"+b;if(!n(a))return a[h]-$(a)[b.toLowerCase()]();var b="client"+b,k=a.ownerDocument||a.document,l=k.documentElement,k=k.body;return Math.max(l[h],k[h])-Math.min(l[b],k[b])};$.Tween.propHooks.scrollLeft=$.Tween.propHooks.scrollTop={get:function(a){return $(a.elem)[a.prop]()}, set:function(a){var d=this.get(a);if(a.options.interrupt&&a._last&&a._last!==d)return $(a.elem).stop();var b=Math.round(a.now);d!==b&&($(a.elem)[a.prop](b),a._last=this.get(a))}};return p});
 
 
 
 
 
 
 
core/js/tracking.js DELETED
@@ -1,154 +0,0 @@
1
- if ( typeof itsec_tracking_vars != 'undefined' ) {
2
-
3
- href = location.href;
4
-
5
- (function () {
6
- var ga = document.createElement( 'script' );
7
- ga.type = 'text/javascript';
8
- ga.async = true;
9
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
10
- var s = document.getElementsByTagName( 'script' )[0];
11
- s.parentNode.insertBefore( ga, s );
12
- })();
13
-
14
- var _gaq = _gaq || [];
15
- _gaq.push( ['_setAccount', 'UA-47645120-1'] );
16
-
17
- function itsec_get_vars( type, values ) {
18
-
19
- var data = {
20
- action: 'itsec_tracking_ajax',
21
- type : type,
22
- nonce : itsec_tracking_vars.nonce
23
- };
24
-
25
- if ( type != 'receive' ) {
26
- data.values = values;
27
- }
28
-
29
- jQuery.post( ajaxurl, data, function ( response ) {
30
-
31
- if ( response == 'false' ) {
32
-
33
- return false;
34
-
35
- } else {
36
-
37
- return true;
38
-
39
- }
40
-
41
- } );
42
-
43
- }
44
-
45
- jQuery( document ).ready( function () {
46
-
47
- var tracking_settings = itsec_tracking_vars.vars;
48
- var track_it = new Array();
49
- var timestamp = new Date().getTime();
50
-
51
- jQuery( '.itsec-settings-form' ).submit( function ( event ) {
52
-
53
- var values = jQuery( this ).serializeArray();
54
-
55
- jQuery.each( values, function ( name, value ) {
56
-
57
- var section = value.name.substring( 0, value.name.indexOf( '[' ) );
58
- var setting = value.name.substring( value.name.indexOf( '[' ) + 1, value.name.indexOf( ']' ) );
59
-
60
- if ( typeof tracking_settings[section] != 'undefined' && typeof tracking_settings[section][setting] != 'undefined' ) {
61
-
62
- var setting_value = tracking_settings[section][setting];
63
-
64
- var value_array = setting_value.split( ':' );
65
- var default_type = value_array[1];
66
-
67
- if ( default_type == 'b' && value.value == 1 ) {
68
-
69
- var saved_value = 'true';
70
-
71
- }
72
- else {
73
-
74
- if ( default_type == 'b' ) {
75
- var saved_value = 'false';
76
- }
77
- else {
78
- var saved_value = value.value;
79
- }
80
-
81
- }
82
-
83
- delete tracking_settings[section][setting];
84
-
85
- var item = new Object();
86
-
87
- item.section = section;
88
- item.setting = setting;
89
- item.value = saved_value;
90
-
91
- track_it.push( item );
92
-
93
- }
94
-
95
- } );
96
-
97
- jQuery.each( tracking_settings, function ( section, settings ) {
98
-
99
- var section = section;
100
-
101
- jQuery.each( tracking_settings[section], function ( setting, value ) {
102
-
103
- var value_array = value.split( ':' );
104
- var default_type = value_array[1];
105
- var default_value = value_array[0];
106
-
107
- if ( default_type == 'b' && default_value == 0 ) {
108
- var saved_value = 'false';
109
- } else if ( default_type == 'b' ) {
110
- var saved_value = 'true';
111
- } else {
112
- var saved_value = default_value;
113
- }
114
-
115
- var item = new Object();
116
-
117
- item.section = section;
118
- item.setting = setting;
119
- item.value = saved_value;
120
-
121
- track_it.push( item );
122
-
123
- } );
124
-
125
- } );
126
-
127
- var group_size = Math.ceil( track_it.length / 9 );
128
- var group_count = 1;
129
- var count = 0;
130
- var saved_values = '';
131
-
132
- jQuery.each( track_it, function ( setting, value ) {
133
-
134
- saved_value = ( value.section + '[' + value.setting + '] = ' + value.value + '; ')
135
- saved_values = saved_values.concat( saved_value );
136
-
137
- if ( count === group_size - 1 ) {
138
-
139
- _gaq.push( ['_trackEvent', 'ITSEC', 'Group ' + ( group_count ), saved_values, timestamp, true] );
140
- saved_values = '';
141
- count = 0;
142
- group_count ++;
143
-
144
- } else {
145
- count ++;
146
- }
147
-
148
- } );
149
-
150
- } );
151
-
152
- } );
153
-
154
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/lib.php CHANGED
@@ -202,7 +202,7 @@ final class ITSEC_Lib {
202
  *
203
  * @since 4.0.0
204
  *
205
- * @return string|bool server type the user is using of false if undetectable.
206
  */
207
  public static function get_server() {
208
  require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-utility.php' );
@@ -287,9 +287,9 @@ final class ITSEC_Lib {
287
  /**
288
  * Gets the list of banned IPs.
289
  *
 
290
  * @deprecated 6.7.0
291
  *
292
- * @return string[]
293
  */
294
  public static function get_blacklisted_ips() {
295
  _deprecated_function( __METHOD__, '6.7.0', \iThemesSecurity\Ban_Hosts\Multi_Repository::class );
@@ -310,12 +310,12 @@ final class ITSEC_Lib {
310
  /**
311
  * Determines whether a given IP address is blacklisted.
312
  *
313
- * @deprecated 6.7.0
314
- *
315
  * @param string $ip ip to check (can be in CIDR notation)
316
  * @param array $blacklisted_ips ip list to compare to if not yet saved to options
317
  *
318
  * @return boolean true if blacklisted or false
 
 
319
  */
320
  public static function is_ip_blacklisted( $ip = null, $blacklisted_ips = null ) {
321
  _deprecated_function( __METHOD__, '6.7.0', 'ITSEC_Lib::is_ip_banned' );
@@ -1008,6 +1008,28 @@ final class ITSEC_Lib {
1008
  return date_i18n( $format, strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp ) ) ) );
1009
  }
1010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1011
  /**
1012
  * Get the value of an option directly from the database, bypassing any caching.
1013
  *
@@ -1045,10 +1067,11 @@ final class ITSEC_Lib {
1045
  * @param array $array
1046
  * @param string $key
1047
  * @param mixed $default
 
1048
  *
1049
  * @return mixed
1050
  */
1051
- public static function array_get( $array, $key, $default = null ) {
1052
  if ( ! is_array( $array ) ) {
1053
  return $default;
1054
  }
@@ -1057,11 +1080,11 @@ final class ITSEC_Lib {
1057
  return $array[ $key ];
1058
  }
1059
 
1060
- if ( strpos( $key, '.' ) === false ) {
1061
  return isset( $array[ $key ] ) ? $array[ $key ] : $default;
1062
  }
1063
 
1064
- foreach ( explode( '.', $key ) as $segment ) {
1065
  if ( is_array( $array ) && isset( $array[ $segment ] ) ) {
1066
  $array = $array[ $segment ];
1067
  } else {
@@ -1102,6 +1125,28 @@ final class ITSEC_Lib {
1102
  return $array;
1103
  }
1104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  public static function print_r( $data, $args = array() ) {
1106
  require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' );
1107
 
@@ -1363,6 +1408,30 @@ final class ITSEC_Lib {
1363
  return key( $arr );
1364
  }
1365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1366
  /**
1367
  * Plucks a certain field out of each item in the list.
1368
  *
@@ -1417,6 +1486,24 @@ final class ITSEC_Lib {
1417
  return $default;
1418
  }
1419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1420
  /**
1421
  * Array unique implementation that allows for non-scalar values.
1422
  *
@@ -1425,16 +1512,21 @@ final class ITSEC_Lib {
1425
  * Keys are preserved. If a numeric array is given, the array will be re-indexed.
1426
  *
1427
  * @param array $array
 
1428
  *
1429
  * @return array
1430
  */
1431
- public static function non_scalar_array_unique( $array ) {
1432
 
1433
  $is_numeric = wp_is_numeric_array( $array );
1434
 
1435
  $hashes = array();
1436
 
1437
  foreach ( $array as $key => $value ) {
 
 
 
 
1438
  $hash = serialize( $value );
1439
 
1440
  if ( isset( $hashes[ $hash ] ) ) {
@@ -2218,13 +2310,18 @@ final class ITSEC_Lib {
2218
  ],
2219
  ],
2220
  ],
 
 
 
2221
  ],
2222
  ];
2223
 
2224
- $valid_requirements = rest_validate_value_from_schema( $requirements, $schema );
 
2225
 
2226
- if ( is_wp_error( $valid_requirements ) ) {
2227
- return $valid_requirements;
 
2228
  }
2229
 
2230
  $error = new WP_Error();
@@ -2238,10 +2335,18 @@ final class ITSEC_Lib {
2238
  if ( version_compare( ITSEC_Core::get_plugin_version(), $version, '<' ) ) {
2239
  $error->add(
2240
  'version',
2241
- sprintf( __( 'Must be running at least version %s of iThemes Security.', 'better-wp-security' ), $version )
2242
  );
2243
  }
2244
 
 
 
 
 
 
 
 
 
2245
  break;
2246
  }
2247
  }
@@ -2397,4 +2502,65 @@ final class ITSEC_Lib {
2397
 
2398
  return apply_filters( 'itsec_login_url', $url, $action, $redirect, $scheme );
2399
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2400
  }
202
  *
203
  * @since 4.0.0
204
  *
205
+ * @return string Server type the user is using. Falls back to 'apache'.
206
  */
207
  public static function get_server() {
208
  require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-utility.php' );
287
  /**
288
  * Gets the list of banned IPs.
289
  *
290
+ * @return string[]
291
  * @deprecated 6.7.0
292
  *
 
293
  */
294
  public static function get_blacklisted_ips() {
295
  _deprecated_function( __METHOD__, '6.7.0', \iThemesSecurity\Ban_Hosts\Multi_Repository::class );
310
  /**
311
  * Determines whether a given IP address is blacklisted.
312
  *
 
 
313
  * @param string $ip ip to check (can be in CIDR notation)
314
  * @param array $blacklisted_ips ip list to compare to if not yet saved to options
315
  *
316
  * @return boolean true if blacklisted or false
317
+ * @deprecated 6.7.0
318
+ *
319
  */
320
  public static function is_ip_blacklisted( $ip = null, $blacklisted_ips = null ) {
321
  _deprecated_function( __METHOD__, '6.7.0', 'ITSEC_Lib::is_ip_banned' );
1008
  return date_i18n( $format, strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp ) ) ) );
1009
  }
1010
 
1011
+ /**
1012
+ * Displays a human time diff, or a formatted date if the time is too far in the past.
1013
+ *
1014
+ * @param int $from Unix timestamp from which the difference begins.
1015
+ * @param int $to Optional. Unix timestamp to end the time difference. Default becomes time() if not set.
1016
+ * @param string $format The date format to use. If omitted, the configured date_format is used instead.
1017
+ *
1018
+ * @return string
1019
+ */
1020
+ public static function human_time_diff_or_date( int $from, int $to = 0, string $format = '' ): string {
1021
+ $to = $to ?: time();
1022
+ $diff = abs( $to - $from );
1023
+
1024
+ if ( $diff < DAY_IN_SECONDS ) {
1025
+ return sprintf( __( '%s ago' ), human_time_diff( $from, $to ) ); // Uses core translation
1026
+ }
1027
+
1028
+ $format = $format ?: get_option( 'date_format' );
1029
+
1030
+ return gmdate( $format, $from );
1031
+ }
1032
+
1033
  /**
1034
  * Get the value of an option directly from the database, bypassing any caching.
1035
  *
1067
  * @param array $array
1068
  * @param string $key
1069
  * @param mixed $default
1070
+ * @param string $delimeter
1071
  *
1072
  * @return mixed
1073
  */
1074
+ public static function array_get( $array, $key, $default = null, $delimeter = '.' ) {
1075
  if ( ! is_array( $array ) ) {
1076
  return $default;
1077
  }
1080
  return $array[ $key ];
1081
  }
1082
 
1083
+ if ( strpos( $key, $delimeter ) === false ) {
1084
  return isset( $array[ $key ] ) ? $array[ $key ] : $default;
1085
  }
1086
 
1087
+ foreach ( explode( $delimeter, $key ) as $segment ) {
1088
  if ( is_array( $array ) && isset( $array[ $segment ] ) ) {
1089
  $array = $array[ $segment ];
1090
  } else {
1125
  return $array;
1126
  }
1127
 
1128
+ /**
1129
+ * Merges two arrays recursively such that only arrays are deeply merged.
1130
+ *
1131
+ * @param array $array1
1132
+ * @param array $array2
1133
+ *
1134
+ * @return array
1135
+ */
1136
+ public static function array_merge_recursive_distinct( array $array1, array $array2 ): array {
1137
+ $merged = $array1;
1138
+
1139
+ foreach ( $array2 as $key => $value ) {
1140
+ if ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
1141
+ $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value );
1142
+ } else {
1143
+ $merged[ $key ] = $value;
1144
+ }
1145
+ }
1146
+
1147
+ return $merged;
1148
+ }
1149
+
1150
  public static function print_r( $data, $args = array() ) {
1151
  require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' );
1152
 
1408
  return key( $arr );
1409
  }
1410
 
1411
+ /**
1412
+ * Gets the first item from an array.
1413
+ *
1414
+ * @param array $arr
1415
+ * @param mixed $default
1416
+ *
1417
+ * @return mixed
1418
+ */
1419
+ public static function first( array $arr, $default = null ) {
1420
+ return $arr[ self::array_key_first( $arr ) ] ?? $default;
1421
+ }
1422
+
1423
+ /**
1424
+ * Gets the last item from an array.
1425
+ *
1426
+ * @param array $arr
1427
+ * @param mixed $default
1428
+ *
1429
+ * @return mixed
1430
+ */
1431
+ public static function last( array $arr, $default = null ) {
1432
+ return $arr[ self::array_key_last( $arr ) ] ?? $default;
1433
+ }
1434
+
1435
  /**
1436
  * Plucks a certain field out of each item in the list.
1437
  *
1486
  return $default;
1487
  }
1488
 
1489
+ /**
1490
+ * Finds the first item in a list matching the given predicate.
1491
+ *
1492
+ * @param iterable $list
1493
+ * @param callable $predicate
1494
+ *
1495
+ * @return mixed|null
1496
+ */
1497
+ public static function find_where( iterable $list, callable $predicate ) {
1498
+ foreach ( $list as $item ) {
1499
+ if ( $predicate( $item ) ) {
1500
+ return $item;
1501
+ }
1502
+ }
1503
+
1504
+ return null;
1505
+ }
1506
+
1507
  /**
1508
  * Array unique implementation that allows for non-scalar values.
1509
  *
1512
  * Keys are preserved. If a numeric array is given, the array will be re-indexed.
1513
  *
1514
  * @param array $array
1515
+ * @param bool $stabilize If true, stabilizes the values first according to JSON semantics.
1516
  *
1517
  * @return array
1518
  */
1519
+ public static function non_scalar_array_unique( $array, $stabilize = false ) {
1520
 
1521
  $is_numeric = wp_is_numeric_array( $array );
1522
 
1523
  $hashes = array();
1524
 
1525
  foreach ( $array as $key => $value ) {
1526
+ if ( $stabilize ) {
1527
+ $value = rest_stabilize_value( $value );
1528
+ }
1529
+
1530
  $hash = serialize( $value );
1531
 
1532
  if ( isset( $hashes[ $hash ] ) ) {
2310
  ],
2311
  ],
2312
  ],
2313
+ 'ssl' => [
2314
+ 'type' => 'boolean',
2315
+ ],
2316
  ],
2317
  ];
2318
 
2319
+ if ( ITSEC_Core::is_development() ) {
2320
+ $valid_requirements = rest_validate_value_from_schema( $requirements, $schema );
2321
 
2322
+ if ( is_wp_error( $valid_requirements ) ) {
2323
+ return $valid_requirements;
2324
+ }
2325
  }
2326
 
2327
  $error = new WP_Error();
2335
  if ( version_compare( ITSEC_Core::get_plugin_version(), $version, '<' ) ) {
2336
  $error->add(
2337
  'version',
2338
+ sprintf( __( 'You must be running at least version %s of iThemes Security.', 'better-wp-security' ), $version )
2339
  );
2340
  }
2341
 
2342
+ break;
2343
+ case 'ssl':
2344
+ if ( $requirement !== is_ssl() ) {
2345
+ $error->add(
2346
+ 'ssl',
2347
+ $requirement ? __( 'Your site must support SSL.', 'better-wp-security' ) : __( 'Your site must not support SSL.', 'better-wp-security' )
2348
+ );
2349
+ }
2350
  break;
2351
  }
2352
  }
2502
 
2503
  return apply_filters( 'itsec_login_url', $url, $action, $redirect, $scheme );
2504
  }
2505
+
2506
+ /**
2507
+ * Extends a service definition, ignoring if the service has been frozen.
2508
+ *
2509
+ * @param \Pimple\Container $c
2510
+ * @param string $id
2511
+ * @param callable $extend
2512
+ *
2513
+ * @return bool
2514
+ */
2515
+ public static function extend_if_able( \Pimple\Container $c, string $id, callable $extend ): bool {
2516
+ try {
2517
+ $c->extend( $id, $extend );
2518
+
2519
+ return true;
2520
+ } catch ( \Pimple\Exception\FrozenServiceException $e ) {
2521
+ return false;
2522
+ }
2523
+ }
2524
+
2525
+ /**
2526
+ * Resolve JSON Schema refs.
2527
+ *
2528
+ * @param array $schema
2529
+ *
2530
+ * @return array
2531
+ */
2532
+ public static function resolve_schema_refs( array $schema ): array {
2533
+ if ( isset( $schema['definitions'] ) ) {
2534
+ array_walk( $schema, [ static::class, 'resolve_ref' ], $schema['definitions'] );
2535
+ }
2536
+
2537
+ return $schema;
2538
+ }
2539
+
2540
+ /**
2541
+ * Resolves $ref entries at any point in the config.
2542
+ *
2543
+ * Currently, only a simplified form of JSON Pointers are supported where `/` is the only
2544
+ * allowed control character.
2545
+ *
2546
+ * Additionally, the `$ref` keyword must start with `#/definitions`.
2547
+ *
2548
+ * @param mixed $value The incoming value.
2549
+ * @param string $key The array key.
2550
+ * @param array $definitions The shared definitions.
2551
+ */
2552
+ private static function resolve_ref( &$value, $key, $definitions ) {
2553
+ if ( ! is_array( $value ) ) {
2554
+ return;
2555
+ }
2556
+
2557
+ if ( isset( $value['$ref'] ) ) {
2558
+ $ref = str_replace( '#/definitions/', '', $value['$ref'] );
2559
+ $value = \ITSEC_Lib::array_get( $definitions, $ref, null, '/' );
2560
+
2561
+ return;
2562
+ }
2563
+
2564
+ array_walk( $value, [ static::class, 'resolve_ref' ], $definitions );
2565
+ }
2566
  }
core/lib/Config_Password_Requirement.php ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib;
4
+
5
+ use iThemesSecurity\Module_Config;
6
+
7
+ abstract class Config_Password_Requirement implements Password_Requirement {
8
+
9
+ /** @var Module_Config */
10
+ private $config;
11
+
12
+ /** @var string */
13
+ private $code;
14
+
15
+ /**
16
+ * Config_Password_Requirement constructor.
17
+ *
18
+ * @param Module_Config $config
19
+ * @param string $code
20
+ */
21
+ public function __construct( Module_Config $config, string $code ) {
22
+ $this->config = $config;
23
+ $this->code = $code;
24
+ }
25
+
26
+ public function get_code(): string {
27
+ return $this->code;
28
+ }
29
+
30
+ public function get_module(): string {
31
+ return $this->config->get_id();
32
+ }
33
+
34
+ public function get_title(): string {
35
+ return $this->get_config()['title'] ?? $this->config->translate( Module_Config::T_ABOUT )->get_title();
36
+ }
37
+
38
+ public function get_description(): string {
39
+ return $this->get_config()['description'] ?? $this->config->translate( Module_Config::T_ABOUT )->get_description();
40
+ }
41
+
42
+ public function get_meta_key(): string {
43
+ return "_itsec_password_evaluation_{$this->get_code()}";
44
+ }
45
+
46
+ public function get_settings_schema(): array {
47
+ $schema = $this->get_config()['settings'] ?? [];
48
+
49
+ if ( $this->has_user_group() ) {
50
+ if ( ! $schema ) {
51
+ $schema['type'] = 'object';
52
+ $schema['properties'] = [];
53
+ }
54
+
55
+ $schema['properties']['group'] = [
56
+ 'type' => 'array',
57
+ 'items' => [
58
+ 'type' => 'string'
59
+ ],
60
+ 'default' => [],
61
+ 'readonly' => true,
62
+ ];
63
+
64
+ $schema['uiSchema'] = array_merge(
65
+ [
66
+ 'group' => [
67
+ 'ui:widget' => 'hidden',
68
+ ]
69
+ ],
70
+ $schema['uiSchema'] ?? []
71
+ );
72
+ }
73
+
74
+ if ( ! $schema ) {
75
+ return $schema;
76
+ }
77
+
78
+ $schema['title'] = $schema['title'] ?? $this->get_title();
79
+ $schema['description'] = $schema['description'] ?? $this->get_description();
80
+
81
+ $all_hidden = true;
82
+
83
+ foreach ( $schema['properties'] ?? [] as $property => $prop_schema ) {
84
+ if ( 'hidden' !== ( $schema['uiSchema'][ $property ]['ui:widget'] ?? '' ) ) {
85
+ $all_hidden = false;
86
+ break;
87
+ }
88
+ }
89
+
90
+ if ( $all_hidden ) {
91
+ $schema['uiSchema']['ui:widget'] = 'hidden';
92
+ }
93
+
94
+ return $schema;
95
+ }
96
+
97
+ public function has_user_group(): bool {
98
+ return ! empty( $this->get_config()['user-group'] );
99
+ }
100
+
101
+ private function get_config(): array {
102
+ return $this->config->translate( Module_Config::T_PASSWORD_REQUIREMENTS )
103
+ ->get_password_requirements()[ $this->get_code() ];
104
+ }
105
+ }
core/lib/Config_Settings.php ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity;
4
+
5
+ class Config_Settings extends \ITSEC_Settings {
6
+
7
+ public function __construct( Module_Config $config ) {
8
+ parent::__construct( $config );
9
+ }
10
+
11
+ public function get_id() {
12
+ return $this->config->get_id();
13
+ }
14
+
15
+ public function get_defaults() {
16
+ $defaults = [];
17
+
18
+ foreach ( $this->get_known_settings() as $setting ) {
19
+ $defaults[ $setting ] = $this->get_default( $setting );
20
+ }
21
+
22
+ return $defaults;
23
+ }
24
+
25
+ public function get_settings_schema() {
26
+ return $this->config->translate( Module_Config::T_SETTINGS )->get_settings();
27
+ }
28
+
29
+ public function get_known_settings() {
30
+ return array_keys( $this->config->get_settings()['properties'] );
31
+ }
32
+
33
+ public function is_known_setting( $setting ) {
34
+ return isset( $this->config->get_settings()['properties'][ $setting ] ) || in_array( $setting, $this->config->get_deprecated_settings(), true );
35
+ }
36
+
37
+ public function is_interactive_setting( $setting ) {
38
+ if ( ! empty( $this->config->get_settings()['properties'][ $setting ]['readonly'] ) ) {
39
+ return false;
40
+ }
41
+
42
+ if ( in_array( $setting, $this->config->get_deprecated_settings(), true ) ) {
43
+ return false;
44
+ }
45
+
46
+ return parent::is_interactive_setting( $setting );
47
+ }
48
+
49
+ public function is_conditional_setting( $setting ) {
50
+ $config = $this->config->get_conditional_settings();
51
+
52
+ return isset( $config[ $setting ] );
53
+ }
54
+
55
+ public function get_conditional_setting_config( $setting ) {
56
+ if ( ! $this->is_conditional_setting( $setting ) ) {
57
+ return [];
58
+ }
59
+
60
+ return $this->config->get_conditional_settings()[ $setting ];
61
+ }
62
+
63
+ public function get_default( $setting, $default = null ) {
64
+ if ( ! isset( $this->config->get_settings()['properties'][ $setting ] ) ) {
65
+ return $default;
66
+ }
67
+
68
+ $definition = $this->config->get_settings()['properties'][ $setting ];
69
+ $default = $definition['default'];
70
+
71
+ if ( 'object' === $definition['type'] ) {
72
+ foreach ( $definition['properties'] ?? [] as $property => $prop_schema ) {
73
+ $default[ $property ] = $prop_schema['default'] ?? $default[ $property ] ?? null;
74
+ }
75
+ }
76
+
77
+ return $default;
78
+ }
79
+
80
+ protected function after_save() {
81
+ parent::after_save();
82
+
83
+ \ITSEC_Core::get_scheduler()->register_events_for_config( $this->config );
84
+ }
85
+ }
core/lib/Config_Validator.php ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity;
4
+
5
+ class Config_Validator extends \ITSEC_Validator {
6
+
7
+ /**
8
+ * Config_Validator constructor.
9
+ *
10
+ * @param Module_Config $config
11
+ */
12
+ public function __construct( Module_Config $config ) {
13
+ parent::__construct( $config );
14
+
15
+ $this->run_validate_matching_types = false;
16
+ $this->vars_to_skip_validate_matching_fields = array_merge( $this->vars_to_skip_validate_matching_fields, $config->get_removed_settings() );
17
+ }
18
+
19
+ public function get_id() {
20
+ return $this->config->get_id();
21
+ }
22
+
23
+ protected function sanitize_settings() {
24
+ foreach ( $this->config->get_removed_settings() as $setting ) {
25
+ unset( $this->settings[ $setting ] );
26
+ }
27
+
28
+ $this->preserve_setting_if_exists( $this->config->get_deprecated_settings() );
29
+
30
+ if ( \ITSEC_Core::is_interactive() ) {
31
+ foreach ( $this->settings_obj->get_known_settings() as $setting ) {
32
+ if ( ! $this->settings_obj->is_interactive_setting( $setting ) ) {
33
+ $this->set_previous_if_missing( $setting );
34
+ }
35
+ }
36
+ }
37
+
38
+ foreach ( $this->settings_obj->get_conditional_settings() as $setting ) {
39
+ if ( $this->settings_obj->is_known_setting( $setting ) && ! $this->settings_obj->is_conditional_setting_active( $setting, $this->settings ) ) {
40
+ $this->set_previous_if_missing( $setting );
41
+ }
42
+ }
43
+ }
44
+
45
+ protected function validate_settings() {
46
+ if ( ! $this->can_save() ) {
47
+ return;
48
+ }
49
+
50
+ foreach ( $this->settings_obj->get_settings_schema()['properties'] as $setting => $schema ) {
51
+ $param = $schema['title'] ?? $setting;
52
+ $valid = rest_validate_value_from_schema( $this->settings[ $setting ], $schema, $param );
53
+
54
+ if ( is_wp_error( $valid ) ) {
55
+ $this->add_error( $valid );
56
+ $this->set_can_save( false );
57
+ continue;
58
+ }
59
+
60
+ $sanitized = rest_sanitize_value_from_schema( $this->settings[ $setting ], $schema, $param );
61
+
62
+ if ( is_wp_error( $sanitized ) ) {
63
+ $this->add_error( $sanitized );
64
+ $this->set_can_save( false );
65
+ }
66
+
67
+ $this->settings[ $setting ] = $sanitized;
68
+ }
69
+
70
+ foreach ( $this->config->get_user_groups() as $user_group => $config ) {
71
+ $valid = $this->validate_user_groups( $config['title'], $user_group );
72
+
73
+ if ( is_wp_error( $valid ) ) {
74
+ $this->add_error( $valid );
75
+ $this->set_can_save( false );
76
+ } else {
77
+ \ITSEC_Lib::array_set( $this->settings, $user_group, $valid );
78
+ }
79
+ }
80
+ }
81
+ }
core/lib/Legacy_Password_Requirement.php ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib;
4
+
5
+ final class Legacy_Password_Requirement implements Password_Requirement, \ArrayAccess, \IteratorAggregate {
6
+
7
+ /** @var string */
8
+ private $code;
9
+
10
+ /** @var array */
11
+ private $config;
12
+
13
+ /**
14
+ * Legacy_Password_Requirement constructor.
15
+ *
16
+ * @param string $code
17
+ * @param array $config
18
+ */
19
+ public function __construct( string $code, array $config ) {
20
+ $this->code = $code;
21
+ $this->config = $config;
22
+ }
23
+
24
+ public function get_code(): string {
25
+ return $this->code;
26
+ }
27
+
28
+ public function get_module(): string {
29
+ return '';
30
+ }
31
+
32
+ public function get_title(): string {
33
+ if ( ! $this->config['settings_config'] ) {
34
+ return $this->get_code();
35
+ }
36
+
37
+ return call_user_func( $this->config['settings_config'] )['label'] ?? $this->get_code();
38
+ }
39
+
40
+ public function get_description(): string {
41
+ if ( ! $this->config['settings_config'] ) {
42
+ return '';
43
+ }
44
+
45
+ return call_user_func( $this->config['settings_config'] )['description'];
46
+ }
47
+
48
+ public function is_password_change_required( \WP_User $user, array $settings ): bool {
49
+ if ( ! $this->config['flag_check'] ) {
50
+ return false;
51
+ }
52
+
53
+ return call_user_func( $this->config['flag_check'], $user, $settings );
54
+ }
55
+
56
+ public function evaluate( string $password, $user ) {
57
+ if ( ! $this->config['evaluate'] ) {
58
+ return new \WP_Error( 'not_implemented', __( 'This password requirement does not evaluate passwords.' ) );
59
+ }
60
+
61
+ return call_user_func( $this->config['evaluate'], $password, $user );
62
+ }
63
+
64
+ public function validate( $evaluation, $user, array $settings, array $args ) {
65
+ if ( ! $this->config['validate'] ) {
66
+ return true;
67
+ }
68
+
69
+ return call_user_func( $this->config['validate'], $evaluation, $user, $settings, $args );
70
+ }
71
+
72
+ public function get_reason_message( $evaluation, array $settings ): string {
73
+ return call_user_func( $this->config['reason'], $evaluation, $settings );
74
+ }
75
+
76
+ public function get_meta_key(): string {
77
+ return $this->config['meta'] ?: "_itsec_password_evaluation_{$this->get_code()}";
78
+ }
79
+
80
+ public function is_always_enabled(): bool {
81
+ return empty( $this->config['settings_config'] );
82
+ }
83
+
84
+ public function should_evaluate_if_not_enabled(): bool {
85
+ return ! empty( $this->config['evaluate_if_not_enabled'] );
86
+ }
87
+
88
+ public function get_settings_schema(): array {
89
+ if ( ! isset( $this->config['defaults'] ) ) {
90
+ return [];
91
+ }
92
+
93
+ $schema = [
94
+ 'type' => 'object',
95
+ 'properties' => [],
96
+ ];
97
+
98
+ foreach ( $this->config['defaults'] as $setting => $default ) {
99
+ $schema['properties'][ $setting ] = [
100
+ 'type' => [ 'string', 'array' ],
101
+ 'default' => $default,
102
+ ];
103
+ }
104
+
105
+ return $schema;
106
+ }
107
+
108
+ public function has_user_group(): bool {
109
+ return false;
110
+ }
111
+
112
+ public function getIterator() {
113
+ return new \ArrayIterator( $this->config );
114
+ }
115
+
116
+ public function offsetExists( $offset ) {
117
+ return isset( $this->config[ $offset ] );
118
+ }
119
+
120
+ public function offsetGet( $offset ) {
121
+ return $this->config[ $offset ] ?? null;
122
+ }
123
+
124
+ public function offsetSet( $offset, $value ) {
125
+
126
+ }
127
+
128
+ public function offsetUnset( $offset ) {
129
+
130
+ }
131
+ }
core/lib/Module_Config.php ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity;
4
+
5
+ final class Module_Config {
6
+
7
+ const T_ABOUT = 'about';
8
+ const T_SETTINGS = 'settings';
9
+ const T_USER_GROUPS = 'user-groups';
10
+ const T_PASSWORD_REQUIREMENTS = 'password-requirements';
11
+ const T_TOOLS = 'tools';
12
+ const T_ALL = [
13
+ self::T_ABOUT,
14
+ self::T_SETTINGS,
15
+ self::T_USER_GROUPS,
16
+ self::T_PASSWORD_REQUIREMENTS,
17
+ self::T_TOOLS,
18
+ ];
19
+ const TRANSLATE = [ 'title', 'description', 'help', 'keywords', 'enumNames' ];
20
+
21
+ /** @var array */
22
+ private $config;
23
+
24
+ /** @var Module_Config|null */
25
+ private $translated;
26
+
27
+ /** @var string[] */
28
+ private $translated_fields = [];
29
+
30
+ /**
31
+ * Module_Config constructor.
32
+ *
33
+ * @param array $config
34
+ */
35
+ public function __construct( array $config ) {
36
+ $this->config = self::transform_module_config( $config );
37
+ }
38
+
39
+ /**
40
+ * Get's the ID.
41
+ *
42
+ * @return string
43
+ */
44
+ public function get_id(): string {
45
+ return $this->get_config()['id'];
46
+ }
47
+
48
+ public function get_config(): array {
49
+ return $this->config;
50
+ }
51
+
52
+ public function get_status( $for = '' ): string {
53
+ $status = $this->get_config()['status'];
54
+
55
+ if ( is_string( $status ) ) {
56
+ return $status;
57
+ }
58
+
59
+ if ( ! $for ) {
60
+ $for = \ITSEC_Core::is_pro() ? 'pro' : 'free';
61
+ }
62
+
63
+ return $status[ $for ];
64
+ }
65
+
66
+ public function get_type(): string {
67
+ return $this->get_config()['type'];
68
+ }
69
+
70
+ public function is_deprecated(): bool {
71
+ return ! empty( $this->get_config()['deprecated'] );
72
+ }
73
+
74
+ public function get_deprecated_version(): string {
75
+ return $this->get_config()['deprecated'] ?? '';
76
+ }
77
+
78
+ public function is_onboard(): bool {
79
+ return $this->get_config()['onboard'] ?? false;
80
+ }
81
+
82
+ public function has_side_effects(): bool {
83
+ return ! empty( $this->get_config()['side-effects'] );
84
+ }
85
+
86
+ public function get_keywords(): array {
87
+ return $this->get_config()['keywords'] ?? [];
88
+ }
89
+
90
+ public function get_title(): string {
91
+ return $this->get_config()['title'] ?? '';
92
+ }
93
+
94
+ public function get_description(): string {
95
+ return $this->get_config()['description'] ?? '';
96
+ }
97
+
98
+ public function get_help(): string {
99
+ return $this->get_config()['help'] ?? '';
100
+ }
101
+
102
+ public function get_tools(): array {
103
+ return $this->get_config()['tools'] ?? [];
104
+ }
105
+
106
+ public function get_user_groups(): array {
107
+ return $this->get_config()['user-groups'] ?? [];
108
+ }
109
+
110
+ public function get_password_requirements(): array {
111
+ return $this->get_config()['password-requirements'] ?? [];
112
+ }
113
+
114
+ public function get_settings(): array {
115
+ return $this->get_config()['settings'] ?? [];
116
+ }
117
+
118
+ public function get_conditional_settings(): array {
119
+ return $this->get_config()['conditional-settings'] ?? [];
120
+ }
121
+
122
+ public function get_removed_settings(): array {
123
+ return $this->get_config()['removed-settings'] ?? [];
124
+ }
125
+
126
+ public function get_deprecated_settings(): array {
127
+ return $this->get_config()['deprecated-settings'] ?? [];
128
+ }
129
+
130
+ public function get_onboard_settings(): array {
131
+ return $this->get_config()['onboard-settings'] ?? [];
132
+ }
133
+
134
+ public function get_scheduling(): array {
135
+ return $this->get_config()['scheduling'] ?? [];
136
+ }
137
+
138
+ public function get_extends(): string {
139
+ return $this->get_config()['extends'] ?? '';
140
+ }
141
+
142
+ public function get_requirements(): array {
143
+ return $this->get_config()['requirements'] ?? [];
144
+ }
145
+
146
+ /**
147
+ * Translates user-facing strings and returns a new
148
+ * config instance with those translations applied.
149
+ *
150
+ * @param string ...$fields Optionally, limit to translating only the given fields.
151
+ *
152
+ * @return $this
153
+ */
154
+ public function translate( string ...$fields ): self {
155
+ $fields = $fields ?: self::T_ALL;
156
+
157
+ if ( ! $this->translated ) {
158
+ $this->translated = clone $this;
159
+ }
160
+
161
+ foreach ( $fields as $field ) {
162
+ if ( in_array( $field, $this->translated->translated_fields, true ) ) {
163
+ continue;
164
+ }
165
+
166
+ if ( 'about' === $field ) {
167
+ $this->translated->config['title'] = static::translate_value( $this->get_title() );
168
+ $this->translated->config['description'] = static::translate_value( $this->get_description() );
169
+ $this->translated->config['help'] = static::translate_value( $this->get_help() );
170
+ $this->translated->config['keywords'] = array_map( static function ( $value ) {
171
+ return static::translate_value( $value );
172
+ }, $this->get_keywords() );
173
+ } elseif ( isset( $this->translated->config[ $field ] ) ) {
174
+ self::apply_translate( $this->translated->config[ $field ] );
175
+ }
176
+
177
+ $this->translated->translated_fields[] = $field;
178
+ }
179
+
180
+ return $this->translated;
181
+ }
182
+
183
+ /**
184
+ * Extends the module config with a provided module config.
185
+ *
186
+ * @param Module_Config $with
187
+ *
188
+ * @return $this A new merged module config is returned.
189
+ */
190
+ public function extend( Module_Config $with ): self {
191
+ $new = clone $this;
192
+
193
+ if ( $with->get_keywords() ) {
194
+ $new->config['keywords'] = array_merge( $new->get_keywords(), $with->get_keywords() );
195
+ }
196
+
197
+ if ( $with->get_tools() ) {
198
+ $new->config['tools'] = array_merge( $new->get_tools(), $with->get_tools() );
199
+ }
200
+
201
+ if ( $with->get_user_groups() ) {
202
+ $new->config['user-groups'] = array_merge( $new->get_user_groups(), $with->get_user_groups() );
203
+ }
204
+
205
+ if ( $with->get_password_requirements() ) {
206
+ $new->config['password-requirements'] = array_merge( $new->get_password_requirements(), $with->get_password_requirements() );
207
+ }
208
+
209
+ if ( $with->get_settings() ) {
210
+ if ( ! $new->get_settings() ) {
211
+ $new->config['settings'] = $with->get_settings();
212
+ } else {
213
+ $new->config['settings'] = \ITSEC_Lib::array_merge_recursive_distinct( $new->config['settings'], $with->get_settings() );
214
+ }
215
+ }
216
+
217
+ if ( $with->get_conditional_settings() ) {
218
+ $new->config['conditional-settings'] = array_merge( $new->get_conditional_settings(), $with->get_conditional_settings() );
219
+ }
220
+
221
+ if ( $with->get_removed_settings() ) {
222
+ $new->config['removed-settings'] = array_merge( $new->get_removed_settings(), $with->get_removed_settings() );
223
+ }
224
+
225
+ if ( $with->get_removed_settings() ) {
226
+ $new->config['removed-settings'] = array_merge( $new->get_removed_settings(), $with->get_removed_settings() );
227
+ }
228
+
229
+ if ( $with->get_deprecated_settings() ) {
230
+ $new->config['deprecated-settings'] = array_merge( $new->get_deprecated_settings(), $with->get_deprecated_settings() );
231
+ }
232
+
233
+ if ( $with->get_onboard_settings() ) {
234
+ $new->config['onboard-settings'] = array_merge( $new->get_onboard_settings(), $with->get_onboard_settings() );
235
+ }
236
+
237
+ if ( $with->get_scheduling() ) {
238
+ $new->config['scheduling'] = array_merge( $new->get_scheduling(), $with->get_scheduling() );
239
+ }
240
+
241
+ return $new;
242
+ }
243
+
244
+ /**
245
+ * Extracts the list of strings that can be translated.
246
+ *
247
+ * @return array
248
+ */
249
+ public function extract_translatable_strings(): array {
250
+ $config = $this->config;
251
+ $strings = array_intersect_key( $config, array_flip( [
252
+ 'title',
253
+ 'description',
254
+ 'help',
255
+ ] ) );
256
+
257
+ if ( isset( $config['keywords'] ) ) {
258
+ $strings = array_merge( $strings, $config['keywords'] );
259
+ }
260
+
261
+ $extract = static function ( &$value ) use ( &$strings, &$extract ) {
262
+ if ( ! is_array( $value ) ) {
263
+ return;
264
+ }
265
+
266
+ foreach ( self::TRANSLATE as $field ) {
267
+ if ( isset( $value[ $field ] ) ) {
268
+ $strings = array_merge( $strings, (array) $value[ $field ] );
269
+ }
270
+ }
271
+
272
+ array_walk( $value, $extract );
273
+ };
274
+
275
+ foreach ( self::T_ALL as $field ) {
276
+ if ( self::T_ABOUT === $field ) {
277
+ continue;
278
+ }
279
+
280
+ if ( isset( $config[ $field ] ) ) {
281
+ $extract( $config[ $field ] );
282
+ }
283
+ }
284
+
285
+ return array_filter( array_values( $strings ), function ( $string ) {
286
+ return is_string( $string ) && trim( $string, " \t\n\r\0\x0B​" ) !== '';
287
+ } );
288
+ }
289
+
290
+ /**
291
+ * Transforms a module's config definition.
292
+ *
293
+ * @param array $config The module config.
294
+ *
295
+ * @return array
296
+ */
297
+ private static function transform_module_config( array $config ): array {
298
+ $transformed = \ITSEC_Lib::resolve_schema_refs( $config );
299
+
300
+ if ( isset( $transformed['user-groups'] ) ) {
301
+ $item = [
302
+ 'oneOf' => [
303
+ [
304
+ 'type' => 'string',
305
+ 'format' => 'uuid',
306
+ ],
307
+ [
308
+ 'type' => 'string',
309
+ 'enum' => [ 'everybody-else' ],
310
+ ]
311
+ ]
312
+ ];
313
+
314
+ foreach ( $transformed['user-groups'] as $slug => $group ) {
315
+ if ( isset( $transformed['settings']['properties'][ $slug ] ) ) {
316
+ continue;
317
+ }
318
+
319
+ if ( $group['type'] === 'multiple' ) {
320
+ $transformed['settings']['properties'][ $slug ] = [
321
+ 'type' => 'array',
322
+ 'items' => $item,
323
+ 'default' => [],
324
+ 'readonly' => true,
325
+ ];
326
+ } else {
327
+ $transformed['settings']['properties'][ $slug ] = [
328
+ 'oneOf' => $item['oneOf'],
329
+ 'default' => '',
330
+ 'readonly' => true,
331
+ ];
332
+ }
333
+ }
334
+ }
335
+
336
+ if ( isset( $transformed['settings'] ) ) {
337
+ if ( ! isset( $transformed['settings']['$schema'] ) ) {
338
+ $transformed['settings']['$schema'] = 'http://json-schema.org/draft-04/schema#';
339
+ }
340
+
341
+ if ( ! isset( $transformed['settings']['id'] ) ) {
342
+ $transformed['settings']['id'] = 'itsec-module-' . $config['id'];
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Filters the config definition for a module.
348
+ *
349
+ * @param array $transformed The transformed config.
350
+ * @param array $config The original config.
351
+ */
352
+ return apply_filters( "itsec_{$config['id']}_module_config", $transformed, $config );
353
+ }
354
+
355
+ /**
356
+ * Resolves $ref entries at any point in the config.
357
+ *
358
+ * Currently, only a simplified form of JSON Pointers are supported where `/` is the only
359
+ * allowed control character.
360
+ *
361
+ * Additionally, the `$ref` keyword must start with `#/definitions`.
362
+ *
363
+ * @param mixed $value The incoming value.
364
+ * @param string $key The array key.
365
+ * @param array $definitions The shared definitions.
366
+ */
367
+ private static function resolve_ref( &$value, $key, $definitions ) {
368
+ if ( ! is_array( $value ) ) {
369
+ return;
370
+ }
371
+
372
+ if ( isset( $value['$ref'] ) ) {
373
+ $ref = str_replace( '#/definitions/', '', $value['$ref'] );
374
+ $value = \ITSEC_Lib::array_get( $definitions, $ref, null, '/' );
375
+
376
+ return;
377
+ }
378
+
379
+ array_walk( $value, [ static::class, 'resolve_ref' ], $definitions );
380
+ }
381
+
382
+ /**
383
+ * Applies translations to the given value.
384
+ *
385
+ * @param mixed $value
386
+ */
387
+ private static function apply_translate( &$value ) {
388
+ if ( ! is_array( $value ) ) {
389
+ return;
390
+ }
391
+
392
+ foreach ( self::TRANSLATE as $field ) {
393
+ if ( isset( $value[ $field ] ) ) {
394
+ if ( is_string( $value[ $field ] ) ) {
395
+ $value[ $field ] = static::translate_value( $value[ $field ] );
396
+ } elseif ( wp_is_numeric_array( $value[ $field ] ) ) {
397
+ $value[ $field ] = array_map( [ static::class, 'translate_value' ], $value[ $field ] );
398
+ }
399
+ }
400
+ }
401
+
402
+ array_walk( $value, [ static::class, 'apply_translate' ] );
403
+ }
404
+
405
+ private static function translate_value( string $value ): string {
406
+ $translated = translate( $value, 'better-wp-security' );
407
+
408
+ if ( strpos( $value, '[' ) !== false ) {
409
+ $translated = preg_replace_callback( '/\[([^\]]+)\]\(([^\)]+)\)/', function ( $matches ) {
410
+ if ( 0 === strpos( $matches[2], 'itsec://' ) ) {
411
+ $path = substr( $matches[2], strlen( 'itsec://' ) );
412
+
413
+ return \ITSEC_Core::get_link_for_settings_route( '/' . $path ) . $matches[1] . '</a>';
414
+ }
415
+
416
+ return sprintf( '<a href="%s">%s</a>', esc_url( $matches[2] ), $matches[1] );
417
+ }, $translated );
418
+ }
419
+
420
+ return $translated;
421
+ }
422
+ }
core/lib/Password_Requirement.php ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib;
4
+
5
+ interface Password_Requirement {
6
+ /**
7
+ * Gets the reason code for the password requirement.
8
+ *
9
+ * @return string
10
+ */
11
+ public function get_code(): string;
12
+
13
+ /**
14
+ * Gets the module responsible for the password requirement.
15
+ *
16
+ * @return string
17
+ */
18
+ public function get_module(): string;
19
+
20
+ /**
21
+ * Gets the title for the password requirement.
22
+ *
23
+ * @return string
24
+ */
25
+ public function get_title(): string;
26
+
27
+ /**
28
+ * Gets the description for the password requirement.
29
+ *
30
+ * @return string
31
+ */
32
+ public function get_description(): string;
33
+
34
+ /**
35
+ * Checks if the user must change their password.
36
+ *
37
+ * @param \WP_User $user The user object.
38
+ * @param array $settings The selected settings.
39
+ *
40
+ * @return bool
41
+ */
42
+ public function is_password_change_required( \WP_User $user, array $settings ): bool;
43
+
44
+ /**
45
+ * Evaluates a password.
46
+ *
47
+ * @param string $password The raw password.
48
+ * @param \WP_User|\stdClass $user The user object. May be an \stdClass
49
+ * if a user is being edited or created.
50
+ *
51
+ * @return mixed|\WP_Error The evaluation, or a WP_Error instance if the password could not be evaluated.
52
+ */
53
+ public function evaluate( string $password, $user );
54
+
55
+ /**
56
+ * Validates that a password is valid for the given user.
57
+ *
58
+ * @param mixed $evaluation The password evaluation returned from {@see Password_Requirement::evaluate()}.
59
+ * @param \WP_User|\stdClass $user The user object. May be an \stdClass
60
+ * if a user is being edited or created.
61
+ * @param array $settings The selected settings.
62
+ * @param array $args Additional arguments describing the validation.
63
+ *
64
+ * @return bool|string Whether the password is valid for this user.
65
+ * Optionally return a message to display to the user in case it is invalid.
66
+ */
67
+ public function validate( $evaluation, $user, array $settings, array $args );
68
+
69
+ /**
70
+ * Gets the reason a user must change their password.
71
+ *
72
+ * @param mixed $evaluation The password evaluation returned from {@see Password_Requirement::evaluate()}.
73
+ * @param array $settings The selected settings.
74
+ *
75
+ * @return string
76
+ */
77
+ public function get_reason_message( $evaluation, array $settings ): string;
78
+
79
+ /**
80
+ * Gets the meta key that should be used to store the password's evaluation.
81
+ *
82
+ * @return string
83
+ */
84
+ public function get_meta_key(): string;
85
+
86
+ /**
87
+ * Is the password requirement always enabled, or can it be disabled.
88
+ *
89
+ * @return bool
90
+ */
91
+ public function is_always_enabled(): bool;
92
+
93
+ /**
94
+ * Should passwords be evaluated even if the Password Requirement isn't enabled.
95
+ *
96
+ * @return bool
97
+ */
98
+ public function should_evaluate_if_not_enabled(): bool;
99
+
100
+ /**
101
+ * Gets the settings schema.
102
+ *
103
+ * @return array
104
+ */
105
+ public function get_settings_schema(): array;
106
+
107
+ /**
108
+ * Checks if this password requirement should have an associated user group.
109
+ *
110
+ * @return bool
111
+ */
112
+ public function has_user_group(): bool;
113
+ }
core/lib/Result.php ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib;
4
+
5
+ final class Result {
6
+ const SUCCESS = 'success';
7
+ const ERROR = 'error';
8
+
9
+ /** @var string */
10
+ private $type;
11
+
12
+ /** @var \WP_Error */
13
+ private $error;
14
+
15
+ /** @var mixed */
16
+ private $data;
17
+
18
+ private $success = [];
19
+
20
+ private $info = [];
21
+
22
+ private $warning = [];
23
+
24
+ private function __construct( string $type ) {
25
+ $this->type = $type;
26
+ }
27
+
28
+ public static function success( $data = null ): self {
29
+ $self = new self( self::SUCCESS );
30
+ $self->data = $data;
31
+
32
+ return $self;
33
+ }
34
+
35
+ public static function error( \WP_Error $error ): self {
36
+ $self = new self( self::ERROR );
37
+ $self->error = $error;
38
+
39
+ return $self;
40
+ }
41
+
42
+ public static function from_response(): self {
43
+ if ( \ITSEC_Response::is_success() ) {
44
+ $result = self::success( \ITSEC_Response::get_response() );
45
+ } else {
46
+ $result = self::error( \ITSEC_Response::as_wp_error() );
47
+ }
48
+
49
+ $result->add_warning_message( ...\ITSEC_Response::get_warnings() );
50
+ $result->add_info_message( ...\ITSEC_Response::get_infos() );
51
+ $result->add_success_message( ...\ITSEC_Response::get_messages() );
52
+
53
+ return $result;
54
+ }
55
+
56
+ public function is_success(): bool { return self::SUCCESS === $this->type; }
57
+
58
+ public function get_data() { return $this->data; }
59
+
60
+ public function get_error(): \WP_Error { return $this->error; }
61
+
62
+ public function add_success_message( string ...$messages ): self {
63
+ $this->success = array_merge( $this->success, $messages );
64
+
65
+ return $this;
66
+ }
67
+
68
+ public function get_success_messages(): array { return $this->success; }
69
+
70
+ public function add_info_message( string ...$messages ): self {
71
+ $this->info = array_merge( $this->info, $messages );
72
+
73
+ return $this;
74
+ }
75
+
76
+ public function get_info_messages(): array { return $this->info; }
77
+
78
+ public function add_warning_message( string ...$messages ): self {
79
+ $this->warning = array_merge( $this->warning, $messages );
80
+
81
+ return $this;
82
+ }
83
+
84
+ public function get_warning_messages(): array { return $this->warning; }
85
+
86
+ public function as_rest_response(): \WP_REST_Response {
87
+ if ( $this->is_success() ) {
88
+ $data = $this->get_data();
89
+ $response = new \WP_REST_Response( $data, $data ? 200 : 204 );
90
+ } else {
91
+ $response = \ITSEC_Lib_REST::error_to_response( $this->get_error() );
92
+ }
93
+
94
+ if ( $success = $this->get_success_messages() ) {
95
+ $response->header( 'X-Messages-Success', wp_json_encode( $success ) );
96
+ }
97
+
98
+ if ( $info = $this->get_info_messages() ) {
99
+ $response->header( 'X-Messages-Info', wp_json_encode( $info ) );
100
+ }
101
+
102
+ if ( $warning = $this->get_warning_messages() ) {
103
+ $response->header( 'X-Messages-Warning', wp_json_encode( $warning ) );
104
+ }
105
+
106
+ return $response;
107
+ }
108
+ }
core/lib/ban-hosts/REST.php CHANGED
@@ -391,10 +391,13 @@ class REST extends \WP_REST_Controller {
391
 
392
  foreach ( $this->repository->get_sources() as $source ) {
393
  if ( $this->repository->supports_create( $source ) ) {
 
 
394
  $links[] = [
395
  'rel' => 'create-form',
396
  'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $source ) ),
397
- 'submissionSchema' => \ITSEC_Lib_REST::sanitize_schema_for_output( $this->repository->get_creation_schema( $source ) ),
 
398
  ];
399
  }
400
 
@@ -407,7 +410,7 @@ class REST extends \WP_REST_Controller {
407
  }
408
  }
409
 
410
- return $this->schema = [
411
  '$schema' => 'http://json-schema.org/draft-04/schema#',
412
  'title' => 'ithemes-security-ban',
413
  'type' => 'object',
@@ -453,6 +456,17 @@ class REST extends \WP_REST_Controller {
453
  ],
454
  'links' => $links,
455
  ];
 
 
 
 
 
 
 
 
 
 
 
456
  }
457
 
458
  public function get_collection_params( $source = '' ) {
391
 
392
  foreach ( $this->repository->get_sources() as $source ) {
393
  if ( $this->repository->supports_create( $source ) ) {
394
+ $schema = $this->repository->get_creation_schema( $source );
395
+
396
  $links[] = [
397
  'rel' => 'create-form',
398
  'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $source ) ),
399
+ 'submissionSchema' => \ITSEC_Lib_REST::sanitize_schema_for_output( $schema ),
400
+ 'title' => $schema['title'] ?? __( 'Add Ban', 'better-wp-security' ),
401
  ];
402
  }
403
 
410
  }
411
  }
412
 
413
+ $schema = [
414
  '$schema' => 'http://json-schema.org/draft-04/schema#',
415
  'title' => 'ithemes-security-ban',
416
  'type' => 'object',
456
  ],
457
  'links' => $links,
458
  ];
459
+
460
+ /**
461
+ * Filters the schema for the Ban Hosts REST API endpoint.
462
+ *
463
+ * @since 7.0.0
464
+ *
465
+ * @param array $schema The schema to filter.
466
+ */
467
+ $this->schema = apply_filters( 'itsec_ban_hosts_rest_schema', $schema );
468
+
469
+ return $this->schema;
470
  }
471
 
472
  public function get_collection_params( $source = '' ) {
core/lib/class-itsec-lib-feature-flags.php CHANGED
@@ -32,7 +32,7 @@ class ITSEC_Lib_Feature_Flags {
32
  * @return true|WP_Error
33
  */
34
  public static function register_flag( $name, $args = array() ) {
35
- if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $name ) ) {
36
  return new WP_Error( 'invalid_flag_name', __( 'Invalid flag name.', 'better-wp-security' ) );
37
  }
38
 
32
  * @return true|WP_Error
33
  */
34
  public static function register_flag( $name, $args = array() ) {
35
+ if ( ! preg_match( '/^\w+$/', $name ) ) {
36
  return new WP_Error( 'invalid_flag_name', __( 'Invalid flag name.', 'better-wp-security' ) );
37
  }
38
 
core/lib/class-itsec-lib-ip-detector.php CHANGED
@@ -33,6 +33,10 @@ class ITSEC_Lib_IP_Detector {
33
  return apply_filters( 'itsec_filter_remote_addr_headers', array(
34
  'HTTP_CF_CONNECTING_IP', // CloudFlare
35
  'HTTP_X_FORWARDED_FOR', // Squid and most other forward and reverse proxies
 
 
 
 
36
  ) );
37
  }
38
 
33
  return apply_filters( 'itsec_filter_remote_addr_headers', array(
34
  'HTTP_CF_CONNECTING_IP', // CloudFlare
35
  'HTTP_X_FORWARDED_FOR', // Squid and most other forward and reverse proxies
36
+ 'HTTP_X_REAL_IP',
37
+ 'HTTP_X_CLIENT_IP',
38
+ 'HTTP_CLIENT_IP',
39
+ 'HTTP_X_CLUSTER_CLIENT_IP',
40
  ) );
41
  }
42
 
core/lib/class-itsec-lib-password-requirements.php CHANGED
@@ -1,5 +1,7 @@
1
  <?php
2
 
 
 
3
  use iThemesSecurity\User_Groups;
4
 
5
  /**
@@ -7,13 +9,13 @@ use iThemesSecurity\User_Groups;
7
  */
8
  class ITSEC_Lib_Password_Requirements {
9
 
10
- /** @var array[] */
11
  private static $requirements;
12
 
13
  /**
14
  * Get all registered password requirements.
15
  *
16
- * @return array
17
  */
18
  public static function get_registered() {
19
  if ( null === self::$requirements ) {
@@ -31,10 +33,20 @@ class ITSEC_Lib_Password_Requirements {
31
  /**
32
  * Register a password requirement.
33
  *
34
- * @param string $reason_code
35
- * @param array $opts
36
  */
37
- public static function register( $reason_code, $opts ) {
 
 
 
 
 
 
 
 
 
 
38
  $merged = wp_parse_args( $opts, array(
39
  'evaluate' => null,
40
  'validate' => null,
@@ -51,6 +63,7 @@ class ITSEC_Lib_Password_Requirements {
51
  ( ! is_callable( $merged['validate'] ) || ! is_callable( $merged['evaluate'] ) )
52
  ) {
53
  _doing_it_wrong( __METHOD__, 'Validate and evaluate must be callable if defined.', '5.8.0' );
 
54
  return;
55
  }
56
 
@@ -63,6 +76,7 @@ class ITSEC_Lib_Password_Requirements {
63
  if ( array_key_exists( 'defaults', $opts ) ) {
64
  if ( ! is_array( $merged['defaults'] ) ) {
65
  _doing_it_wrong( __METHOD__, 'Defaults must be an array if defined.', '5.8.0' );
 
66
  return;
67
  }
68
 
@@ -79,7 +93,7 @@ class ITSEC_Lib_Password_Requirements {
79
  return;
80
  }
81
 
82
- self::$requirements[ $reason_code ] = $merged;
83
  }
84
 
85
  /**
@@ -100,8 +114,9 @@ class ITSEC_Lib_Password_Requirements {
100
  $registered = self::get_registered();
101
 
102
  if ( isset( $registered[ $reason ] ) ) {
103
- $settings = self::get_requirement_settings( $reason );
104
- $message = call_user_func( $registered[ $reason ]['reason'], get_user_meta( $user->ID, $registered[ $reason ]['meta'], true ), $settings );
 
105
  }
106
 
107
  /**
@@ -289,11 +304,14 @@ class ITSEC_Lib_Password_Requirements {
289
  return false;
290
  }
291
 
292
- // If the requirement does not have any settings, than it is always enabled.
293
- if ( null === $requirements[ $requirement ]['settings_config'] ) {
294
  return true;
295
  }
296
 
 
 
 
 
297
  $enabled = ITSEC_Modules::get_setting( 'password-requirements', 'enabled_requirements' );
298
 
299
  if ( ! empty( $enabled[ $requirement ] ) ) {
@@ -308,23 +326,22 @@ class ITSEC_Lib_Password_Requirements {
308
  *
309
  * @param string $requirement
310
  *
311
- * @return array|false
312
  */
313
  public static function get_requirement_settings( $requirement ) {
314
 
315
  $requirements = self::get_registered();
316
 
317
  if ( ! isset( $requirements[ $requirement ] ) ) {
318
- return false;
319
  }
320
 
321
- if ( null === $requirements[ $requirement ]['settings_config'] ) {
322
- return false;
323
  }
324
 
325
  $all_settings = ITSEC_Modules::get_setting( 'password-requirements', 'requirement_settings' );
326
- $settings = isset( $all_settings[ $requirement ] ) ? $all_settings[ $requirement ] : array();
327
 
328
- return wp_parse_args( $settings, $requirements[ $requirement ]['defaults'] );
329
  }
330
  }
1
  <?php
2
 
3
+ use iThemesSecurity\Lib\Legacy_Password_Requirement;
4
+ use iThemesSecurity\Lib\Password_Requirement;
5
  use iThemesSecurity\User_Groups;
6
 
7
  /**
9
  */
10
  class ITSEC_Lib_Password_Requirements {
11
 
12
+ /** @var Password_Requirement[] */
13
  private static $requirements;
14
 
15
  /**
16
  * Get all registered password requirements.
17
  *
18
+ * @return Password_Requirement[]
19
  */
20
  public static function get_registered() {
21
  if ( null === self::$requirements ) {
33
  /**
34
  * Register a password requirement.
35
  *
36
+ * @param Password_Requirement|string $requirement_or_code
37
+ * @param array $opts
38
  */
39
+ public static function register( $requirement_or_code, $opts = [] ) {
40
+ if ( $requirement_or_code instanceof Password_Requirement ) {
41
+ self::$requirements[ $requirement_or_code->get_code() ] = $requirement_or_code;
42
+
43
+ return;
44
+ }
45
+
46
+ _doing_it_wrong( __METHOD__, 'Pass a Password_Requirement instance instead of a configuration array.', '7.0.0' );
47
+
48
+ $reason_code = $requirement_or_code;
49
+
50
  $merged = wp_parse_args( $opts, array(
51
  'evaluate' => null,
52
  'validate' => null,
63
  ( ! is_callable( $merged['validate'] ) || ! is_callable( $merged['evaluate'] ) )
64
  ) {
65
  _doing_it_wrong( __METHOD__, 'Validate and evaluate must be callable if defined.', '5.8.0' );
66
+
67
  return;
68
  }
69
 
76
  if ( array_key_exists( 'defaults', $opts ) ) {
77
  if ( ! is_array( $merged['defaults'] ) ) {
78
  _doing_it_wrong( __METHOD__, 'Defaults must be an array if defined.', '5.8.0' );
79
+
80
  return;
81
  }
82
 
93
  return;
94
  }
95
 
96
+ self::$requirements[ $reason_code ] = new Legacy_Password_Requirement( $reason_code, $merged );
97
  }
98
 
99
  /**
114
  $registered = self::get_registered();
115
 
116
  if ( isset( $registered[ $reason ] ) ) {
117
+ $settings = self::get_requirement_settings( $reason );
118
+ $evaluation = get_user_meta( $user->ID, $registered[ $reason ]->get_meta_key(), true );
119
+ $message = $registered[ $reason ]->get_reason_message( $evaluation, $settings );
120
  }
121
 
122
  /**
304
  return false;
305
  }
306
 
307
+ if ( $requirements[ $requirement ]->is_always_enabled() ) {
 
308
  return true;
309
  }
310
 
311
+ if ( $requirements[ $requirement ]->has_user_group() ) {
312
+ return ! empty( self::get_requirement_settings( $requirement )['group'] );
313
+ }
314
+
315
  $enabled = ITSEC_Modules::get_setting( 'password-requirements', 'enabled_requirements' );
316
 
317
  if ( ! empty( $enabled[ $requirement ] ) ) {
326
  *
327
  * @param string $requirement
328
  *
329
+ * @return array
330
  */
331
  public static function get_requirement_settings( $requirement ) {
332
 
333
  $requirements = self::get_registered();
334
 
335
  if ( ! isset( $requirements[ $requirement ] ) ) {
336
+ return [];
337
  }
338
 
339
+ if ( ! $requirements[ $requirement ]->get_settings_schema() ) {
340
+ return [];
341
  }
342
 
343
  $all_settings = ITSEC_Modules::get_setting( 'password-requirements', 'requirement_settings' );
 
344
 
345
+ return $all_settings[ $requirement ] ?? [];
346
  }
347
  }
core/lib/class-itsec-lib-rest.php CHANGED
@@ -29,23 +29,36 @@ class ITSEC_Lib_REST {
29
  * @return WP_REST_Response List of associative arrays with code and message keys.
30
  */
31
  public static function error_to_response( WP_Error $error ) {
32
- $error_data = $error->get_error_data();
33
-
34
- if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
35
- $status = $error_data['status'];
36
- } else {
37
- $status = 500;
38
  }
39
 
 
 
 
 
 
 
 
 
40
  $errors = array();
41
 
42
  foreach ( (array) $error->errors as $code => $messages ) {
 
 
 
43
  foreach ( (array) $messages as $message ) {
44
- $errors[] = array(
45
  'code' => $code,
46
  'message' => $message,
47
- 'data' => $error->get_error_data( $code ),
48
  );
 
 
 
 
 
 
49
  }
50
  }
51
 
@@ -264,4 +277,21 @@ class ITSEC_Lib_REST {
264
 
265
  return strtoupper( $_SERVER['REQUEST_METHOD'] );
266
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  }
29
  * @return WP_REST_Response List of associative arrays with code and message keys.
30
  */
31
  public static function error_to_response( WP_Error $error ) {
32
+ if ( function_exists( 'rest_convert_error_to_response' ) ) {
33
+ return rest_convert_error_to_response( $error );
 
 
 
 
34
  }
35
 
36
+ $status = array_reduce(
37
+ $error->get_all_error_data(),
38
+ function ( $status, $error_data ) {
39
+ return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status;
40
+ },
41
+ 500
42
+ );
43
+
44
  $errors = array();
45
 
46
  foreach ( (array) $error->errors as $code => $messages ) {
47
+ $all_data = $error->get_all_error_data( $code );
48
+ $last_data = array_pop( $all_data );
49
+
50
  foreach ( (array) $messages as $message ) {
51
+ $formatted = array(
52
  'code' => $code,
53
  'message' => $message,
54
+ 'data' => $last_data,
55
  );
56
+
57
+ if ( $all_data ) {
58
+ $formatted['additional_data'] = $all_data;
59
+ }
60
+
61
+ $errors[] = $formatted;
62
  }
63
  }
64
 
277
 
278
  return strtoupper( $_SERVER['REQUEST_METHOD'] );
279
  }
280
+
281
+ /**
282
+ * Adds a status code to a WP_Error object.
283
+ *
284
+ * @param int $status
285
+ * @param WP_Error $error
286
+ * @param bool $overwrite
287
+ */
288
+ public static function add_status_to_error( int $status, WP_Error $error, bool $overwrite = false ) {
289
+ $data = $error->get_error_data();
290
+
291
+ if ( ! $data ) {
292
+ $error->add_data( [ 'status' => $status ] );
293
+ } elseif ( ! isset( $data['status'] ) || $overwrite ) {
294
+ $error->add_data( array_merge( (array) $data, [ 'status' => $status ] ) );
295
+ }
296
+ }
297
  }
core/lib/class-itsec-lib-user-activity.php CHANGED
@@ -30,7 +30,7 @@ final class ITSEC_Lib_User_Activity {
30
  return false;
31
  }
32
 
33
- return get_user_meta( $user_id, 'itsec_user_activity_last_seen', true );
34
  }
35
 
36
  public function identify_user() {
30
  return false;
31
  }
32
 
33
+ return (int) get_user_meta( $user_id, 'itsec_user_activity_last_seen', true );
34
  }
35
 
36
  public function identify_user() {
core/lib/class-itsec-scheduler.php CHANGED
@@ -1,5 +1,7 @@
1
  <?php
2
 
 
 
3
  abstract class ITSEC_Scheduler {
4
 
5
  const S_TWICE_HOURLY = 'twice-hourly';
@@ -69,7 +71,7 @@ abstract class ITSEC_Scheduler {
69
  * @param string $id The event ID.
70
  * @param array $data Event data.
71
  * @param array $opts
72
- * - fire_at: Manually specify the first time the event should be fired.
73
  */
74
  public function schedule_loop( $id, $data = array(), $opts = array() ) {
75
  $start = isset( $opts['fire_at'] ) ? $opts['fire_at'] : ITSEC_Core::get_current_time_gmt() + 60 * mt_rand( 1, 30 );
@@ -114,7 +116,7 @@ abstract class ITSEC_Scheduler {
114
  *
115
  * The data specified needs to be identical to the data the single event was scheduled with.
116
  *
117
- * @param string $id The event ID to unschedule.
118
  * @param array|null $data Unschedules the event with the given data. Pass null to delete any and all events matching the ID.
119
  *
120
  * @return bool
@@ -226,7 +228,7 @@ abstract class ITSEC_Scheduler {
226
  /**
227
  * Registers events for a given module using the "scheduling.php" module file.
228
  *
229
- * @param string $module
230
  */
231
  public function register_events_for_module( $module ) {
232
  ITSEC_Modules::load_module_file( 'scheduling.php', $module, function ( $fn ) {
@@ -238,6 +240,53 @@ abstract class ITSEC_Scheduler {
238
 
239
  $fn( $this );
240
  } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
  /**
1
  <?php
2
 
3
+ use iThemesSecurity\Module_Config;
4
+
5
  abstract class ITSEC_Scheduler {
6
 
7
  const S_TWICE_HOURLY = 'twice-hourly';
71
  * @param string $id The event ID.
72
  * @param array $data Event data.
73
  * @param array $opts
74
+ * - fire_at: Manually specify the first time the event should be fired.
75
  */
76
  public function schedule_loop( $id, $data = array(), $opts = array() ) {
77
  $start = isset( $opts['fire_at'] ) ? $opts['fire_at'] : ITSEC_Core::get_current_time_gmt() + 60 * mt_rand( 1, 30 );
116
  *
117
  * The data specified needs to be identical to the data the single event was scheduled with.
118
  *
119
+ * @param string $id The event ID to unschedule.
120
  * @param array|null $data Unschedules the event with the given data. Pass null to delete any and all events matching the ID.
121
  *
122
  * @return bool
228
  /**
229
  * Registers events for a given module using the "scheduling.php" module file.
230
  *
231
+ * @param string $module The module specifier. For example ':active' or 'global'.
232
  */
233
  public function register_events_for_module( $module ) {
234
  ITSEC_Modules::load_module_file( 'scheduling.php', $module, function ( $fn ) {
240
 
241
  $fn( $this );
242
  } );
243
+
244
+ foreach ( ITSEC_Modules::get_config_list( $module ) as $config ) {
245
+ $this->register_events_for_config( $config );
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Registers events based on a module configuration.
251
+ *
252
+ * @param Module_Config $config The config to use.
253
+ */
254
+ public function register_events_for_config( Module_Config $config ) {
255
+ foreach ( $config->get_scheduling() as $id => $definition ) {
256
+ if ( $conditional_schema = $definition['conditional'] ?? [] ) {
257
+ $settings = ITSEC_Modules::get_settings( $config->get_id() );
258
+
259
+ $validated = rest_validate_value_from_schema( $settings, $conditional_schema );
260
+ $sanitized = is_wp_error( $validated ) ? $validated : rest_sanitize_value_from_schema( $settings, $conditional_schema );
261
+ $is_active = ! is_wp_error( $validated ) && ! is_wp_error( $sanitized );
262
+
263
+ if ( ! $is_active ) {
264
+ $this->unschedule( $id );
265
+ continue;
266
+ }
267
+ }
268
+
269
+ $schedule = $definition['schedule'];
270
+ $data = $definition['data'] ?? [];
271
+ $opts = $definition['opts'] ?? [];
272
+
273
+ if ( isset( $opts['fire_at'] ) ) {
274
+ $opts['fire_at'] = ITSEC_Core::get_current_time_gmt() + $opts['fire_at'];
275
+ }
276
+
277
+ $this->schedule( $schedule, $id, $data, $opts );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Unregisters events for a given module.
283
+ *
284
+ * @param Module_Config $config The config to use.
285
+ */
286
+ public function unregister_events_for_config( Module_Config $config ) {
287
+ foreach ( $config->get_scheduling() as $id => $definition ) {
288
+ $this->unschedule( $id );
289
+ }
290
  }
291
 
292
  /**
core/lib/form.php CHANGED
@@ -508,6 +508,8 @@ final class ITSEC_Form {
508
  }
509
 
510
  public function add_user_groups( $var, $module, $setting = '', $options = array() ) {
 
 
511
  $source = ITSEC_Modules::get_container()->get( Matchables_Source::class );
512
 
513
  $user_groups = [];
@@ -527,8 +529,6 @@ final class ITSEC_Form {
527
  $options['class'] = 'itsec-form-input--type-user-groups';
528
 
529
  $this->add_multi_select( $var, $options );
530
- wp_enqueue_script( 'itsec-form-user-groups' );
531
- wp_enqueue_style( 'itsec-jquery-multi-select' );
532
  }
533
 
534
  public function get_dotted_var( $var ) {
508
  }
509
 
510
  public function add_user_groups( $var, $module, $setting = '', $options = array() ) {
511
+ _deprecated_function( __METHOD__, '7.0.0' );
512
+
513
  $source = ITSEC_Modules::get_container()->get( Matchables_Source::class );
514
 
515
  $user_groups = [];
529
  $options['class'] = 'itsec-form-input--type-user-groups';
530
 
531
  $this->add_multi_select( $var, $options );
 
 
532
  }
533
 
534
  public function get_dotted_var( $var ) {
core/lib/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/lib/rest/Modules_Controller.php ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\REST;
4
+
5
+ use iThemesSecurity\Module_Config;
6
+
7
+ final class Modules_Controller extends \WP_REST_Controller {
8
+
9
+ /**
10
+ * Modules_Controller constructor.
11
+ */
12
+ public function __construct() {
13
+ $this->namespace = 'ithemes-security/v1';
14
+ $this->rest_base = 'modules';
15
+ }
16
+
17
+ public function register_routes() {
18
+ register_rest_route( $this->namespace, $this->rest_base, [
19
+ [
20
+ 'methods' => \WP_REST_Server::READABLE,
21
+ 'callback' => [ $this, 'get_items' ],
22
+ 'permission_callback' => [ $this, 'get_items_permissions_check' ],
23
+ 'args' => $this->get_collection_params(),
24
+ ],
25
+ 'schema' => [ $this, 'get_public_item_schema' ],
26
+ ] );
27
+ register_rest_route( $this->namespace, $this->rest_base . '/(?P<id>[\w-]+)', [
28
+ [
29
+ 'methods' => \WP_REST_Server::READABLE,
30
+ 'callback' => [ $this, 'get_item' ],
31
+ 'permission_callback' => [ $this, 'get_item_permissions_check' ],
32
+ 'args' => [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ) ],
33
+ ],
34
+ [
35
+ 'methods' => 'PUT',
36
+ 'callback' => [ $this, 'update_item' ],
37
+ 'permission_callback' => [ $this, 'update_item_permissions_check' ],
38
+ 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ),
39
+ ],
40
+ 'schema' => [ $this, 'get_public_item_schema' ],
41
+ 'allow_batch' => [ 'v1' => true ],
42
+ ] );
43
+ }
44
+
45
+ public function get_items_permissions_check( $request ) {
46
+ return \ITSEC_Core::current_user_can_manage();
47
+ }
48
+
49
+ public function get_items( $request ) {
50
+ $modules = \ITSEC_Modules::get_available_modules();
51
+
52
+ if ( $request['status'] ) {
53
+ $modules = array_filter( $modules, function ( $module ) use ( $request ) {
54
+ return $this->get_module_status( $module ) === $request['status'];
55
+ } );
56
+ }
57
+
58
+ $responses = [];
59
+
60
+ foreach ( $modules as $module ) {
61
+ if ( ( $config = \ITSEC_Modules::get_config( $module ) ) && ! $config->get_extends() ) {
62
+ $responses[] = $this->prepare_response_for_collection(
63
+ $this->prepare_item_for_response( $config, $request )
64
+ );
65
+ }
66
+ }
67
+
68
+ return new \WP_REST_Response( $responses );
69
+ }
70
+
71
+ public function get_item_permissions_check( $request ) {
72
+ return \ITSEC_Core::current_user_can_manage();
73
+ }
74
+
75
+ public function get_item( $request ) {
76
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
77
+ return new \WP_Error(
78
+ 'rest_module_not_found',
79
+ __( 'No module was found with that id.', 'better-wp-security' ),
80
+ [ 'status' => \WP_Http::NOT_FOUND ]
81
+ );
82
+ }
83
+
84
+ return $this->prepare_item_for_response( $config, $request );
85
+ }
86
+
87
+ public function update_item_permissions_check( $request ) {
88
+ return \ITSEC_Core::current_user_can_manage();
89
+ }
90
+
91
+ public function update_item( $request ) {
92
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
93
+ return new \WP_Error(
94
+ 'rest_module_not_found',
95
+ __( 'No module was found with that id.', 'better-wp-security' ),
96
+ [ 'status' => \WP_Http::NOT_FOUND ]
97
+ );
98
+ }
99
+
100
+ $status = $request['status']['selected'] ?? $this->get_module_status( $config->get_id() );
101
+
102
+ if ( $status !== $this->get_module_status( $config->get_id() ) ) {
103
+ if ( 'active' === $status ) {
104
+ $error = \ITSEC_Modules::activate( $config->get_id() );
105
+ } else {
106
+ $error = \ITSEC_Modules::deactivate( $config->get_id() );
107
+ }
108
+
109
+ if ( is_wp_error( $error ) ) {
110
+ \ITSEC_Lib_REST::add_status_to_error( \WP_Http::BAD_REQUEST, $error );
111
+
112
+ return $error;
113
+ }
114
+ }
115
+
116
+ $request['context'] = 'edit';
117
+
118
+ return $this->prepare_item_for_response( $config, $request );
119
+ }
120
+
121
+ /**
122
+ * Prepares an individual module for response.
123
+ *
124
+ * @param Module_Config $item
125
+ * @param \WP_REST_Request $request
126
+ *
127
+ * @return \WP_REST_Response
128
+ */
129
+ public function prepare_item_for_response( $item, $request ) {
130
+ $item = $item->translate();
131
+
132
+ $data = [
133
+ 'id' => $item->get_id(),
134
+ 'status' => [
135
+ 'selected' => $this->get_module_status( $item->get_id() ),
136
+ 'default' => $item->get_status(),
137
+ ],
138
+ 'type' => $item->get_type(),
139
+ 'onboard' => $item->is_onboard(),
140
+ 'side_effects' => $item->has_side_effects(),
141
+ 'keywords' => $item->get_keywords(),
142
+ 'title' => $item->get_title(),
143
+ 'description' => $item->get_description(),
144
+ 'help' => $item->get_help(),
145
+ 'user_groups' => $item->get_user_groups(),
146
+ 'password_requirements' => $item->get_password_requirements(),
147
+ 'tools' => $item->get_tools() ?: new \stdClass(),
148
+ 'requirements' => $item->get_requirements(),
149
+ ];
150
+
151
+ $fields = $this->get_fields_for_response( $request );
152
+
153
+ if ( $settings = \ITSEC_Modules::get_settings_obj( $item->get_id() ) ) {
154
+ if ( rest_is_field_included( 'settings.schema', $fields ) ) {
155
+ $data['settings']['schema'] = $settings->get_settings_schema();
156
+
157
+ foreach ( $settings->get_defaults() as $setting => $default ) {
158
+ $data['settings']['schema']['properties'][ $setting ]['default'] = $default;
159
+ }
160
+ }
161
+
162
+ if ( rest_is_field_included( 'settings.conditional', $fields ) ) {
163
+ $data['settings']['conditional'] = [];
164
+
165
+ foreach ( $settings->get_conditional_settings() as $setting ) {
166
+ $data['settings']['conditional'][ $setting ] = $settings->get_conditional_setting_config( $setting );
167
+ }
168
+ }
169
+
170
+ if ( rest_is_field_included( 'settings.interactive', $fields ) ) {
171
+ $data['settings']['interactive'] = array_values(
172
+ array_filter( $settings->get_known_settings(), [ $settings, 'is_interactive_setting' ] )
173
+ );
174
+ }
175
+
176
+ if ( rest_is_field_included( 'settings.removed', $fields ) ) {
177
+ $data['settings']['removed'] = $item->get_removed_settings();
178
+ }
179
+
180
+ if ( rest_is_field_included( 'settings.deprecated', $fields ) ) {
181
+ $data['settings']['deprecated'] = $item->get_deprecated_settings();
182
+ }
183
+
184
+ if ( rest_is_field_included( 'settings.onboard', $fields ) ) {
185
+ $data['settings']['onboard'] = $item->get_onboard_settings();
186
+ }
187
+ }
188
+
189
+ $data = $this->add_additional_fields_to_object( $data, $request );
190
+ $data = $this->filter_response_by_context( $data, $request['context'] ?: 'view' );
191
+
192
+ $response = new \WP_REST_Response( $data );
193
+ $response->add_links( $this->prepare_links( $item ) );
194
+
195
+ return $response;
196
+ }
197
+
198
+ /**
199
+ * Gets the module's status.
200
+ *
201
+ * @param string $id The module id.
202
+ *
203
+ * @return string
204
+ */
205
+ private function get_module_status( string $id ): string {
206
+ return \ITSEC_Modules::is_active( $id ) ? 'active' : 'inactive';
207
+ }
208
+
209
+ /**
210
+ * Prepares the list of links to be attached to the module.
211
+ *
212
+ * @param Module_Config $config
213
+ *
214
+ * @return array[]
215
+ */
216
+ private function prepare_links( Module_Config $config ): array {
217
+ $links = [
218
+ 'self' => [
219
+ 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $config->get_id() ) ),
220
+ ]
221
+ ];
222
+
223
+ if ( $config->get_settings() ) {
224
+ $links[ \ITSEC_Lib_REST::get_link_relation( 'settings' ) ] = [
225
+ 'href' => rest_url( sprintf( '%s/settings/%s', $this->namespace, $config->get_id() ) ),
226
+ 'embeddable' => true,
227
+ ];
228
+ }
229
+
230
+ return $links;
231
+ }
232
+
233
+ public function get_item_schema() {
234
+ if ( $this->schema ) {
235
+ return $this->add_additional_fields_schema( $this->schema );
236
+ }
237
+
238
+ $this->schema = [
239
+ 'title' => 'ithemes-security-module',
240
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
241
+ 'type' => 'object',
242
+ 'properties' => [
243
+ 'id' => [
244
+ 'title' => __( 'Module ID' ),
245
+ 'type' => 'string',
246
+ 'context' => [ 'view', 'edit', 'embed' ],
247
+ 'readonly' => true,
248
+ ],
249
+ 'status' => [
250
+ 'title' => __( 'Module Status' ),
251
+ 'type' => 'object',
252
+ 'properties' => [
253
+ 'selected' => [
254
+ 'type' => 'string',
255
+ 'enum' => [ 'active', 'inactive' ],
256
+ 'context' => [ 'view', 'edit', 'embed' ],
257
+ ],
258
+ 'default' => [
259
+ 'type' => 'string',
260
+ 'enum' => [ 'always-active', 'default-active', 'default-inactive' ],
261
+ 'context' => [ 'edit' ],
262
+ 'readonly' => true,
263
+ ],
264
+ ],
265
+ 'context' => [ 'view', 'edit', 'embed' ],
266
+ ],
267
+ 'type' => [
268
+ 'title' => __( 'The Module Type' ),
269
+ 'type' => 'string',
270
+ 'enum' => [ 'recommended', 'advanced', 'tool' ],
271
+ 'context' => [ 'view', 'edit', 'embed' ],
272
+ 'readonly' => true,
273
+ ],
274
+ 'onboard' => [
275
+ 'title' => __( 'Show in Onboard' ),
276
+ 'type' => 'boolean',
277
+ 'context' => [ 'view', 'edit', 'embed' ],
278
+ 'readonly' => true,
279
+ ],
280
+ 'side_effects' => [
281
+ 'title' => __( 'Has Side-Effects' ),
282
+ 'type' => 'boolean',
283
+ 'context' => [ 'edit' ],
284
+ 'readonly' => true,
285
+ ],
286
+ 'keywords' => [
287
+ 'title' => __( 'Module Search Keywords', 'better-wp-security' ),
288
+ 'type' => 'array',
289
+ 'items' => [
290
+ 'type' => 'string',
291
+ ],
292
+ 'context' => [ 'view', 'edit', 'embed' ],
293
+ 'readonly' => true,
294
+ ],
295
+ 'title' => [
296
+ 'title' => __( 'Module Title' ),
297
+ 'type' => 'string',
298
+ 'context' => [ 'view', 'edit', 'embed' ],
299
+ 'readonly' => true,
300
+ ],
301
+ 'description' => [
302
+ 'title' => __( 'Module Description' ),
303
+ 'type' => 'string',
304
+ 'context' => [ 'view', 'edit', 'embed' ],
305
+ 'readonly' => true,
306
+ ],
307
+ 'help' => [
308
+ 'title' => __( 'Module Help' ),
309
+ 'type' => 'string',
310
+ 'context' => [ 'view', 'edit', 'embed' ],
311
+ 'readonly' => true,
312
+ ],
313
+ 'user_groups' => [
314
+ 'title' => __( 'User Groups' ),
315
+ 'type' => 'object',
316
+ 'context' => [ 'edit' ],
317
+ 'readonly' => true,
318
+ ],
319
+ 'password_requirements' => [
320
+ 'title' => __( 'Password Requirements' ),
321
+ 'type' => 'object',
322
+ 'context' => [ 'edit' ],
323
+ 'readonly' => true,
324
+ ],
325
+ 'requirements' => [
326
+ 'title' => __( 'Requirements' ),
327
+ 'type' => 'object',
328
+ 'context' => [ 'edit' ],
329
+ 'readonly' => true,
330
+ ],
331
+ 'tools' => [
332
+ 'title' => __( 'Module Tools', 'better-wp-security' ),
333
+ 'type' => 'object',
334
+ 'additionalProperties' => [
335
+ 'type' => 'object',
336
+ ],
337
+ ],
338
+ 'settings' => [
339
+ 'title' => __( 'Module Settings Configuration' ),
340
+ 'type' => 'object',
341
+ 'properties' => [
342
+ 'schema' => [
343
+ 'description' => __( 'The schema describing the settings.' ),
344
+ 'type' => 'object',
345
+ ],
346
+ 'conditional' => [
347
+ 'description' => __( 'Map of setting names to conditional setting definitions.' ),
348
+ 'type' => 'object',
349
+ ],
350
+ 'interactive' => [
351
+ 'description' => __( 'List of interactive setting names.' ),
352
+ 'type' => 'array',
353
+ 'items' => [
354
+ 'type' => 'string',
355
+ ],
356
+ ],
357
+ 'removed' => [
358
+ 'description' => __( 'List of removed setting names.' ),
359
+ 'type' => 'array',
360
+ 'items' => [
361
+ 'type' => 'string',
362
+ ],
363
+ ],
364
+ 'deprecated' => [
365
+ 'description' => __( 'List of deprecated setting names.' ),
366
+ 'type' => 'array',
367
+ 'items' => [
368
+ 'type' => 'string',
369
+ ],
370
+ ],
371
+ 'onboard' => [
372
+ 'description' => __( 'List of settings to onboard.' ),
373
+ 'type' => 'array',
374
+ 'items' => [
375
+ 'type' => 'string',
376
+ ],
377
+ ]
378
+ ],
379
+ 'context' => [ 'edit' ],
380
+ 'readonly' => true,
381
+ ],
382
+ ],
383
+ ];
384
+
385
+ return $this->add_additional_fields_schema( $this->schema );
386
+ }
387
+
388
+ public function get_collection_params() {
389
+ return [
390
+ 'context' => $this->get_context_param( [ 'default' => 'view' ] ),
391
+ 'status' => [
392
+ 'type' => 'string',
393
+ 'enum' => [
394
+ 'active',
395
+ 'inactive',
396
+ ],
397
+ ],
398
+ ];
399
+ }
400
+ }
core/lib/rest/Settings_Controller.php ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\REST;
4
+
5
+ use iThemesSecurity\Module_Config;
6
+
7
+ final class Settings_Controller extends \WP_REST_Controller {
8
+
9
+ /**
10
+ * Settings_Controller constructor.
11
+ */
12
+ public function __construct() {
13
+ $this->namespace = 'ithemes-security/v1';
14
+ $this->rest_base = 'settings';
15
+ }
16
+
17
+ public function register_routes() {
18
+ register_rest_route( $this->namespace, $this->rest_base, [
19
+ [
20
+ 'methods' => \WP_REST_Server::READABLE,
21
+ 'callback' => [ $this, 'get_items' ],
22
+ 'permission_callback' => [ $this, 'get_items_permissions_check' ],
23
+ 'args' => $this->get_collection_params(),
24
+ ],
25
+ 'schema' => [ $this, 'get_public_item_schema' ],
26
+ ] );
27
+ register_rest_route( $this->namespace, $this->rest_base . '/(?P<id>[\w-]+)', [
28
+ [
29
+ 'methods' => \WP_REST_Server::READABLE,
30
+ 'callback' => [ $this, 'get_item' ],
31
+ 'permission_callback' => [ $this, 'get_item_permissions_check' ],
32
+ ],
33
+ [
34
+ 'methods' => 'PUT',
35
+ 'callback' => [ $this, 'update_item' ],
36
+ 'permission_callback' => [ $this, 'update_item_permissions_check' ],
37
+ 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ),
38
+ ],
39
+ [
40
+ 'methods' => 'PATCH',
41
+ 'callback' => [ $this, 'patch_item' ],
42
+ 'permission_callback' => [ $this, 'patch_item_permissions_check' ],
43
+ 'args' => $this->get_endpoint_args_for_item_schema( 'PATCH' ),
44
+ ],
45
+ [
46
+ 'methods' => \WP_REST_Server::DELETABLE,
47
+ 'callback' => [ $this, 'delete_item' ],
48
+ 'permission_callback' => [ $this, 'delete_item_permissions_check' ],
49
+ ],
50
+ 'allow_batch' => [ 'v1' => true ],
51
+ 'schema' => [ $this, 'get_public_item_schema' ],
52
+ ] );
53
+ }
54
+
55
+ public function get_items_permissions_check( $request ) {
56
+ return \ITSEC_Core::current_user_can_manage();
57
+ }
58
+
59
+ public function get_items( $request ) {
60
+ $responses = [];
61
+
62
+ foreach ( \ITSEC_Modules::get_config_list( $request['modules'] ) as $config ) {
63
+ if ( ! \ITSEC_Modules::get_settings_obj( $config->get_id() ) ) {
64
+ continue;
65
+ }
66
+
67
+ $responses[ $config->get_id() ] = $this->prepare_response_for_collection(
68
+ $this->prepare_item_for_response( $config, $request )
69
+ );
70
+ }
71
+
72
+ return new \WP_REST_Response( $responses );
73
+ }
74
+
75
+ public function get_item_permissions_check( $request ) {
76
+ return \ITSEC_Core::current_user_can_manage();
77
+ }
78
+
79
+ public function get_item( $request ) {
80
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
81
+ return new \WP_Error(
82
+ 'rest_module_not_found',
83
+ __( 'No module was found with that id.', 'better-wp-security' ),
84
+ [ 'status' => \WP_Http::NOT_FOUND ]
85
+ );
86
+ }
87
+
88
+ return $this->prepare_item_for_response( $config, $request );
89
+ }
90
+
91
+ public function update_item_permissions_check( $request ) {
92
+ return \ITSEC_Core::current_user_can_manage();
93
+ }
94
+
95
+ public function update_item( $request ) {
96
+ \ITSEC_Core::set_interactive();
97
+
98
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
99
+ return new \WP_Error(
100
+ 'rest_module_not_found',
101
+ __( 'No module was found with that id.', 'better-wp-security' ),
102
+ [ 'status' => \WP_Http::NOT_FOUND ]
103
+ );
104
+ }
105
+
106
+ $updated = \ITSEC_Modules::set_settings( $config->get_id(), $request->get_json_params() ?: $request->get_body_params() );
107
+ $updated = \ITSEC_Lib::updated_settings_to_wp_error( $updated );
108
+
109
+ if ( is_wp_error( $updated ) ) {
110
+ \ITSEC_Lib_REST::add_status_to_error( \WP_Http::BAD_REQUEST, $updated );
111
+
112
+ return $updated;
113
+ }
114
+
115
+ return $this->prepare_item_for_response( $config, $request );
116
+ }
117
+
118
+ public function patch_item_permissions_check( $request ) {
119
+ return \ITSEC_Core::current_user_can_manage();
120
+ }
121
+
122
+ public function patch_item( $request ) {
123
+ \ITSEC_Core::set_interactive();
124
+
125
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
126
+ return new \WP_Error(
127
+ 'rest_module_not_found',
128
+ __( 'No module was found with that id.', 'better-wp-security' ),
129
+ [ 'status' => \WP_Http::NOT_FOUND ]
130
+ );
131
+ }
132
+
133
+ $current = \ITSEC_Modules::get_settings( $config->get_id() );
134
+
135
+ foreach ( $request->get_json_params() ?: $request->get_body_params() as $setting => $value ) {
136
+ $current[ $setting ] = $value;
137
+ }
138
+
139
+ $updated = \ITSEC_Modules::set_settings( $config->get_id(), $current );
140
+ $updated = \ITSEC_Lib::updated_settings_to_wp_error( $updated );
141
+
142
+ if ( is_wp_error( $updated ) ) {
143
+ \ITSEC_Lib_REST::add_status_to_error( \WP_Http::BAD_REQUEST, $updated );
144
+
145
+ return $updated;
146
+ }
147
+
148
+ return $this->prepare_item_for_response( $config, $request );
149
+ }
150
+
151
+ public function delete_item_permissions_check( $request ) {
152
+ return \ITSEC_Core::current_user_can_manage();
153
+ }
154
+
155
+ public function delete_item( $request ) {
156
+ \ITSEC_Core::set_interactive();
157
+
158
+ if ( ! $config = \ITSEC_Modules::get_config( $request['id'] ) ) {
159
+ return new \WP_Error(
160
+ 'rest_module_not_found',
161
+ __( 'No module was found with that id.', 'better-wp-security' ),
162
+ [ 'status' => \WP_Http::NOT_FOUND ]
163
+ );
164
+ }
165
+
166
+ $defaults = \ITSEC_Modules::get_defaults( $config->get_id() );
167
+ $updated = \ITSEC_Modules::set_settings( $config->get_id(), $defaults );
168
+ $updated = \ITSEC_Lib::updated_settings_to_wp_error( $updated );
169
+
170
+ if ( is_wp_error( $updated ) ) {
171
+ \ITSEC_Lib_REST::add_status_to_error( \WP_Http::BAD_REQUEST, $updated );
172
+
173
+ return $updated;
174
+ }
175
+
176
+ return $this->prepare_item_for_response( $config, $request );
177
+ }
178
+
179
+ /**
180
+ * Prepares an individual module's settings for response.
181
+ *
182
+ * @param Module_Config $item
183
+ * @param \WP_REST_Request $request
184
+ *
185
+ * @return \WP_REST_Response|\WP_Error
186
+ */
187
+ public function prepare_item_for_response( $item, $request ) {
188
+ $settings = \ITSEC_Modules::get_settings( $item->get_id() );
189
+ $obj = \ITSEC_Modules::get_settings_obj( $item->get_id() );
190
+
191
+ if ( ! $obj ) {
192
+ return new \WP_Error( 'rest_unsupported_module', __( 'This module does not have settings.', 'better-wp-security' ) );
193
+ }
194
+
195
+ $schema = $obj->get_settings_schema();
196
+
197
+ foreach ( $settings as $setting => $value ) {
198
+ if ( ! is_array( $value ) || $value ) {
199
+ continue;
200
+ }
201
+
202
+ $type = $schema['properties'][ $setting ]['type'] ?? null;
203
+
204
+ if ( 'object' !== $type ) {
205
+ continue;
206
+ }
207
+
208
+ $settings[ $setting ] = new \stdClass();
209
+ }
210
+
211
+ return new \WP_REST_Response( $settings );
212
+ }
213
+
214
+ public function get_item_schema() {
215
+ return [
216
+ 'title' => 'ithemes-security-settings',
217
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
218
+ 'type' => 'object',
219
+ 'properties' => [],
220
+ ];
221
+ }
222
+
223
+ public function get_collection_params() {
224
+ return [
225
+ 'modules' => [
226
+ 'oneOf' => [
227
+ [
228
+ 'type' => 'string',
229
+ 'enum' => [ ':all', ':active' ],
230
+ ],
231
+ [
232
+ 'type' => 'array',
233
+ 'items' => [
234
+ 'type' => 'string'
235
+ ]
236
+ ]
237
+ ],
238
+ 'default' => ':all',
239
+ ]
240
+ ];
241
+ }
242
+ }
core/lib/rest/Site_Types_Controller.php ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\REST;
4
+
5
+ use iThemesSecurity\Lib\Site_Types;
6
+ use iThemesSecurity\User_Groups\User_Group;
7
+
8
+ final class Site_Types_Controller extends \WP_REST_Controller {
9
+
10
+ /** @var Site_Types\Registry */
11
+ private $registry;
12
+
13
+ /** @var Site_Types\Defaults */
14
+ private $defaults;
15
+
16
+ /**
17
+ * Site_Types_Controller constructor.
18
+ *
19
+ * @param Site_Types\Registry $registry
20
+ * @param Site_Types\Defaults $defaults
21
+ */
22
+ public function __construct( Site_Types\Registry $registry, Site_Types\Defaults $defaults ) {
23
+ $this->registry = $registry;
24
+ $this->defaults = $defaults;
25
+ $this->namespace = 'ithemes-security/v1';
26
+ $this->rest_base = 'site-types';
27
+ }
28
+
29
+ public function register_routes() {
30
+ register_rest_route( $this->namespace, $this->rest_base, [
31
+ [
32
+ 'methods' => \WP_REST_Server::READABLE,
33
+ 'callback' => [ $this, 'get_items' ],
34
+ 'permission_callback' => [ $this, 'get_items_permissions_check' ],
35
+ ],
36
+ 'schema' => [ $this, 'get_public_item_schema' ],
37
+ ] );
38
+
39
+ register_rest_route( $this->namespace, $this->rest_base . '/(?P<id>[\w-]+)', [
40
+ [
41
+ 'methods' => \WP_REST_Server::READABLE,
42
+ 'callback' => [ $this, 'get_item' ],
43
+ 'permission_callback' => [ $this, 'get_item_permissions_check' ],
44
+ ],
45
+ [
46
+ 'methods' => 'PUT',
47
+ 'callback' => [ $this, 'update_item' ],
48
+ 'permission_callback' => [ $this, 'update_item_permissions_check' ],
49
+ 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ),
50
+ ],
51
+ 'schema' => [ $this, 'get_public_item_schema' ],
52
+ ] );
53
+ }
54
+
55
+ public function get_items_permissions_check( $request ) {
56
+ return \ITSEC_Core::current_user_can_manage();
57
+ }
58
+
59
+ public function get_items( $request ) {
60
+ return array_map( function ( Site_Types\Site_Type $site_type ) use ( $request ) {
61
+ return $this->prepare_response_for_collection(
62
+ $this->prepare_item_for_response( new Site_Types\Controller( $site_type ), $request )
63
+ );
64
+ }, $this->registry->get_site_types() );
65
+ }
66
+
67
+ public function get_item_permissions_check( $request ) {
68
+ return \ITSEC_Core::current_user_can_manage();
69
+ }
70
+
71
+ public function get_item( $request ) {
72
+ if ( ! $site_type = $this->registry->get_by_slug( $request['id'] ) ) {
73
+ return new \WP_Error( 'rest_not_found', __( 'Site Type not found.', 'better-wp-security' ), [ 'status' => \WP_Http::NOT_FOUND ] );
74
+ }
75
+
76
+ return $this->prepare_item_for_response( new Site_Types\Controller( $site_type ), $request );
77
+ }
78
+
79
+ public function update_item_permissions_check( $request ) {
80
+ return \ITSEC_Core::current_user_can_manage();
81
+ }
82
+
83
+ public function update_item( $request ) {
84
+ $controller = $this->prepare_item_for_database( $request );
85
+
86
+ if ( is_wp_error( $controller ) ) {
87
+ return $controller;
88
+ }
89
+
90
+ return $this->prepare_item_for_response( $controller, $request );
91
+ }
92
+
93
+ protected function prepare_item_for_database( $request ) {
94
+ if ( ! $site_type = $this->registry->get_by_slug( $request['id'] ) ) {
95
+ return new \WP_Error( 'rest_not_found', __( 'Site Type not found.', 'better-wp-security' ), [ 'status' => \WP_Http::NOT_FOUND ] );
96
+ }
97
+
98
+ $answers = [];
99
+ $latest = null;
100
+
101
+ foreach ( $request['answers'] ?? [] as $i => $answer ) {
102
+ if ( $i === count( $request['answers'] ) - 1 ) {
103
+ $latest = $answer;
104
+ break;
105
+ }
106
+
107
+ $question = \ITSEC_Lib::find_where( $site_type->get_questions(), static function ( Site_Types\Question $question ) use ( $answer ) {
108
+ return $question->get_id() === $answer['question'];
109
+ } );
110
+
111
+ $user_groups = array_map( function ( $user_group ) {
112
+ return ( new User_Group( $user_group['id'] ) )
113
+ ->set_label( $user_group['label'] )
114
+ ->set_users( array_filter( array_map( 'get_userdata', $user_group['users'] ) ) )
115
+ ->set_roles( $user_group['roles'] )
116
+ ->set_canonical_roles( $user_group['canonical'] )
117
+ ->set_min_role( $user_group['min_role'] );
118
+ }, $answer['user_groups'] );
119
+
120
+ $answers[] = new Site_Types\Answered_Question(
121
+ $question,
122
+ $answer['answer'],
123
+ $user_groups ?? [],
124
+ $answer['user_groups_settings'] ?? [],
125
+ $answer['canonical_group_substitutions'] ?? [],
126
+ $answer['modules'] ?? [],
127
+ $answer['settings'] ?? []
128
+ );
129
+ }
130
+
131
+ $controller = new Site_Types\Controller( $site_type, $answers );
132
+
133
+ if ( null !== $latest ) {
134
+ $question = \ITSEC_Lib::find_where( $site_type->get_questions(), static function ( Site_Types\Question $question ) use ( $latest ) {
135
+ return $question->get_id() === $latest['question'];
136
+ } );
137
+
138
+ $answered = $controller->answer( $question, $latest['answer'] );
139
+
140
+ if ( is_wp_error( $answered ) ) {
141
+ return $answered;
142
+ }
143
+ }
144
+
145
+ return $controller;
146
+ }
147
+
148
+ /**
149
+ * Prepares a site type controller for a REST API response.
150
+ *
151
+ * @param Site_Types\Controller $item
152
+ * @param \WP_REST_Request $request
153
+ *
154
+ * @return \WP_REST_Response
155
+ */
156
+ public function prepare_item_for_response( $item, $request ) {
157
+ if ( ! $site_type = $item->get_selected_site_type() ) {
158
+ return new \WP_REST_Response();
159
+ }
160
+
161
+ $data = [
162
+ 'id' => $site_type->get_slug(),
163
+ 'title' => $site_type->get_title(),
164
+ 'description' => $site_type->get_description(),
165
+ 'icon' => $site_type->get_icon(),
166
+ 'recommended' => $site_type->get_slug() === $this->defaults->get_suggested_site_type(),
167
+ 'next_question' => null,
168
+ 'answers' => [],
169
+ ];
170
+
171
+ if ( $next = $item->get_next_question() ) {
172
+ $data['next_question'] = [
173
+ 'id' => $next->get_id(),
174
+ 'prompt' => $next->get_prompt(),
175
+ 'description' => $next->get_description(),
176
+ 'answer_schema' => $next->get_answer_schema(),
177
+ ];
178
+
179
+ if ( null !== ( $default = $this->defaults->get_default_for_question( $next->get_id() ) ) ) {
180
+ $data['next_question']['answer_schema']['default'] = $default;
181
+ }
182
+ }
183
+
184
+ foreach ( $item->get_previous() as $previous ) {
185
+ $data['answers'][] = [
186
+ 'question' => $previous->get_question()->get_id(),
187
+ 'answer' => $previous->get_answer(),
188
+ 'modules' => $previous->get_enabled_modules(),
189
+ 'settings' => $previous->get_settings(),
190
+ 'user_groups' => array_map(
191
+ function ( $user_group ) { return $user_group->jsonSerialize(); },
192
+ $previous->get_user_groups()
193
+ ),
194
+ 'user_groups_settings' => $previous->get_user_group_settings(),
195
+ 'canonical_group_substitutions' => $previous->get_canonical_user_group_substitutions(),
196
+ ];
197
+ }
198
+
199
+ return new \WP_REST_Response( $data );
200
+ }
201
+
202
+ public function get_item_schema() {
203
+ if ( ! $this->schema ) {
204
+ $this->schema = [
205
+ 'title' => 'ithemes-security-site-type',
206
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
207
+ 'type' => 'object',
208
+ 'properties' => [
209
+ 'id' => [
210
+ 'description' => __( 'The unique id identifying the site type.', 'better-wp-security' ),
211
+ 'type' => 'string',
212
+ 'readonly' => true,
213
+ ],
214
+ 'title' => [
215
+ 'description' => __( 'The title of the Site Type.', 'better-wp-security' ),
216
+ 'type' => 'string',
217
+ 'readonly' => true,
218
+ ],
219
+ 'description' => [
220
+ 'description' => __( 'The description of the Site Type.', 'better-wp-security' ),
221
+ 'type' => 'string',
222
+ 'readonly' => true,
223
+ ],
224
+ 'icon' => [
225
+ 'description' => __( 'The name of the icon representing the Site Type.', 'better-wp-security' ),
226
+ 'type' => 'string',
227
+ 'readonly' => true,
228
+ ],
229
+ 'next_question' => [
230
+ 'description' => __( 'The next question to ask the user.', 'better-wp-security' ),
231
+ 'type' => 'object',
232
+ 'readonly' => true,
233
+ 'properties' => [
234
+ 'id' => [
235
+ 'type' => 'string',
236
+ ],
237
+ 'prompt' => [
238
+ 'type' => 'string',
239
+ ],
240
+ 'answer_schema' => [
241
+ 'type' => 'string',
242
+ ],
243
+ ],
244
+ ],
245
+ 'answers' => [
246
+ 'description' => __( 'The list of answers.', 'better-wp-security' ),
247
+ 'type' => 'array',
248
+ 'items' => [
249
+ 'type' => 'object',
250
+ 'properties' => [
251
+ 'question' => [
252
+ 'description' => __( 'The question id.', 'better-wp-security' ),
253
+ 'type' => 'string',
254
+ ],
255
+ 'answer' => [
256
+ 'description' => __( 'The user provided answer.', 'better-wp-security' ),
257
+ 'type' => [ 'array', 'object', 'boolean', 'number', 'integer', 'string' ],
258
+ ],
259
+ 'modules' => [
260
+ 'type' => 'array',
261
+ 'readonly' => true,
262
+ 'items' => [
263
+ 'type' => 'string',
264
+ ],
265
+ ],
266
+ 'settings' => [
267
+ 'type' => 'object',
268
+ 'readonly' => true,
269
+ ],
270
+ 'user_groups' => [
271
+ 'type' => 'array',
272
+ 'readonly' => true,
273
+ 'items' => [
274
+ 'type' => 'object',
275
+ ],
276
+ ],
277
+ 'user_groups_settings' => [
278
+ 'type' => 'object',
279
+ 'readonly' => true,
280
+ 'additionalProperties' => [
281
+ 'type' => 'object',
282
+ ],
283
+ ],
284
+ 'canonical_group_substitutions' => [
285
+ 'type' => 'object',
286
+ 'readonly' => true,
287
+ 'additionalProperties' => [
288
+ 'type' => 'string',
289
+ ],
290
+ ],
291
+ ],
292
+ ],
293
+ 'arg_options' => [
294
+ 'validate_callback' => function ( $value ) {
295
+ if ( ! is_array( $value ) ) {
296
+ return new \WP_Error( 'rest_invalid_type', __( 'Answers must be an array.', 'better-wp-security' ) );
297
+ }
298
+
299
+ foreach ( $value as $i => $item ) {
300
+ if ( ! is_array( $item ) ) {
301
+ return new \WP_Error( 'rest_invalid_type', sprintf( __( 'Answers entry %d must be an object.', 'better-wp-security' ), $i ) );
302
+ }
303
+
304
+ if ( ! isset( $item['question'], $item['answer'] ) ) {
305
+ return new \WP_Error( 'rest_property_required', sprintf( __( 'The question and answer properties are required for answers entry %d.', 'better-wp-security' ), $i ) );
306
+ }
307
+
308
+ if ( ! is_string( $item['question'] ) ) {
309
+ return new \WP_Error( 'rest_invalid_type', sprintf( __( 'The question property must be a string for answers entry %d.', 'better-wp-security' ), $i ) );
310
+ }
311
+ }
312
+
313
+ return true;
314
+ },
315
+ 'sanitize_callback' => function ( $value ) {
316
+ return is_array( $value ) ? $value : [];
317
+ },
318
+ ],
319
+ ],
320
+ ],
321
+ ];
322
+ }
323
+
324
+ return $this->schema;
325
+ }
326
+ }
core/lib/rest/Tools_Controller.php ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\REST;
4
+
5
+ use iThemesSecurity\Lib\Tools\Tool;
6
+ use iThemesSecurity\Lib\Tools\Tools_Registry;
7
+ use iThemesSecurity\Lib\Tools\Tools_Runner;
8
+
9
+ final class Tools_Controller extends \WP_REST_Controller {
10
+
11
+ /** @var Tools_Registry */
12
+ private $registry;
13
+
14
+ /** @var Tools_Runner */
15
+ private $runner;
16
+
17
+ /**
18
+ * Tools_Controller constructor.
19
+ *
20
+ * @param Tools_Registry $registry
21
+ * @param Tools_Runner $runner
22
+ */
23
+ public function __construct( Tools_Registry $registry, Tools_Runner $runner ) {
24
+ $this->registry = $registry;
25
+ $this->runner = $runner;
26
+
27
+ $this->namespace = 'ithemes-security/v1';
28
+ $this->rest_base = 'tools';
29
+ }
30
+
31
+ public function register_routes() {
32
+ register_rest_route( $this->namespace, $this->rest_base, [
33
+ [
34
+ 'methods' => \WP_REST_Server::READABLE,
35
+ 'callback' => [ $this, 'get_items' ],
36
+ 'permission_callback' => [ $this, 'get_items_permissions_check' ],
37
+ ],
38
+ 'schema' => [ $this, 'get_public_item_schema' ],
39
+ ] );
40
+
41
+ register_rest_route( $this->namespace, $this->rest_base . '/(?P<tool>[\w\-]+)', [
42
+ [
43
+ 'methods' => \WP_REST_Server::READABLE,
44
+ 'callback' => [ $this, 'get_item' ],
45
+ 'permission_callback' => [ $this, 'get_item_permissions_check' ],
46
+ ],
47
+ [
48
+ 'methods' => 'PUT',
49
+ 'callback' => [ $this, 'update_item' ],
50
+ 'permission_callback' => [ $this, 'update_item_permissions_check' ],
51
+ 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ),
52
+ ],
53
+ [
54
+ 'methods' => \WP_REST_Server::CREATABLE,
55
+ 'callback' => [ $this, 'run_tool' ],
56
+ 'permission_callback' => [ $this, 'run_tool_permissions_check' ],
57
+ ],
58
+ 'schema' => [ $this, 'get_public_item_schema' ],
59
+ ] );
60
+ }
61
+
62
+ public function get_items_permissions_check( $request ) {
63
+ return \ITSEC_Core::current_user_can_manage();
64
+ }
65
+
66
+ public function get_items( $request ) {
67
+ $items = [];
68
+
69
+ foreach ( $this->registry->get_tools() as $tool ) {
70
+ $items[] = $this->prepare_response_for_collection(
71
+ $this->prepare_item_for_response( $tool, $request )
72
+ );
73
+ }
74
+
75
+ return rest_ensure_response( $items );
76
+ }
77
+
78
+ public function get_item_permissions_check( $request ) {
79
+ return \ITSEC_Core::current_user_can_manage();
80
+ }
81
+
82
+ public function get_item( $request ) {
83
+ $slug = $request['tool'];
84
+
85
+ if ( ! $this->registry->is_registered( $slug ) ) {
86
+ return new \WP_Error(
87
+ 'rest_not_found',
88
+ __( 'Tool not found.', 'better-wp-security' ),
89
+ [ 'status' => \WP_Http::NOT_FOUND ]
90
+ );
91
+ }
92
+
93
+ $tool = $this->registry->get_tool( $slug );
94
+
95
+ return $this->prepare_item_for_response( $tool, $request );
96
+ }
97
+
98
+ public function update_item_permissions_check( $request ) {
99
+ return \ITSEC_Core::current_user_can_manage();
100
+ }
101
+
102
+ public function update_item( $request ) {
103
+ $slug = $request['tool'];
104
+
105
+ if ( ! $this->registry->is_registered( $slug ) ) {
106
+ return new \WP_Error(
107
+ 'rest_not_found',
108
+ __( 'Tool not found.', 'better-wp-security' ),
109
+ [ 'status' => \WP_Http::NOT_FOUND ]
110
+ );
111
+ }
112
+
113
+ $tool = $this->registry->get_tool( $slug );
114
+
115
+ if ( $request->has_param( 'enabled' ) ) {
116
+ if ( $request['enabled'] ) {
117
+ $toggled = $this->runner->enable_tool( $tool );
118
+ } else {
119
+ $toggled = $this->runner->disable_tool( $tool );
120
+ }
121
+
122
+ if ( is_wp_error( $toggled ) ) {
123
+ return $toggled;
124
+ }
125
+ }
126
+
127
+ return $this->prepare_item_for_response( $tool, $request );
128
+ }
129
+
130
+ public function run_tool( \WP_REST_Request $request ) {
131
+ $slug = $request['tool'];
132
+
133
+ if ( ! $this->registry->is_registered( $slug ) ) {
134
+ return new \WP_Error(
135
+ 'rest_not_found',
136
+ __( 'Tool not found.', 'better-wp-security' ),
137
+ [ 'status' => \WP_Http::NOT_FOUND ]
138
+ );
139
+ }
140
+
141
+ $tool = $this->registry->get_tool( $slug );
142
+ $form = $request->get_json_params() ?: $request->get_body_params();
143
+
144
+ return $this->runner->run_tool( $tool, $form )->as_rest_response();
145
+ }
146
+
147
+ public function run_tool_permissions_check( \WP_REST_Request $request ) {
148
+ return \ITSEC_Core::current_user_can_manage();
149
+ }
150
+
151
+ /**
152
+ * Prepares a tool for the REST API response.
153
+ *
154
+ * @param Tool $item
155
+ * @param \WP_REST_Request $request
156
+ *
157
+ * @return \WP_REST_Response
158
+ */
159
+ public function prepare_item_for_response( $item, $request ) {
160
+ $data = [
161
+ 'slug' => $item->get_slug(),
162
+ 'module' => $item->get_module(),
163
+ 'title' => $item->get_title(),
164
+ 'description' => $item->get_description(),
165
+ 'help' => $item->get_help(),
166
+ 'keywords' => $item->get_keywords(),
167
+ 'available' => $item->is_available(),
168
+ 'condition' => $item->get_condition() ?: null,
169
+ 'toggleable' => $item->is_toggleable(),
170
+ 'schedule' => $item->get_schedule(),
171
+ 'form' => $item->get_form() ?: null,
172
+ ];
173
+
174
+ if ( $item->is_toggleable() ) {
175
+ $data['enabled'] = $this->runner->is_enabled( $item );
176
+ }
177
+
178
+ $response = new \WP_REST_Response( $data );
179
+ $response->add_link( 'self', rest_url( sprintf(
180
+ '%s/%s/%s',
181
+ $this->namespace,
182
+ $this->rest_base,
183
+ $item->get_slug()
184
+ ) ) );
185
+
186
+ return $response;
187
+ }
188
+
189
+ public function get_item_schema() {
190
+ if ( $this->schema ) {
191
+ return $this->schema;
192
+ }
193
+
194
+ $this->schema = [
195
+ 'title' => 'ithemes-security-tool',
196
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
197
+ 'type' => 'object',
198
+ 'properties' => [
199
+ 'slug' => [
200
+ 'type' => 'string',
201
+ 'context' => [ 'view', 'edit', 'embed' ],
202
+ 'readonly' => true,
203
+ ],
204
+ 'module' => [
205
+ 'type' => 'string',
206
+ 'context' => [ 'view', 'edit', 'embed' ],
207
+ 'readonly' => true,
208
+ ],
209
+ 'title' => [
210
+ 'type' => 'string',
211
+ 'context' => [ 'view', 'edit', 'embed' ],
212
+ 'readonly' => true,
213
+ ],
214
+ 'description' => [
215
+ 'type' => 'string',
216
+ 'context' => [ 'view', 'edit', 'embed' ],
217
+ 'readonly' => true,
218
+ ],
219
+ 'help' => [
220
+ 'type' => 'string',
221
+ 'context' => [ 'view', 'edit', 'embed' ],
222
+ 'readonly' => true,
223
+ ],
224
+ 'keywords' => [
225
+ 'type' => 'array',
226
+ 'items' => [
227
+ 'type' => 'string',
228
+ ],
229
+ 'context' => [ 'view', 'edit', 'embed' ],
230
+ 'readonly' => true,
231
+ ],
232
+ 'available' => [
233
+ 'type' => 'string',
234
+ 'context' => [ 'view', 'edit', 'embed' ],
235
+ 'readonly' => true,
236
+ ],
237
+ 'condition' => [
238
+ 'type' => [ 'object', 'null' ],
239
+ 'context' => [ 'view', 'edit', 'embed' ],
240
+ 'readonly' => true,
241
+ ],
242
+ 'toggleable' => [
243
+ 'type' => 'boolean',
244
+ 'context' => [ 'view', 'edit', 'embed' ],
245
+ 'readonly' => true,
246
+ ],
247
+ 'enabled' => [
248
+ 'type' => 'boolean',
249
+ 'context' => [ 'view', 'edit', 'embed' ],
250
+ ],
251
+ 'schedule' => [
252
+ 'type' => 'string',
253
+ 'context' => [ 'view', 'edit', 'embed' ],
254
+ 'readonly' => true,
255
+ ],
256
+ 'form' => [
257
+ 'type' => [ 'object', 'null' ],
258
+ 'context' => [ 'view', 'edit', 'embed' ],
259
+ 'readonly' => true,
260
+ ],
261
+ ],
262
+ ];
263
+
264
+ return $this->schema;
265
+ }
266
+ }
core/lib/rest/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/schema.php CHANGED
@@ -12,6 +12,7 @@ final class ITSEC_Schema {
12
  'itsec_user_groups',
13
  'itsec_mutexes',
14
  'itsec_bans',
 
15
  ];
16
 
17
  /**
@@ -171,6 +172,16 @@ CREATE TABLE {$wpdb->base_prefix}itsec_bans (
171
  UNIQUE KEY host (host),
172
  KEY actor (actor_type,actor_id)
173
  ) $charset_collate;
 
 
 
 
 
 
 
 
 
 
174
  ";
175
 
176
  $wp_error = new WP_Error();
12
  'itsec_user_groups',
13
  'itsec_mutexes',
14
  'itsec_bans',
15
+ 'itsec_dashboard_events',
16
  ];
17
 
18
  /**
172
  UNIQUE KEY host (host),
173
  KEY actor (actor_type,actor_id)
174
  ) $charset_collate;
175
+
176
+ CREATE TABLE {$wpdb->base_prefix}itsec_dashboard_events (
177
+ event_id int(11) unsigned NOT NULL AUTO_INCREMENT,
178
+ event_slug varchar(128) NOT NULL DEFAULT '',
179
+ event_time datetime NOT NULL,
180
+ event_count int(11) unsigned NOT NULL DEFAULT '1',
181
+ event_consolidated tinyint(1) NOT NULL DEFAULT '0',
182
+ PRIMARY KEY (`event_id`),
183
+ UNIQUE KEY `event_slug__time__consolidated` (event_slug,event_time,event_consolidated)
184
+ ) $charset_collate;
185
  ";
186
 
187
  $wp_error = new WP_Error();
core/lib/settings.php CHANGED
@@ -1,19 +1,200 @@
1
  <?php
2
 
 
3
  use iThemesSecurity\User_Groups;
4
 
5
  abstract class ITSEC_Settings {
 
 
 
 
 
6
  protected $settings;
7
 
8
- public function __construct() {
 
 
 
 
 
 
9
  $this->load();
10
-
11
- add_action( 'itsec-lib-clear-caches', array( $this, 'load' ), 0 );
12
  }
13
 
 
 
 
 
 
14
  abstract public function get_id();
 
 
 
 
 
 
15
  abstract public function get_defaults();
16
- protected function after_save() {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  protected function handle_settings_changes( $old_settings ) {
19
  $user_group_settings = ITSEC_Modules::get_container()->get( User_Groups\Settings_Registry::class );
@@ -23,7 +204,7 @@ abstract class ITSEC_Settings {
23
  continue;
24
  }
25
 
26
- $current = ITSEC_Lib::array_get( $this->settings, $user_group_setting->get_setting() );
27
  $previous = ITSEC_Lib::array_get( $old_settings, $user_group_setting->get_setting() );
28
 
29
  if ( $previous !== $current ) {
@@ -42,8 +223,8 @@ abstract class ITSEC_Settings {
42
  }
43
 
44
  public function get( $name, $default = null ) {
45
- if ( isset( $this->settings[$name] ) ) {
46
- return $this->settings[$name];
47
  }
48
 
49
  return $default;
@@ -54,8 +235,8 @@ abstract class ITSEC_Settings {
54
  }
55
 
56
  public function set( $name, $value ) {
57
- $settings = $this->settings;
58
- $settings[$name] = $value;
59
 
60
  return $this->set_all( $settings );
61
  }
@@ -76,7 +257,7 @@ abstract class ITSEC_Settings {
76
  } else {
77
  $validator->validate( $settings );
78
 
79
- $retval['errors'] = $validator->get_errors();
80
  $retval['messages'] = $validator->get_messages();
81
 
82
  if ( $validator->can_save() ) {
@@ -87,7 +268,7 @@ abstract class ITSEC_Settings {
87
  $this->handle_settings_changes( $retval['old_settings'] );
88
 
89
  $retval['new_settings'] = $this->settings;
90
- $retval['saved'] = true;
91
 
92
  do_action( 'itsec-settings-updated', $this->get_id() );
93
  } else {
@@ -103,12 +284,15 @@ abstract class ITSEC_Settings {
103
 
104
  public function load() {
105
  $this->settings = ITSEC_Storage::get( $this->get_id() );
106
- $defaults = $this->get_defaults();
107
 
108
  if ( ! is_array( $this->settings ) ) {
109
  $this->settings = array();
110
  }
111
 
112
- $this->settings = array_merge( $defaults, $this->settings );
 
 
 
 
113
  }
114
  }
1
  <?php
2
 
3
+ use iThemesSecurity\Module_Config;
4
  use iThemesSecurity\User_Groups;
5
 
6
  abstract class ITSEC_Settings {
7
+
8
+ /** @var Module_Config|null */
9
+ protected $config;
10
+
11
+ /** @var array */
12
  protected $settings;
13
 
14
+ /**
15
+ * ITSEC_Settings constructor.
16
+ *
17
+ * @param Module_Config|null $config The configuration object. If omitted, will attempt to retrieve it.
18
+ */
19
+ public function __construct( Module_Config $config = null ) {
20
+ $this->config = $config ?: ITSEC_Modules::get_config( $this->get_id() );
21
  $this->load();
 
 
22
  }
23
 
24
+ /**
25
+ * Gets the module ID this settings class backs.
26
+ *
27
+ * @return string
28
+ */
29
  abstract public function get_id();
30
+
31
+ /**
32
+ * Gets the list of default setting values.
33
+ *
34
+ * @return array
35
+ */
36
  abstract public function get_defaults();
37
+
38
+ /**
39
+ * Gets the default value for a particular setting.
40
+ *
41
+ * @param string $setting The setting slug.
42
+ * @param mixed $default The default value to use if the module did not declare one.
43
+ *
44
+ * @return mixed
45
+ */
46
+ public function get_default( $setting, $default = null ) {
47
+ $defaults = $this->get_defaults();
48
+
49
+ if ( array_key_exists( $setting, $defaults ) ) {
50
+ return $defaults[ $setting ];
51
+ }
52
+
53
+ return $default;
54
+ }
55
+
56
+ /**
57
+ * Gets the settings schema.
58
+ *
59
+ * @return array
60
+ */
61
+ public function get_settings_schema() {
62
+ return [];
63
+ }
64
+
65
+ /**
66
+ * Gets the list of known settings slugs.
67
+ *
68
+ * @return string[]
69
+ */
70
+ public function get_known_settings() {
71
+ return array_keys( $this->get_defaults() );
72
+ }
73
+
74
+ /**
75
+ * Checks if this is a known setting name.
76
+ *
77
+ * @param string $setting The setting slug.
78
+ *
79
+ * @return bool
80
+ */
81
+ public function is_known_setting( $setting ) {
82
+ return isset( $this->get_defaults()[ $setting ] );
83
+ }
84
+
85
+ /**
86
+ * Checks if this module has any interactive settings.
87
+ *
88
+ * @return bool
89
+ */
90
+ final public function has_interactive_settings() {
91
+ foreach ( $this->get_known_settings() as $setting ) {
92
+ if ( $this->is_interactive_setting( $setting ) ) {
93
+ return true;
94
+ }
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * Checks if this setting can be set by the user, or is it wholly managed by the module.
102
+ *
103
+ * @param string $setting The setting name.
104
+ *
105
+ * @return bool
106
+ */
107
+ public function is_interactive_setting( $setting ) {
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Gets the list of conditional settings slugs.
113
+ *
114
+ * @return array
115
+ */
116
+ public function get_conditional_settings() {
117
+ return array_filter( $this->get_known_settings(), [ $this, 'is_conditional_setting' ] );
118
+ }
119
+
120
+ /**
121
+ * Checks if the setting is conditionally rendered, or is it always available.
122
+ *
123
+ * @param string $setting The setting name.
124
+ *
125
+ * @return bool
126
+ */
127
+ public function is_conditional_setting( $setting ) {
128
+ return false;
129
+ }
130
+
131
+ /**
132
+ * Gets the configuration for a conditional setting to be active.
133
+ *
134
+ * @param string $setting The setting name.
135
+ *
136
+ * @return array
137
+ */
138
+ public function get_conditional_setting_config( $setting ) {
139
+ return [];
140
+ }
141
+
142
+ /**
143
+ * Checks if a conditional setting is currently visible.
144
+ *
145
+ * @param string $setting The setting name.
146
+ * @param array|null $settings Optionally, compares against the given settings
147
+ * instead of the saved settings.
148
+ *
149
+ * @return bool
150
+ */
151
+ public function is_conditional_setting_active( string $setting, array $settings = null ): bool {
152
+ $settings = $settings ?? $this->settings;
153
+
154
+ if ( ! $this->is_conditional_setting( $setting ) ) {
155
+ return false;
156
+ }
157
+
158
+ $config = $this->get_conditional_setting_config( $setting );
159
+
160
+ if ( isset( $config['settings'] ) ) {
161
+ if ( is_wp_error( rest_validate_value_from_schema( $settings, $config['settings'] ) ) ) {
162
+ return false;
163
+ }
164
+
165
+ if ( is_wp_error( rest_sanitize_value_from_schema( $settings, $config['settings'] ) ) ) {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ if ( isset( $config['server-type'] ) && ! in_array( ITSEC_Lib::get_server(), $config['server-type'], true ) ) {
171
+ return false;
172
+ }
173
+
174
+ if ( isset( $config['install-type'] ) && $config['install-type'] !== ITSEC_Core::get_install_type() ) {
175
+ return false;
176
+ }
177
+
178
+ if ( isset( $config['active-modules'] ) ) {
179
+ $active = array_filter( $config['active-modules'], 'ITSEC_Modules::is_active' );
180
+
181
+ if ( count( $active ) !== count( $config['active-modules'] ) ) {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ if ( isset( $config['user-groups'] ) ) {
187
+ foreach ( $config['user-groups'] as $group_setting ) {
188
+ if ( ! ITSEC_Lib::array_get( $settings, $group_setting ) ) {
189
+ return false;
190
+ }
191
+ }
192
+ }
193
+
194
+ return true;
195
+ }
196
+
197
+ protected function after_save() { }
198
 
199
  protected function handle_settings_changes( $old_settings ) {
200
  $user_group_settings = ITSEC_Modules::get_container()->get( User_Groups\Settings_Registry::class );
204
  continue;
205
  }
206
 
207
+ $current = ITSEC_Lib::array_get( $this->settings, $user_group_setting->get_setting() );
208
  $previous = ITSEC_Lib::array_get( $old_settings, $user_group_setting->get_setting() );
209
 
210
  if ( $previous !== $current ) {
223
  }
224
 
225
  public function get( $name, $default = null ) {
226
+ if ( isset( $this->settings[ $name ] ) ) {
227
+ return $this->settings[ $name ];
228
  }
229
 
230
  return $default;
235
  }
236
 
237
  public function set( $name, $value ) {
238
+ $settings = $this->settings;
239
+ $settings[ $name ] = $value;
240
 
241
  return $this->set_all( $settings );
242
  }
257
  } else {
258
  $validator->validate( $settings );
259
 
260
+ $retval['errors'] = $validator->get_errors();
261
  $retval['messages'] = $validator->get_messages();
262
 
263
  if ( $validator->can_save() ) {
268
  $this->handle_settings_changes( $retval['old_settings'] );
269
 
270
  $retval['new_settings'] = $this->settings;
271
+ $retval['saved'] = true;
272
 
273
  do_action( 'itsec-settings-updated', $this->get_id() );
274
  } else {
284
 
285
  public function load() {
286
  $this->settings = ITSEC_Storage::get( $this->get_id() );
 
287
 
288
  if ( ! is_array( $this->settings ) ) {
289
  $this->settings = array();
290
  }
291
 
292
+ foreach ( $this->get_known_settings() as $setting ) {
293
+ if ( ! array_key_exists( $setting, $this->settings ) ) {
294
+ $this->settings[ $setting ] = $this->get_default( $setting );
295
+ }
296
+ }
297
  }
298
  }
core/lib/site-types/Answer_Details.php ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ use iThemesSecurity\User_Groups\User_Group;
6
+
7
+ interface Answer_Details {
8
+ /**
9
+ * Gets the question asked.
10
+ *
11
+ * @return Question
12
+ */
13
+ public function get_question(): Question;
14
+
15
+ /**
16
+ * Gets the answer provided by the user.
17
+ *
18
+ * @return mixed
19
+ */
20
+ public function get_answer();
21
+
22
+ /**
23
+ * Gets the list of modules to enable.
24
+ *
25
+ * @return array
26
+ */
27
+ public function get_enabled_modules(): array;
28
+
29
+ /**
30
+ * Gets the user groups to create.
31
+ *
32
+ * @return User_Group[]
33
+ */
34
+ public function get_user_groups(): array;
35
+
36
+ /**
37
+ * Gets the user group's enabled settings.
38
+ *
39
+ * @return array
40
+ */
41
+ public function get_user_group_settings(): array;
42
+
43
+ /**
44
+ * Gets the list of user groups that should be substituted for default canonical groups.
45
+ *
46
+ * @return string[] Map of canonical roles to user group ids.
47
+ */
48
+ public function get_canonical_user_group_substitutions(): array;
49
+
50
+ /**
51
+ * Gets the settings to set.
52
+ *
53
+ * @return array
54
+ */
55
+ public function get_settings(): array;
56
+ }
core/lib/site-types/Answer_Handler.php ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ use iThemesSecurity\Exception\Invalid_Argument_Exception;
6
+ use iThemesSecurity\User_Groups\User_Group;
7
+
8
+ final class Answer_Handler implements Answer_Details {
9
+
10
+ /** @var Question */
11
+ private $question;
12
+
13
+ /** @var mixed */
14
+ private $answer;
15
+
16
+ /** @var User_Group[] */
17
+ private $user_groups = [];
18
+
19
+ /** @var array */
20
+ private $user_group_settings = [];
21
+
22
+ /** @var array */
23
+ private $canonical_group_substitutions = [];
24
+
25
+ /** @var array */
26
+ private $settings = [];
27
+
28
+ /** @var array */
29
+ private $modules = [];
30
+
31
+ /** @var Answer_Details[] */
32
+ private $previous;
33
+
34
+ /**
35
+ * Answer_Handler constructor.
36
+ *
37
+ * @param Question $question
38
+ * @param mixed $answer
39
+ * @param Answer_Details ...$previous
40
+ */
41
+ public function __construct( Question $question, $answer, Answer_Details ...$previous ) {
42
+ $this->question = $question;
43
+ $this->answer = $answer;
44
+ $this->previous = $previous;
45
+ }
46
+
47
+ public function get_question(): Question {
48
+ return $this->question;
49
+ }
50
+
51
+ public function get_answer() {
52
+ return $this->answer;
53
+ }
54
+
55
+ /**
56
+ * Prefills a user group with the given description.
57
+ *
58
+ * @param User_Group $user_group A non-persisted user group.
59
+ *
60
+ * @return self
61
+ */
62
+ public function create_user_group( User_Group $user_group ): self {
63
+ foreach ( $this->user_groups as $i => $other_user_group ) {
64
+ if ( $other_user_group->identical( $user_group ) || $other_user_group->equals( $user_group ) ) {
65
+ $this->user_groups[ $i ] = $user_group;
66
+
67
+ return $this;
68
+ }
69
+ }
70
+
71
+ $this->user_groups[] = $user_group;
72
+
73
+ return $this;
74
+ }
75
+
76
+ /**
77
+ * Enables a setting for the given user group.
78
+ *
79
+ * @param User_Group $user_group
80
+ * @param string $module
81
+ * @param string $setting
82
+ *
83
+ * @return self
84
+ */
85
+ public function enable_setting_for( User_Group $user_group, string $module, string $setting ): self {
86
+ $this->user_group_settings[ $user_group->get_id() ][ $module ][] = $setting;
87
+
88
+ return $this;
89
+ }
90
+
91
+ /**
92
+ * Enables a setting for a default canonical user group.
93
+ *
94
+ * @param string $role
95
+ * @param string $module
96
+ * @param string $setting
97
+ *
98
+ * @return $this
99
+ */
100
+ public function enable_setting_for_canonical( string $role, string $module, string $setting ): self {
101
+ if ( ! in_array( $role, \ITSEC_Lib_Canonical_Roles::get_canonical_roles() ) ) {
102
+ throw new Invalid_Argument_Exception( "'$role' is not a valid canonical role." );
103
+ }
104
+
105
+ $this->user_group_settings[ $role ][ $module ][] = $setting;
106
+
107
+ return $this;
108
+ }
109
+
110
+ /**
111
+ * Enables the selected module.
112
+ *
113
+ * @param string $module
114
+ *
115
+ * @return $this
116
+ */
117
+ public function enable_module( string $module ): self {
118
+ $this->modules[] = $module;
119
+
120
+ return $this;
121
+ }
122
+
123
+ /**
124
+ * Prefills a setting value.
125
+ *
126
+ * @param string $module The module id.
127
+ * @param string $setting The setting name.
128
+ * @param mixed $value The value to use.
129
+ *
130
+ * @return self
131
+ */
132
+ public function set_setting( string $module, string $setting, $value ): self {
133
+ $this->settings[ $module ][ $setting ] = $value;
134
+
135
+ return $this;
136
+ }
137
+
138
+ public function get_user_groups(): array {
139
+ return $this->user_groups;
140
+ }
141
+
142
+ public function get_user_group_settings(): array {
143
+ return $this->user_group_settings;
144
+ }
145
+
146
+ public function get_settings(): array {
147
+ return $this->settings;
148
+ }
149
+
150
+ public function get_enabled_modules(): array {
151
+ return $this->modules;
152
+ }
153
+
154
+ /**
155
+ * Substitutes the default created canonical group with the given user group.
156
+ *
157
+ * @param string $role The canonical role. For example, 'subscriber'.
158
+ * @param User_Group|null $user_group The user group to substitute.
159
+ * Or `null` to prevent the group from being created.
160
+ *
161
+ * @return $this
162
+ */
163
+ public function substitute_canonical_user_group( string $role, User_Group $user_group = null ): self {
164
+ if ( ! in_array( $role, \ITSEC_Lib_Canonical_Roles::get_canonical_roles() ) ) {
165
+ throw new Invalid_Argument_Exception( "'$role' is not a valid canonical role." );
166
+ }
167
+
168
+ $this->canonical_group_substitutions[ $role ] = $user_group ? $user_group->get_id() : null;
169
+
170
+ return $this;
171
+ }
172
+
173
+ public function get_canonical_user_group_substitutions(): array {
174
+ return $this->canonical_group_substitutions;
175
+ }
176
+
177
+ /**
178
+ * Checks if the given question has been answered previously.
179
+ *
180
+ * @param string $question_id
181
+ *
182
+ * @return bool
183
+ */
184
+ public function has_answered( string $question_id ): bool {
185
+ foreach ( $this->previous as $previous ) {
186
+ if ( $previous->get_question()->get_id() === $question_id ) {
187
+ return true;
188
+ }
189
+ }
190
+
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Gets the answer to a previous question.
196
+ *
197
+ * @param string $question_id
198
+ *
199
+ * @return Answer_Details
200
+ */
201
+ public function get_previous( string $question_id ): Answer_Details {
202
+ foreach ( $this->previous as $previous ) {
203
+ if ( $previous->get_question()->get_id() === $question_id ) {
204
+ return $previous;
205
+ }
206
+ }
207
+
208
+ throw new Invalid_Argument_Exception( "No question found for '{$question_id}'." );
209
+ }
210
+ }
core/lib/site-types/Answered_Question.php ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ use iThemesSecurity\User_Groups\User_Group;
6
+
7
+ final class Answered_Question implements Answer_Details {
8
+
9
+ /** @var Question */
10
+ private $question;
11
+
12
+ /** @var mixed */
13
+ private $answer;
14
+
15
+ /** @var User_Group[] */
16
+ private $user_groups;
17
+
18
+ /** @var array */
19
+ private $user_group_settings;
20
+
21
+ /** @var array */
22
+ private $canonical_group_substitutions;
23
+
24
+ /** @var string[] */
25
+ private $modules;
26
+
27
+ /** @var array */
28
+ private $settings;
29
+
30
+ /**
31
+ * Answered_Question constructor.
32
+ *
33
+ * @param Question $question
34
+ * @param mixed $answer
35
+ * @param User_Group[] $user_groups
36
+ * @param array $user_group_settings
37
+ * @param string[] $canonical_group_substitutions
38
+ * @param string[] $modules
39
+ * @param array $settings
40
+ */
41
+ public function __construct(
42
+ Question $question,
43
+ $answer,
44
+ array $user_groups = [],
45
+ array $user_group_settings = [],
46
+ array $canonical_group_substitutions = [],
47
+ array $modules = [],
48
+ array $settings = []
49
+ ) {
50
+ $this->question = $question;
51
+ $this->answer = $answer;
52
+ $this->user_groups = $user_groups;
53
+ $this->user_group_settings = $user_group_settings;
54
+ $this->canonical_group_substitutions = $canonical_group_substitutions;
55
+ $this->modules = $modules;
56
+ $this->settings = $settings;
57
+ }
58
+
59
+ public static function from_answer_details( Answer_Details $details ): self {
60
+ return new self(
61
+ $details->get_question(),
62
+ $details->get_answer(),
63
+ $details->get_user_groups(),
64
+ $details->get_user_group_settings(),
65
+ $details->get_canonical_user_group_substitutions(),
66
+ $details->get_enabled_modules(),
67
+ $details->get_settings()
68
+ );
69
+ }
70
+
71
+ public function get_question(): Question {
72
+ return $this->question;
73
+ }
74
+
75
+ public function get_answer() {
76
+ return $this->answer;
77
+ }
78
+
79
+ public function get_user_groups(): array {
80
+ return $this->user_groups;
81
+ }
82
+
83
+ public function get_user_group_settings(): array {
84
+ return $this->user_group_settings;
85
+ }
86
+
87
+ public function get_canonical_user_group_substitutions(): array {
88
+ return $this->canonical_group_substitutions;
89
+ }
90
+
91
+ public function get_settings(): array {
92
+ return $this->settings;
93
+ }
94
+
95
+ public function get_enabled_modules(): array {
96
+ return $this->modules;
97
+ }
98
+ }
core/lib/site-types/Controller.php ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ final class Controller {
6
+
7
+ /** @var Site_Type|null */
8
+ private $selected_site_type;
9
+
10
+ /** @var Answer_Details[] */
11
+ private $previous = [];
12
+
13
+ /**
14
+ * Controller constructor.
15
+ *
16
+ * @param Site_Type|null $selected_site_type
17
+ * @param Answer_Details[] $previous
18
+ */
19
+ public function __construct( Site_Type $selected_site_type = null, array $previous = [] ) {
20
+ $this->selected_site_type = $selected_site_type;
21
+
22
+ foreach ( $previous as $details ) {
23
+ $this->previous[ $details->get_question()->get_id() ] = $details;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Gets the selected site type.
29
+ *
30
+ * @return Site_Type|null
31
+ */
32
+ public function get_selected_site_type() {
33
+ return $this->selected_site_type;
34
+ }
35
+
36
+ /**
37
+ * Selects a Site Type.
38
+ *
39
+ * @param Site_Type $site_type
40
+ */
41
+ public function select_site_type( Site_Type $site_type ) {
42
+ $this->selected_site_type = $site_type;
43
+ $this->previous = [];
44
+ }
45
+
46
+ /**
47
+ * Gets the next question to be asked.
48
+ *
49
+ * @return Question|null
50
+ */
51
+ public function get_next_question() {
52
+ if ( ! $site_type = $this->selected_site_type ) {
53
+ return null;
54
+ }
55
+
56
+ foreach ( $site_type->get_questions() as $question ) {
57
+ if ( isset( $this->previous[ $question->get_id() ] ) ) {
58
+ continue;
59
+ }
60
+
61
+ if ( $question instanceof Has_Prerequisites && ! $this->has_met_prerequisites( $question ) ) {
62
+ continue;
63
+ }
64
+
65
+ return $question;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Answers a question.
73
+ *
74
+ * @param Question $question The question being answered.
75
+ * @param mixed $answer The user provided answer.
76
+ *
77
+ * @return null|\WP_Error An error if the answer was invalid. Null otherwise.
78
+ */
79
+ public function answer( Question $question, $answer ) {
80
+ $schema = $question->get_answer_schema();
81
+
82
+ $valid = rest_validate_value_from_schema( $answer, $schema, $question->get_id() );
83
+
84
+ if ( is_wp_error( $valid ) ) {
85
+ return $valid;
86
+ }
87
+
88
+ $sanitized = rest_sanitize_value_from_schema( $answer, $schema, $question->get_id() );
89
+
90
+ if ( is_wp_error( $sanitized ) ) {
91
+ return $sanitized;
92
+ }
93
+
94
+ if ( $question instanceof Responds ) {
95
+ $handler = new Answer_Handler( $question, $sanitized, ...array_values( $this->previous ) );
96
+ $question->respond( $handler );
97
+ $previous = Answered_Question::from_answer_details( $handler );
98
+ } else {
99
+ $previous = new Answered_Question( $question, $answer );
100
+ }
101
+
102
+ $this->previous[ $question->get_id() ] = $previous;
103
+
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Gets the list of previously answered questions.
109
+ *
110
+ * @return Answer_Details[]
111
+ */
112
+ public function get_previous(): array {
113
+ return array_values( $this->previous );
114
+ }
115
+
116
+ /**
117
+ * Checks if the question has its prerequisites met.
118
+ *
119
+ * @param Has_Prerequisites $has_prerequisites
120
+ *
121
+ * @return bool
122
+ */
123
+ private function has_met_prerequisites( Has_Prerequisites $has_prerequisites ): bool {
124
+ foreach ( $has_prerequisites->get_prerequisites() as $question_id => $schema ) {
125
+ if ( ! isset( $this->previous[ $question_id ] ) ) {
126
+ return false;
127
+ }
128
+
129
+ $answer = $this->previous[ $question_id ]->get_answer();
130
+
131
+ if ( is_wp_error( rest_validate_value_from_schema( $answer, $schema ) ) ) {
132
+ return false;
133
+ }
134
+
135
+ if ( is_wp_error( rest_sanitize_value_from_schema( $answer, $schema ) ) ) {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ return true;
141
+ }
142
+ }
core/lib/site-types/Defaults.php ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ final class Defaults {
6
+ const DEFAULT = [
7
+ 'woocommerce/woocommerce.php' => [
8
+ 'type' => Site_Type::ECOMMERCE,
9
+ 'questions' => [
10
+ Question::SELECT_END_USERS => [ 'customer', 'subscriber' ],
11
+ ],
12
+ ],
13
+ 'restrict-content/restrictcontent.php' => [
14
+ 'type' => Site_Type::ECOMMERCE,
15
+ 'questions' => [
16
+ Question::SELECT_END_USERS => [ 'subscriber' ],
17
+ ],
18
+ ],
19
+ 'restrict-content-pro/restrict-content-pro.php' => [
20
+ 'type' => Site_Type::ECOMMERCE,
21
+ 'questions' => [
22
+ Question::SELECT_END_USERS => [ 'subscriber' ],
23
+ ],
24
+ ],
25
+ 'easy-digital-downloads/easy-digital-downloads.php' => [
26
+ 'type' => Site_Type::ECOMMERCE,
27
+ 'questions' => [
28
+ Question::SELECT_END_USERS => [ 'subscriber' ],
29
+ ],
30
+ ],
31
+ 'lifterlms/lifterlms.php' => [
32
+ 'type' => Site_Type::ECOMMERCE,
33
+ 'questions' => [
34
+ Question::SELECT_END_USERS => [ 'student', 'subscriber' ],
35
+ ],
36
+ ],
37
+ 'paid-memberships-pro/paid-memberships-pro.php' => [
38
+ 'type' => Site_Type::ECOMMERCE,
39
+ 'questions' => [
40
+ Question::SELECT_END_USERS => [ 'subscriber' ],
41
+ ],
42
+ ],
43
+ 'members/members.php' => [
44
+ 'type' => Site_Type::ECOMMERCE,
45
+ 'questions' => [
46
+ Question::SELECT_END_USERS => [ 'subscriber' ],
47
+ ],
48
+ ],
49
+ 'memberpress/memberpress.php' => [
50
+ 'type' => Site_Type::ECOMMERCE,
51
+ 'questions' => [
52
+ Question::SELECT_END_USERS => [ 'subscriber' ],
53
+ ],
54
+ ],
55
+ 'ultimate-member/ultimate-member.php' => [
56
+ 'type' => Site_Type::ECOMMERCE,
57
+ 'questions' => [
58
+ Question::SELECT_END_USERS => [ 'subscriber' ],
59
+ ],
60
+ ],
61
+ 'give/give.php' => [
62
+ 'type' => Site_Type::NON_PROFIT,
63
+ 'questions' => [
64
+ Question::SELECT_END_USERS => [ 'give_donor', 'subscriber' ],
65
+ ],
66
+ ],
67
+ 'charitable/charitable.php' => [
68
+ 'type' => Site_Type::NON_PROFIT,
69
+ 'questions' => [
70
+ Question::SELECT_END_USERS => [ 'donor', 'subscriber' ],
71
+ ],
72
+ ],
73
+ 'paypal-donations/paypal-donations.php' => [
74
+ 'type' => Site_Type::NON_PROFIT,
75
+ 'questions' => [
76
+ Question::SELECT_END_USERS => [ 'subscriber' ],
77
+ ],
78
+ ],
79
+ 'buddypress/bp-loader.php' => [
80
+ 'type' => Site_Type::NETWORK,
81
+ 'questions' => [
82
+ Question::SELECT_END_USERS => [ 'give_donor', 'subscriber' ],
83
+ ],
84
+ ],
85
+ 'bbpress/bbpress.php' => [
86
+ 'type' => Site_Type::NETWORK,
87
+ 'questions' => [
88
+ Question::SELECT_END_USERS => [ 'bbp_spectator', 'bbp_participant', 'subscriber' ],
89
+ ],
90
+ ],
91
+ ];
92
+
93
+ /** @var array */
94
+ private $config;
95
+
96
+ /**
97
+ * Gets the suggested site type based on the installed plugins.
98
+ *
99
+ * @return string
100
+ */
101
+ public function get_suggested_site_type(): string {
102
+ if ( $config = \ITSEC_Lib::first( $this->get_config() ) ) {
103
+ return $config['type'];
104
+ }
105
+
106
+ return '';
107
+ }
108
+
109
+ /**
110
+ * Gets the default value for a question.
111
+ *
112
+ * @param string $question_id The question id.
113
+ *
114
+ * @retrun mixed|null The default if available, otherwise null.
115
+ */
116
+ public function get_default_for_question( string $question_id ) {
117
+ $default = null;
118
+
119
+ foreach ( $this->get_config() as $config ) {
120
+ if ( isset( $config['questions'][ $question_id ] ) ) {
121
+ $plugin_default = $config['questions'][ $question_id ];
122
+
123
+ if ( is_array( $plugin_default ) && is_array( $default ) ) {
124
+ $default = array_merge( $default, $plugin_default );
125
+ } else {
126
+ $default = $plugin_default;
127
+ }
128
+ }
129
+ }
130
+
131
+ if ( is_array( $default ) ) {
132
+ $default = \ITSEC_Lib::non_scalar_array_unique( $default, true );
133
+ }
134
+
135
+ return $default;
136
+ }
137
+
138
+ /**
139
+ * Gets the active config.
140
+ *
141
+ * @return array
142
+ */
143
+ private function get_config(): array {
144
+ if ( ! $this->config ) {
145
+ if ( ! function_exists( 'is_plugin_active' ) ) {
146
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
147
+ }
148
+
149
+ $config = apply_filters( 'itsec_site_type_default_config', self::DEFAULT );
150
+ $this->config = array_filter( $config, 'is_plugin_active', ARRAY_FILTER_USE_KEY );
151
+ }
152
+
153
+ return $this->config;
154
+ }
155
+ }
core/lib/site-types/Has_End_Users.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Has_End_Users extends Site_Type, Templating_Site_Type {
6
+
7
+ /**
8
+ * Returns the user group label used for the end users of the site.
9
+ *
10
+ * @return string
11
+ */
12
+ public function get_end_users_group_label(): string;
13
+ }
core/lib/site-types/Has_Prerequisites.php ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Has_Prerequisites extends Question {
6
+
7
+ /**
8
+ * Returns a map of question IDs to schemas.
9
+ *
10
+ * In order for this question to be asked
11
+ *
12
+ * @return array
13
+ */
14
+ public function get_prerequisites(): array;
15
+ }
core/lib/site-types/Question.php ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Question {
6
+
7
+ const IS_CLIENT = 'is-client';
8
+ const SELECT_CLIENTS = 'select-clients';
9
+ const SELECT_SITE_ADMINS = 'select-site-admins';
10
+ const CLIENTS_CAN_MANAGE = 'clients-can-manage';
11
+ const SELECT_END_USERS = 'select-end-users';
12
+ const END_USERS_TWO_FACTOR = 'end-users-two-factor';
13
+ const END_USERS_PASSWORD_POLICY = 'end-users-password-policy';
14
+
15
+ /**
16
+ * Gets a unique id identifying this question.
17
+ *
18
+ * @return string
19
+ */
20
+ public function get_id(): string;
21
+
22
+ /**
23
+ * Gets the question prompt.
24
+ *
25
+ * @return string
26
+ */
27
+ public function get_prompt(): string;
28
+
29
+ /**
30
+ * Gets a longer description for the question.
31
+ *
32
+ * @return string
33
+ */
34
+ public function get_description(): string;
35
+
36
+ /**
37
+ * Gets the schema used to validate the user's answer.
38
+ *
39
+ * @return array
40
+ */
41
+ public function get_answer_schema(): array;
42
+ }
core/lib/site-types/Question/Client_Question_Pack.php ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Question;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Answer_Handler;
6
+ use iThemesSecurity\Lib\Site_Types\Has_Prerequisites;
7
+ use iThemesSecurity\Lib\Site_Types\Question;
8
+ use iThemesSecurity\Lib\Site_Types\Questions_Provider;
9
+ use iThemesSecurity\Lib\Site_Types\Responds;
10
+ use iThemesSecurity\User_Groups\User_Group;
11
+
12
+ final class Client_Question_Pack implements Questions_Provider {
13
+ const PREREQUISITE_IS_CLIENT = [
14
+ 'type' => 'boolean',
15
+ 'enum' => [ true ],
16
+ ];
17
+
18
+ public function get_questions(): array {
19
+ return [
20
+ new class implements Question, Responds {
21
+ public function get_id(): string {
22
+ return self::IS_CLIENT;
23
+ }
24
+
25
+ public function get_prompt(): string {
26
+ return __( 'Choosing who you are setting the site up for will help us preconfigure some settings for you.', 'better-wp-security' );
27
+ }
28
+
29
+ public function get_description(): string {
30
+ return __( 'If you are setting up iThemes Security for a client, we will ask you to select which users are theirs. Identifying your clients allows you to control their access to things like the security settings or if they are allowed to view the security grade report.', 'better-wp-security' );
31
+ }
32
+
33
+ public function get_answer_schema(): array {
34
+ return [
35
+ 'type' => 'boolean',
36
+ 'default' => false,
37
+ ];
38
+ }
39
+
40
+ public function respond( Answer_Handler $handler ) {
41
+ if ( $handler->get_answer() === true ) {
42
+ $handler->substitute_canonical_user_group( 'administrator' );
43
+ }
44
+ }
45
+ },
46
+
47
+ new class implements Question, Has_Prerequisites, Responds {
48
+ public function get_prerequisites(): array {
49
+ return [
50
+ self::IS_CLIENT => Client_Question_Pack::PREREQUISITE_IS_CLIENT,
51
+ ];
52
+ }
53
+
54
+ public function get_id(): string {
55
+ return self::SELECT_CLIENTS;
56
+ }
57
+
58
+ public function get_prompt(): string {
59
+ return __( 'Which users are your clients?', 'better-wp-security' );
60
+ }
61
+
62
+ public function get_description(): string {
63
+ return __( 'iThemes Security Pro will group client users together, allowing you to manage their access to sensitive information and which security settings you enable for them.', 'better-wp-security' );
64
+ }
65
+
66
+ public function get_answer_schema(): array {
67
+ return [
68
+ 'title' => __( 'Users', 'better-wp-security' ),
69
+ 'type' => 'array',
70
+ 'items' => [
71
+ 'type' => 'integer',
72
+ 'minimum' => 0,
73
+ ],
74
+ 'minItems' => 1,
75
+ 'uniqueItems' => true,
76
+ 'default' => [],
77
+ 'uiSchema' => [
78
+ 'ui:field' => 'EntitySelectField',
79
+ 'ui:options' => [
80
+ 'path' => '/wp/v2/users',
81
+ 'query' => [
82
+ 'per_page' => 100,
83
+ 'context' => 'embed',
84
+ 'itsec_global' => true,
85
+ ],
86
+ 'labelAttr' => 'name',
87
+ ],
88
+ ],
89
+ ];
90
+ }
91
+
92
+ public function respond( Answer_Handler $handler ) {
93
+ $user_group = new User_Group( wp_generate_uuid4() );
94
+ $user_group->set_label( __( 'Clients', 'better-wp-security' ) );
95
+
96
+ foreach ( $handler->get_answer() as $id ) {
97
+ if ( $user = get_userdata( $id ) ) {
98
+ $user_group->add_user( $user );
99
+ }
100
+ }
101
+
102
+ $handler->create_user_group( $user_group );
103
+ }
104
+ },
105
+
106
+ new class implements Question, Has_Prerequisites, Responds {
107
+ public function get_prerequisites(): array {
108
+ return [
109
+ self::IS_CLIENT => Client_Question_Pack::PREREQUISITE_IS_CLIENT,
110
+ ];
111
+ }
112
+
113
+ public function get_id(): string {
114
+ return self::SELECT_SITE_ADMINS;
115
+ }
116
+
117
+ public function get_prompt(): string {
118
+ return __( 'Which users will manage iThemes Security on this site?', 'better-wp-security' );
119
+ }
120
+
121
+ public function get_description(): string {
122
+ return __( 'Include yourself and anyone else who will need access to iThemes Security. Grouping these users together will allow you to manage their access to sensitive information and which security settings you enable for them.', 'better-wp-security' );
123
+ }
124
+
125
+ public function get_answer_schema(): array {
126
+ return [
127
+ 'title' => __( 'Users', 'better-wp-security' ),
128
+ 'type' => 'array',
129
+ 'items' => [
130
+ 'type' => 'integer',
131
+ 'minimum' => 0,
132
+ ],
133
+ 'default' => [
134
+ get_current_user_id(),
135
+ ],
136
+ 'minItems' => 1,
137
+ 'uniqueItems' => true,
138
+ 'uiSchema' => [
139
+ 'ui:field' => 'EntitySelectField',
140
+ 'ui:options' => [
141
+ 'path' => '/wp/v2/users',
142
+ 'query' => [
143
+ 'per_page' => 100,
144
+ 'context' => 'embed',
145
+ 'itsec_global' => true,
146
+ ],
147
+ 'labelAttr' => 'name',
148
+ ],
149
+ ],
150
+ ];
151
+ }
152
+
153
+ public function respond( Answer_Handler $handler ) {
154
+ $user_group = new User_Group( wp_generate_uuid4() );
155
+ $user_group->set_label( __( 'Security Managers', 'better-wp-security' ) );
156
+
157
+ foreach ( $handler->get_answer() as $id ) {
158
+ if ( $user = get_userdata( $id ) ) {
159
+ $user_group->add_user( $user );
160
+ }
161
+ }
162
+
163
+ $handler->create_user_group( $user_group );
164
+ $handler->enable_setting_for( $user_group, 'global', 'manage_group' );
165
+ $handler->enable_setting_for( $user_group, 'dashboard', 'group' );
166
+ $handler->substitute_canonical_user_group( 'administrator', $user_group );
167
+ }
168
+ },
169
+
170
+ new class implements Question, Has_Prerequisites, Responds {
171
+ public function get_prerequisites(): array {
172
+ return [
173
+ self::IS_CLIENT => Client_Question_Pack::PREREQUISITE_IS_CLIENT,
174
+ ];
175
+ }
176
+
177
+ public function get_id(): string {
178
+ return self::CLIENTS_CAN_MANAGE;
179
+ }
180
+
181
+ public function get_prompt(): string {
182
+ return __( 'Should your clients be able to view and make changes to the iThemes Security settings?', 'better-wp-security' );
183
+ }
184
+
185
+ public function get_description(): string {
186
+ return __( 'Restricting client access to the security settings will prevent them from making unwanted changes and seeing security notifications they may not understand.', 'better-wp-security' );
187
+ }
188
+
189
+ public function get_answer_schema(): array {
190
+ return [
191
+ 'type' => 'boolean',
192
+ 'title' => __( 'Yes, allow managing of iThemes Security', 'better-wp-security' ),
193
+ 'default' => false,
194
+ 'uiSchema' => [
195
+ 'ui:widget' => 'ToggleWidget',
196
+ ],
197
+ ];
198
+ }
199
+
200
+ public function respond( Answer_Handler $handler ) {
201
+ if ( $handler->get_answer() === true ) {
202
+ $user_group = $handler->get_previous( self::SELECT_CLIENTS )->get_user_groups()[0];
203
+ $handler->enable_setting_for( $user_group, 'global', 'manage_group' );
204
+ $handler->enable_setting_for( $user_group, 'dashboard', 'group' );
205
+ }
206
+ }
207
+ },
208
+ ];
209
+ }
210
+ }
core/lib/site-types/Question/End_Users_Question_Pack.php ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Question;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Answer_Handler;
6
+ use iThemesSecurity\Lib\Site_Types\Has_End_Users;
7
+ use iThemesSecurity\Lib\Site_Types\Questions_Provider;
8
+ use iThemesSecurity\Lib\Site_Types\Responds;
9
+ use iThemesSecurity\Lib\Site_Types\Templated_Question;
10
+ use iThemesSecurity\User_Groups\User_Group;
11
+
12
+ final class End_Users_Question_Pack implements Questions_Provider {
13
+
14
+ /** @var Has_End_Users */
15
+ private $site_type;
16
+
17
+ /**
18
+ * End_Users_Question_Pack constructor.
19
+ *
20
+ * @param Has_End_Users $site_type
21
+ */
22
+ public function __construct( Has_End_Users $site_type ) { $this->site_type = $site_type; }
23
+
24
+ public function get_questions(): array {
25
+ return [
26
+ new class( $this->site_type ) extends Templated_Question implements Responds {
27
+ public function __construct( Has_End_Users $site_type ) { parent::__construct( $site_type ); }
28
+
29
+ public function get_id(): string {
30
+ return self::SELECT_END_USERS;
31
+ }
32
+
33
+ public function get_answer_schema(): array {
34
+ return [
35
+ 'type' => 'array',
36
+ 'items' => [
37
+ 'type' => 'string',
38
+ 'enum' => array_keys( wp_roles()->get_names() ),
39
+ 'enumNames' => array_values( wp_roles()->get_names() ),
40
+ ],
41
+ 'minItems' => 1,
42
+ 'uniqueItems' => true,
43
+ 'default' => [],
44
+ 'uiSchema' => [
45
+ 'ui:widget' => 'checkboxes',
46
+ ]
47
+ ];
48
+ }
49
+
50
+ protected function get_prompt_fallback(): string {
51
+ return __( 'Who are the end users of your website?', 'better-wp-security' );
52
+ }
53
+
54
+ protected function get_description_fallback(): string {
55
+ return __( 'Select the WordPress user roles they are assigned to', 'better-wp-security' );
56
+ }
57
+
58
+ public function respond( Answer_Handler $handler ) {
59
+ $user_group = new User_Group( wp_generate_uuid4() );
60
+ $user_group->set_label( $this->site_type->get_end_users_group_label() );
61
+
62
+ foreach ( $handler->get_answer() as $role ) {
63
+ $user_group->add_role( $role );
64
+ }
65
+
66
+ $handler->create_user_group( $user_group );
67
+ $handler->substitute_canonical_user_group( 'subscriber', $user_group );
68
+ }
69
+ },
70
+ ];
71
+ }
72
+ }
core/lib/site-types/Question/Login_Security_Question_Pack.php ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Question;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Answer_Handler;
6
+ use iThemesSecurity\Lib\Site_Types\Questions_Provider;
7
+ use iThemesSecurity\Lib\Site_Types\Responds;
8
+ use iThemesSecurity\Lib\Site_Types\Site_Type;
9
+ use iThemesSecurity\Lib\Site_Types\Templated_Question;
10
+ use iThemesSecurity\Lib\Site_Types\Templating_Site_Type;
11
+ use iThemesSecurity\Lib\Site_Types\Templating_Site_Type_Adapter;
12
+
13
+ final class Login_Security_Question_Pack implements Questions_Provider {
14
+
15
+ /** @var Templating_Site_Type */
16
+ private $site_type;
17
+
18
+ /**
19
+ * Login_Security_Question_Pack constructor.
20
+ *
21
+ * @param Site_Type $site_type
22
+ */
23
+ public function __construct( Site_Type $site_type ) { $this->site_type = new Templating_Site_Type_Adapter( $site_type ); }
24
+
25
+ public function get_questions(): array {
26
+ $questions = [];
27
+
28
+ if ( \ITSEC_Core::is_pro() ) {
29
+ $questions[] = new class ( $this->site_type ) extends Templated_Question implements Responds {
30
+ public function get_id(): string {
31
+ return self::END_USERS_TWO_FACTOR;
32
+ }
33
+
34
+ public function get_answer_schema(): array {
35
+ return [
36
+ 'type' => 'boolean',
37
+ 'title' => __( 'Yes, require Two-Factor for these users.', 'better-wp-security' ),
38
+ 'default' => false,
39
+ 'uiSchema' => [
40
+ 'ui:widget' => 'ToggleWidget',
41
+ ],
42
+ ];
43
+ }
44
+
45
+ protected function get_prompt_fallback(): string {
46
+ return __( 'Do you want to secure your user accounts with two-factor authentication?', 'better-wp-security' );
47
+ }
48
+
49
+ public function get_description(): string {
50
+ return __( 'Securing users with two-factor authentication will require them to enter a security code along with their username and password to log in. Two-Factor authentication is the strongest way to prevent automated bot attacks.', 'better-wp-security' );
51
+ }
52
+
53
+ public function respond( Answer_Handler $handler ) {
54
+ if ( $handler->get_answer() !== true ) {
55
+ return;
56
+ }
57
+
58
+ $handler->enable_module( 'two-factor' );
59
+ $handler->enable_module( 'passwordless-login' );
60
+
61
+ $user_groups = [];
62
+
63
+ if ( $handler->has_answered( self::SELECT_END_USERS ) ) {
64
+ $user_groups = $handler->get_previous( self::SELECT_END_USERS )->get_user_groups();
65
+ }
66
+
67
+ foreach ( $user_groups as $user_group ) {
68
+ $handler->enable_setting_for( $user_group, 'two-factor', 'remember_group' );
69
+ $handler->enable_setting_for( $user_group, 'passwordless-login', '2fa_bypass_group' );
70
+ }
71
+
72
+ if ( $handler->has_answered( self::SELECT_CLIENTS ) ) {
73
+ $user_groups = array_merge( $user_groups, $handler->get_previous( self::SELECT_CLIENTS )->get_user_groups() );
74
+ }
75
+
76
+ if ( $handler->has_answered( self::SELECT_SITE_ADMINS ) ) {
77
+ $user_groups = array_merge( $user_groups, $handler->get_previous( self::SELECT_SITE_ADMINS )->get_user_groups() );
78
+ }
79
+
80
+ foreach ( $user_groups as $user_group ) {
81
+ $handler->enable_setting_for( $user_group, 'two-factor', 'protect_user_group' );
82
+ }
83
+
84
+ foreach ( \ITSEC_Lib_Canonical_Roles::get_canonical_roles( false ) as $canonical ) {
85
+ $handler->enable_setting_for_canonical( $canonical, 'two-factor', 'protect_user_group' );
86
+
87
+ if ( 'administrator' !== $canonical ) {
88
+ $handler->enable_setting_for_canonical( $canonical, 'two-factor', 'remember_group' );
89
+ $handler->enable_setting_for_canonical( $canonical, 'passwordless-login', '2fa_bypass_group' );
90
+ }
91
+ }
92
+ }
93
+ };
94
+ }
95
+
96
+ $questions[] = new class ( $this->site_type ) extends Templated_Question implements Responds {
97
+ public function get_id(): string {
98
+ return self::END_USERS_PASSWORD_POLICY;
99
+ }
100
+
101
+ public function get_answer_schema(): array {
102
+ return [
103
+ 'type' => 'boolean',
104
+ 'title' => __( 'Yes, enforce a password policy for these users.', 'better-wp-security' ),
105
+ 'default' => false,
106
+ 'uiSchema' => [
107
+ 'ui:widget' => 'ToggleWidget',
108
+ ],
109
+ ];
110
+ }
111
+
112
+ protected function get_prompt_fallback(): string {
113
+ return __( 'Do you want to secure your user accounts with a password policy?', 'better-wp-security' );
114
+ }
115
+
116
+ public function get_description(): string {
117
+ return __( 'When you secure your users with a password policy, iThemes Security will require them to create a strong password that hasn’t already been compromised. ', 'better-wp-security' );
118
+ }
119
+
120
+ public function respond( Answer_Handler $handler ) {
121
+ if ( $handler->get_answer() !== true ) {
122
+ return;
123
+ }
124
+
125
+ if ( \ITSEC_Modules::is_available( 'passwordless-login' ) ) {
126
+ $handler->enable_module( 'passwordless-login' );
127
+ }
128
+
129
+ $user_groups = [];
130
+
131
+ if ( $handler->has_answered( self::SELECT_END_USERS ) ) {
132
+ $user_groups = $handler->get_previous( self::SELECT_END_USERS )->get_user_groups();
133
+ }
134
+
135
+ if ( \ITSEC_Modules::is_available( 'passwordless-login' ) ) {
136
+ foreach ( $user_groups as $user_group ) {
137
+ $handler->enable_setting_for( $user_group, 'passwordless-login', 'group' );
138
+ }
139
+ }
140
+
141
+ if ( $handler->has_answered( self::SELECT_CLIENTS ) ) {
142
+ $user_groups = array_merge( $user_groups, $handler->get_previous( self::SELECT_CLIENTS )->get_user_groups() );
143
+ }
144
+
145
+ if ( $handler->has_answered( self::SELECT_SITE_ADMINS ) ) {
146
+ $user_groups = array_merge( $user_groups, $handler->get_previous( self::SELECT_SITE_ADMINS )->get_user_groups() );
147
+ }
148
+
149
+ foreach ( $user_groups as $user_group ) {
150
+ $handler->enable_setting_for( $user_group, 'password-requirements', 'requirement_settings.strength.group' );
151
+ $handler->enable_setting_for( $user_group, 'password-requirements', 'requirement_settings.hibp.group' );
152
+ }
153
+
154
+ foreach ( \ITSEC_Lib_Canonical_Roles::get_canonical_roles( false ) as $canonical ) {
155
+ $handler->enable_setting_for_canonical( $canonical, 'password-requirements', 'requirement_settings.strength.group' );
156
+ $handler->enable_setting_for_canonical( $canonical, 'password-requirements', 'requirement_settings.hibp.group' );
157
+
158
+ if ( \ITSEC_Modules::is_available( 'passwordless-login' ) && 'administrator' !== $canonical ) {
159
+ $handler->enable_setting_for_canonical( $canonical, 'passwordless-login', 'group' );
160
+ }
161
+ }
162
+ }
163
+ };
164
+
165
+ return $questions;
166
+ }
167
+ }
core/lib/site-types/Question/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/site-types/Questions_Provider.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Questions_Provider {
6
+
7
+ /**
8
+ * Gets a list of questions.
9
+ *
10
+ * @return Question[]
11
+ */
12
+ public function get_questions(): array;
13
+ }
core/lib/site-types/Registry.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ final class Registry {
6
+
7
+ /** @var Site_Type[] */
8
+ private $site_types = [];
9
+
10
+ /**
11
+ * Registers a site type.
12
+ *
13
+ * @param Site_Type $site_type
14
+ *
15
+ * @return $this
16
+ */
17
+ public function register( Site_Type $site_type ): self {
18
+ $this->site_types[] = $site_type;
19
+
20
+ return $this;
21
+ }
22
+
23
+ /**
24
+ * Gets the list of registered site types.
25
+ *
26
+ * @return Site_Type[]
27
+ */
28
+ public function get_site_types(): array {
29
+ return $this->site_types;
30
+ }
31
+
32
+ /**
33
+ * Gets a Site Type object by it's slug.
34
+ *
35
+ * @param string $slug
36
+ *
37
+ * @return Site_Type|null
38
+ */
39
+ public function get_by_slug( string $slug ) {
40
+ foreach ( $this->get_site_types() as $site_type ) {
41
+ if ( $slug === $site_type->get_slug() ) {
42
+ return $site_type;
43
+ }
44
+ }
45
+
46
+ return null;
47
+ }
48
+ }
core/lib/site-types/Responds.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Responds {
6
+ /**
7
+ * Handle the user's answer.
8
+ *
9
+ * It has already been validated and sanitized according to the schema.
10
+ *
11
+ * @param Answer_Handler $handler
12
+ *
13
+ * @return void
14
+ */
15
+ public function respond( Answer_Handler $handler );
16
+ }
core/lib/site-types/Site_Type.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Site_Type {
6
+ const ECOMMERCE = 'ecommerce';
7
+ const NETWORK = 'network';
8
+ const NON_PROFIT = 'non-profit';
9
+ const BLOG = 'blog';
10
+ const BROCHURE = 'brochure';
11
+ const PORTFOLIO = 'portfolio';
12
+
13
+ /**
14
+ * Gets the slug.
15
+ *
16
+ * @return string
17
+ */
18
+ public function get_slug(): string;
19
+
20
+ /**
21
+ * Gets the title.
22
+ *
23
+ * @return string
24
+ */
25
+ public function get_title(): string;
26
+
27
+ /**
28
+ * Gets the description.
29
+ *
30
+ * @return string
31
+ */
32
+ public function get_description(): string;
33
+
34
+ /**
35
+ * Gets the icon name.
36
+ *
37
+ * @return string
38
+ */
39
+ public function get_icon(): string;
40
+
41
+ /**
42
+ * Gets the list of all possible Questions this site type asks.
43
+ *
44
+ * @return Question[]
45
+ */
46
+ public function get_questions(): array;
47
+ }
core/lib/site-types/Templated_Question.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ abstract class Templated_Question implements Question {
6
+
7
+ /** @var Templating_Site_Type */
8
+ protected $site_type;
9
+
10
+ /**
11
+ * Templated_Question constructor.
12
+ *
13
+ * @param Templating_Site_Type $site_type
14
+ */
15
+ public function __construct( Templating_Site_Type $site_type ) { $this->site_type = $site_type; }
16
+
17
+ public function get_prompt(): string {
18
+ if ( $this->site_type->is_supported_question( $this->get_id() ) ) {
19
+ return $this->site_type->make_prompt( $this->get_id() );
20
+ }
21
+
22
+ return $this->get_prompt_fallback();
23
+ }
24
+
25
+ public function get_description(): string {
26
+ $description = $this->get_description_fallback();
27
+
28
+ if ( $description && $this->site_type->is_supported_question( $this->get_id() ) ) {
29
+ $description = $this->site_type->make_description( $this->get_id() ) ?: $description;
30
+ }
31
+
32
+ return $description;
33
+ }
34
+
35
+ abstract protected function get_prompt_fallback(): string;
36
+
37
+ protected function get_description_fallback(): string { return ''; }
38
+ }
core/lib/site-types/Templating_Site_Type.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ interface Templating_Site_Type extends Site_Type {
6
+ public function is_supported_question( string $question_id ): bool;
7
+
8
+ public function make_prompt( string $question_id ): string;
9
+
10
+ public function make_description( string $question_id ): string;
11
+ }
core/lib/site-types/Templating_Site_Type_Adapter.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types;
4
+
5
+ final class Templating_Site_Type_Adapter implements Templating_Site_Type {
6
+
7
+ /** @var Site_Type */
8
+ private $site_type;
9
+
10
+ /**
11
+ * Templating_Site_Type_Adapter constructor.
12
+ *
13
+ * @param Site_Type $site_type
14
+ */
15
+ public function __construct( Site_Type $site_type ) { $this->site_type = $site_type; }
16
+
17
+ public function is_supported_question( string $question_id ): bool {
18
+ return $this->site_type instanceof Templating_Site_Type ? $this->site_type->is_supported_question( $question_id ) : false;
19
+ }
20
+
21
+ public function make_prompt( string $question_id ): string {
22
+ return $this->site_type instanceof Templating_Site_Type ? $this->site_type->make_prompt( $question_id ) : '';
23
+ }
24
+
25
+ public function make_description( string $question_id ): string {
26
+ return $this->site_type instanceof Templating_Site_Type ? $this->site_type->make_description( $question_id ) : '';
27
+ }
28
+
29
+ public function get_slug(): string {
30
+ return $this->site_type->get_slug();
31
+ }
32
+
33
+ public function get_title(): string {
34
+ return $this->site_type->get_title();
35
+ }
36
+
37
+ public function get_description(): string {
38
+ return $this->site_type->get_description();
39
+ }
40
+
41
+ public function get_icon(): string {
42
+ return $this->site_type->get_icon();
43
+ }
44
+
45
+ public function get_questions(): array {
46
+ return $this->site_type->get_questions();
47
+ }
48
+ }
core/lib/site-types/Type/Blog.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Login_Security_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Site_Type;
8
+
9
+ final class Blog implements Site_Type {
10
+ public function get_slug(): string {
11
+ return self::BLOG;
12
+ }
13
+
14
+ public function get_title(): string {
15
+ return __( 'Blog', 'better-wp-security' );
16
+ }
17
+
18
+ public function get_description(): string {
19
+ return __( 'A website to share your thoughts or to start a conversation.', 'better-wp-security' );
20
+ }
21
+
22
+ public function get_icon(): string {
23
+ return 'media-text';
24
+ }
25
+
26
+ public function get_questions(): array {
27
+ return array_merge(
28
+ ( new Client_Question_Pack() )->get_questions(),
29
+ ( new Login_Security_Question_Pack( $this ) )->get_questions()
30
+ );
31
+ }
32
+ }
core/lib/site-types/Type/Brochure.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Login_Security_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Site_Type;
8
+
9
+ final class Brochure implements Site_Type {
10
+ public function get_slug(): string {
11
+ return self::BROCHURE;
12
+ }
13
+
14
+ public function get_title(): string {
15
+ return __( 'Brochure', 'better-wp-security' );
16
+ }
17
+
18
+ public function get_description(): string {
19
+ return __( 'A simple website to promote your business.', 'better-wp-security' );
20
+ }
21
+
22
+ public function get_icon(): string {
23
+ return 'media-document';
24
+ }
25
+
26
+ public function get_questions(): array {
27
+ return array_merge(
28
+ ( new Client_Question_Pack() )->get_questions(),
29
+ ( new Login_Security_Question_Pack( $this ) )->get_questions()
30
+ );
31
+ }
32
+ }
core/lib/site-types/Type/Ecommerce.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Exception\Invalid_Argument_Exception;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Question\End_Users_Question_Pack;
8
+ use iThemesSecurity\Lib\Site_Types\Has_End_Users;
9
+ use iThemesSecurity\Lib\Site_Types\Question;
10
+ use iThemesSecurity\Lib\Site_Types\Question\Login_Security_Question_Pack;
11
+ use iThemesSecurity\Lib\Site_Types\Templating_Site_Type;
12
+
13
+ final class Ecommerce implements Templating_Site_Type, Has_End_Users {
14
+ const TEMPLATES = [
15
+ Question::SELECT_END_USERS,
16
+ Question::END_USERS_TWO_FACTOR,
17
+ Question::END_USERS_PASSWORD_POLICY,
18
+ ];
19
+
20
+ public function get_slug(): string {
21
+ return self::ECOMMERCE;
22
+ }
23
+
24
+ public function get_title(): string {
25
+ return __( 'eCommerce', 'better-wp-security' );
26
+ }
27
+
28
+ public function get_description(): string {
29
+ return __( 'A website to sell products or services.', 'better-wp-security' );
30
+ }
31
+
32
+ public function get_icon(): string {
33
+ return 'money';
34
+ }
35
+
36
+ public function get_questions(): array {
37
+ return array_merge(
38
+ ( new Client_Question_Pack() )->get_questions(),
39
+ ( new End_Users_Question_Pack( $this ) )->get_questions(),
40
+ ( new Login_Security_Question_Pack( $this ) )->get_questions()
41
+ );
42
+ }
43
+
44
+ public function get_end_users_group_label(): string {
45
+ return __( 'Customers', 'better-wp-security' );
46
+ }
47
+
48
+ public function is_supported_question( string $question_id ): bool {
49
+ return in_array( $question_id, self::TEMPLATES, true );
50
+ }
51
+
52
+ public function make_prompt( string $question_id ): string {
53
+ switch ( $question_id ) {
54
+ case Question::SELECT_END_USERS:
55
+ return __( 'Select your customers', 'better-wp-security' );
56
+ case Question::END_USERS_TWO_FACTOR:
57
+ return __( 'Do you want to secure your customer accounts with two-factor authentication?', 'better-wp-security' );
58
+ case Question::END_USERS_PASSWORD_POLICY:
59
+ return __( 'Do you want to secure your customer accounts with a password policy?', 'better-wp-security' );
60
+ default:
61
+ throw new Invalid_Argument_Exception( sprintf( 'The eCommerce site type does not support the %s question.', $question_id ) );
62
+ }
63
+ }
64
+
65
+ public function make_description( string $question_id ): string {
66
+ switch ( $question_id ) {
67
+ case Question::SELECT_END_USERS:
68
+ case Question::END_USERS_TWO_FACTOR:
69
+ case Question::END_USERS_PASSWORD_POLICY:
70
+ return '';
71
+ default:
72
+ throw new Invalid_Argument_Exception( sprintf( 'The eCommerce site type does not support the %s question.', $question_id ) );
73
+ }
74
+ }
75
+ }
core/lib/site-types/Type/Network.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Exception\Invalid_Argument_Exception;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Question\End_Users_Question_Pack;
8
+ use iThemesSecurity\Lib\Site_Types\Has_End_Users;
9
+ use iThemesSecurity\Lib\Site_Types\Question;
10
+ use iThemesSecurity\Lib\Site_Types\Question\Login_Security_Question_Pack;
11
+ use iThemesSecurity\Lib\Site_Types\Templating_Site_Type;
12
+
13
+ final class Network implements Templating_Site_Type, Has_End_Users {
14
+ const TEMPLATES = [
15
+ Question::SELECT_END_USERS,
16
+ Question::END_USERS_TWO_FACTOR,
17
+ Question::END_USERS_PASSWORD_POLICY,
18
+ ];
19
+
20
+ public function get_slug(): string {
21
+ return self::NETWORK;
22
+ }
23
+
24
+ public function get_title(): string {
25
+ return __( 'Network', 'better-wp-security' );
26
+ }
27
+
28
+ public function get_description(): string {
29
+ return __( 'A website to connect people and communities.', 'better-wp-security' );
30
+ }
31
+
32
+ public function get_icon(): string {
33
+ return 'groups';
34
+ }
35
+
36
+ public function get_questions(): array {
37
+ return array_merge(
38
+ ( new Client_Question_Pack() )->get_questions(),
39
+ ( new End_Users_Question_Pack( $this ) )->get_questions(),
40
+ ( new Login_Security_Question_Pack( $this ) )->get_questions()
41
+ );
42
+ }
43
+
44
+ public function get_end_users_group_label(): string {
45
+ return __( 'Members', 'better-wp-security' );
46
+ }
47
+
48
+ public function is_supported_question( string $question_id ): bool {
49
+ return in_array( $question_id, self::TEMPLATES, true );
50
+ }
51
+
52
+ public function make_prompt( string $question_id ): string {
53
+ switch ( $question_id ) {
54
+ case Question::SELECT_END_USERS:
55
+ return __( 'Select your members', 'better-wp-security' );
56
+ case Question::END_USERS_TWO_FACTOR:
57
+ return __( 'Do you want to secure your member accounts with two-factor authentication?', 'better-wp-security' );
58
+ case Question::END_USERS_PASSWORD_POLICY:
59
+ return __( 'Do you want to secure your member accounts with a password policy?', 'better-wp-security' );
60
+ default:
61
+ throw new Invalid_Argument_Exception( sprintf( 'The network site type does not support the %s question.', $question_id ) );
62
+ }
63
+ }
64
+
65
+ public function make_description( string $question_id ): string {
66
+ switch ( $question_id ) {
67
+ case Question::SELECT_END_USERS:
68
+ case Question::END_USERS_TWO_FACTOR:
69
+ case Question::END_USERS_PASSWORD_POLICY:
70
+ return '';
71
+ default:
72
+ throw new Invalid_Argument_Exception( sprintf( 'The network site type does not support the %s question.', $question_id ) );
73
+ }
74
+ }
75
+ }
core/lib/site-types/Type/Non_Profit.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Exception\Invalid_Argument_Exception;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Question\End_Users_Question_Pack;
8
+ use iThemesSecurity\Lib\Site_Types\Has_End_Users;
9
+ use iThemesSecurity\Lib\Site_Types\Question;
10
+ use iThemesSecurity\Lib\Site_Types\Templating_Site_Type;
11
+
12
+ final class Non_Profit implements Templating_Site_Type, Has_End_Users {
13
+ const TEMPLATES = [
14
+ Question::SELECT_END_USERS,
15
+ Question::END_USERS_TWO_FACTOR,
16
+ Question::END_USERS_PASSWORD_POLICY,
17
+ ];
18
+
19
+ public function get_slug(): string {
20
+ return self::NON_PROFIT;
21
+ }
22
+
23
+ public function get_title(): string {
24
+ return __( 'Non-Profit', 'better-wp-security' );
25
+ }
26
+
27
+ public function get_description(): string {
28
+ return __( 'A website to promote your cause and collect donations.', 'better-wp-security' );
29
+ }
30
+
31
+ public function get_icon(): string {
32
+ return 'smiley';
33
+ }
34
+
35
+ public function get_questions(): array {
36
+ return array_merge(
37
+ ( new Client_Question_Pack() )->get_questions(),
38
+ ( new End_Users_Question_Pack( $this ) )->get_questions(),
39
+ ( new Question\Login_Security_Question_Pack( $this ) )->get_questions()
40
+ );
41
+ }
42
+
43
+ public function get_end_users_group_label(): string {
44
+ return __( 'Donors', 'better-wp-security' );
45
+ }
46
+
47
+ public function is_supported_question( string $question_id ): bool {
48
+ return in_array( $question_id, self::TEMPLATES, true );
49
+ }
50
+
51
+ public function make_prompt( string $question_id ): string {
52
+ switch ( $question_id ) {
53
+ case Question::SELECT_END_USERS:
54
+ return __( 'Select your donors', 'better-wp-security' );
55
+ case Question::END_USERS_TWO_FACTOR:
56
+ return __( 'Do you want to secure your donor accounts with two-factor authentication?', 'better-wp-security' );
57
+ case Question::END_USERS_PASSWORD_POLICY:
58
+ return __( 'Do you want to secure your donor accounts with a password policy?', 'better-wp-security' );
59
+ default:
60
+ throw new Invalid_Argument_Exception( sprintf( 'The Non-Profit site type does not support the %s question.', $question_id ) );
61
+ }
62
+ }
63
+
64
+ public function make_description( string $question_id ): string {
65
+ switch ( $question_id ) {
66
+ case Question::SELECT_END_USERS:
67
+ case Question::END_USERS_TWO_FACTOR:
68
+ case Question::END_USERS_PASSWORD_POLICY:
69
+ return '';
70
+ default:
71
+ throw new Invalid_Argument_Exception( sprintf( 'The Non-Profit site type does not support the %s question.', $question_id ) );
72
+ }
73
+ }
74
+ }
core/lib/site-types/Type/Portfolio.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Site_Types\Type;
4
+
5
+ use iThemesSecurity\Lib\Site_Types\Question\Client_Question_Pack;
6
+ use iThemesSecurity\Lib\Site_Types\Question\Login_Security_Question_Pack;
7
+ use iThemesSecurity\Lib\Site_Types\Site_Type;
8
+
9
+ final class Portfolio implements Site_Type {
10
+ public function get_slug(): string {
11
+ return self::PORTFOLIO;
12
+ }
13
+
14
+ public function get_title(): string {
15
+ return __( 'Portfolio', 'better-wp-security' );
16
+ }
17
+
18
+ public function get_description(): string {
19
+ return __( 'A website to showcase your craft.', 'better-wp-security' );
20
+ }
21
+
22
+ public function get_icon(): string {
23
+ return 'format-gallery';
24
+ }
25
+
26
+ public function get_questions(): array {
27
+ return array_merge(
28
+ ( new Client_Question_Pack() )->get_questions(),
29
+ ( new Login_Security_Question_Pack( $this ) )->get_questions()
30
+ );
31
+ }
32
+ }
core/lib/site-types/Type/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/site-types/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/storage.php CHANGED
@@ -96,4 +96,12 @@ final class ITSEC_Storage {
96
 
97
  $this->shutdown_done = true;
98
  }
 
 
 
 
 
 
 
 
99
  }
96
 
97
  $this->shutdown_done = true;
98
  }
99
+
100
+ public static function reset() {
101
+ $data = self::get_instance();
102
+
103
+ delete_site_option( $data->option );
104
+ unset( $data->cache );
105
+ $data->changed = false;
106
+ }
107
  }
core/lib/tools/Config_Tool.php ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Tools;
4
+
5
+ use iThemesSecurity\Module_Config;
6
+
7
+ abstract class Config_Tool implements Tool {
8
+
9
+ /** @var string */
10
+ private $slug;
11
+
12
+ /** @var Module_Config */
13
+ private $config;
14
+
15
+ /**
16
+ * Config_Tool constructor.
17
+ *
18
+ * @param string $slug
19
+ * @param Module_Config $config
20
+ */
21
+ public function __construct( string $slug, Module_Config $config ) {
22
+ $this->slug = $slug;
23
+ $this->config = $config;
24
+ }
25
+
26
+ public function get_slug(): string {
27
+ return $this->slug;
28
+ }
29
+
30
+ public function get_module(): string {
31
+ return $this->config->get_id();
32
+ }
33
+
34
+ public function is_available(): bool {
35
+ return true;
36
+ }
37
+
38
+ public function get_condition(): array {
39
+ return $this->get_config()['condition'] ?? [];
40
+ }
41
+
42
+ public function get_title(): string {
43
+ return $this->get_config()['title'] ?? $this->config->translate( Module_Config::T_ABOUT )->get_title();
44
+ }
45
+
46
+ public function get_description(): string {
47
+ return $this->get_config()['description'] ??
48
+ ( $this->config->get_type() === 'tool' ? $this->config->translate( Module_Config::T_ABOUT )->get_description() : '' );
49
+ }
50
+
51
+ public function get_help(): string {
52
+ return $this->get_config()['help'] ??
53
+ ( $this->config->get_type() === 'tool' ? $this->config->translate( Module_Config::T_ABOUT )->get_help() : '' );
54
+ }
55
+
56
+ public function get_keywords(): array {
57
+ return $this->get_config()['keywords'] ?? [];
58
+ }
59
+
60
+ public function is_toggleable(): bool {
61
+ return ! empty( $this->get_config()['toggle'] );
62
+ }
63
+
64
+ public function get_schedule(): string {
65
+ return $this->get_config()['schedule'] ?? '';
66
+ }
67
+
68
+ public function get_form(): array {
69
+ return $this->get_config()['form'] ?? [];
70
+ }
71
+
72
+ private function get_config(): array {
73
+ return $this->config->translate( Module_Config::T_TOOLS )->get_tools()[ $this->get_slug() ];
74
+ }
75
+ }
core/lib/tools/Tool.php ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Tools;
4
+
5
+ use iThemesSecurity\Lib\Result;
6
+
7
+ interface Tool {
8
+ /**
9
+ * Gets the Tool's slug.
10
+ *
11
+ * @return string
12
+ */
13
+ public function get_slug(): string;
14
+
15
+ /**
16
+ * Gets the Tool's module.
17
+ *
18
+ * @return string
19
+ */
20
+ public function get_module(): string;
21
+
22
+ /**
23
+ * Can this tool be run.
24
+ *
25
+ * @return bool
26
+ */
27
+ public function is_available(): bool;
28
+
29
+ /**
30
+ * Gets the condition definition in which this tool can be run.
31
+ *
32
+ * @return array
33
+ */
34
+ public function get_condition(): array;
35
+
36
+ /**
37
+ * Gets the Tool's title.
38
+ *
39
+ * @return string
40
+ */
41
+ public function get_title(): string;
42
+
43
+ /**
44
+ * Gets the Tool's description.
45
+ *
46
+ * @return string
47
+ */
48
+ public function get_description(): string;
49
+
50
+ /**
51
+ * Gets the Tool's search keywords.
52
+ *
53
+ * @return string[]
54
+ */
55
+ public function get_keywords(): array;
56
+
57
+ /**
58
+ * Gets the Tool's help content.
59
+ *
60
+ * @return string
61
+ */
62
+ public function get_help(): string;
63
+
64
+ /**
65
+ * Is this a toggleable tool.
66
+ *
67
+ * @return bool
68
+ */
69
+ public function is_toggleable(): bool;
70
+
71
+ /**
72
+ * If this is a scheduled tool, returns the schedule id.
73
+ *
74
+ * @return string
75
+ */
76
+ public function get_schedule(): string;
77
+
78
+ /**
79
+ * If this Tool accepts user input, returns the JSON Schema for the form.
80
+ *
81
+ * @return array
82
+ */
83
+ public function get_form(): array;
84
+
85
+ /**
86
+ * Runs the tool.
87
+ *
88
+ * @param array $form User provided form data, validated and sanitized according to the Schema.
89
+ *
90
+ * @return Result
91
+ */
92
+ public function run( array $form = [] ): Result;
93
+ }
core/lib/tools/Tools_Registry.php ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Tools;
4
+
5
+ final class Tools_Registry {
6
+
7
+ /** @var Tool[] */
8
+ private $tools = [];
9
+
10
+ /**
11
+ * Registers a tool.
12
+ *
13
+ * @param Tool $tool
14
+ *
15
+ * @return $this
16
+ */
17
+ public function register( Tool $tool ): self {
18
+ $this->tools[ $tool->get_slug() ] = $tool;
19
+
20
+ return $this;
21
+ }
22
+
23
+ /**
24
+ * Checks if a tool is registered.
25
+ *
26
+ * @param string $slug The tool slug.
27
+ *
28
+ * @return bool
29
+ */
30
+ public function is_registered( string $slug ): bool {
31
+ return isset( $this->tools[ $slug ] );
32
+ }
33
+
34
+ /**
35
+ * Gets a tool by slug.
36
+ *
37
+ * @param string $slug
38
+ *
39
+ * @return Tool
40
+ */
41
+ public function get_tool( string $slug ): Tool {
42
+ return $this->tools[ $slug ];
43
+ }
44
+
45
+ /**
46
+ * Gets the list of registered tools.
47
+ *
48
+ * @return Tool[]
49
+ */
50
+ public function get_tools(): array {
51
+ return $this->tools;
52
+ }
53
+ }
core/lib/tools/Tools_Runner.php ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Tools;
4
+
5
+ use iThemesSecurity\Contracts\Runnable;
6
+ use iThemesSecurity\Lib\Result;
7
+
8
+ final class Tools_Runner implements Runnable {
9
+
10
+ /** @var Tools_Registry */
11
+ private $registry;
12
+
13
+ /**
14
+ * Runner constructor.
15
+ *
16
+ * @param Tools_Registry $registry
17
+ */
18
+ public function __construct( Tools_Registry $registry ) { $this->registry = $registry; }
19
+
20
+ public function run() {
21
+ /**
22
+ * Fires when tools should be registered.
23
+ *
24
+ * @param Tools_Registry $registry
25
+ */
26
+ do_action( 'itsec_register_tools', $this->registry );
27
+
28
+ foreach ( $this->registry->get_tools() as $tool ) {
29
+ if ( $schedule = $tool->get_schedule() ) {
30
+ add_action( "itsec_scheduled_{$schedule}", function () use ( $tool ) {
31
+ $this->run_tool( $tool );
32
+ } );
33
+ }
34
+
35
+ if ( $this->is_enabled( $tool ) ) {
36
+ $this->run_tool( $tool );
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Runs a tool.
43
+ *
44
+ * @param Tool $tool The tool to run.
45
+ * @param array $form The form data to provide to the tool.
46
+ *
47
+ * @return Result
48
+ */
49
+ public function run_tool( Tool $tool, array $form = [] ): Result {
50
+ if ( ! $tool->is_available() ) {
51
+ return Result::error( new \WP_Error(
52
+ 'itsec.tools.run.not-available',
53
+ __( 'This tool cannot run because it isn‘t available.', 'better-wp-security' )
54
+ ) );
55
+ }
56
+
57
+ if ( ! $this->is_condition_valid( $tool ) ) {
58
+ return Result::error( new \WP_Error(
59
+ 'itsec.tools.run.condition-invalid',
60
+ __( 'This tool cannot run because it‘s condition is not valid.', 'better-wp-security' )
61
+ ) );
62
+ }
63
+
64
+ if ( $tool->get_form() ) {
65
+ $valid = rest_validate_value_from_schema( $form, $tool->get_form() );
66
+
67
+ if ( is_wp_error( $valid ) ) {
68
+ return Result::error( $valid );
69
+ }
70
+
71
+ $sanitized = rest_sanitize_value_from_schema( $form, $tool->get_form() );
72
+
73
+ if ( is_wp_error( $sanitized ) ) {
74
+ return Result::error( $sanitized );
75
+ }
76
+
77
+ return $tool->run( $form );
78
+ }
79
+
80
+ return $tool->run();
81
+ }
82
+
83
+ /**
84
+ * Checks if the condition for the Tool is valid.
85
+ *
86
+ * @param Tool $tool
87
+ *
88
+ * @return bool
89
+ */
90
+ public function is_condition_valid( Tool $tool ): bool {
91
+ if ( ! $condition = $tool->get_condition() ) {
92
+ return true;
93
+ }
94
+
95
+ if ( isset( $condition['active-modules'] ) ) {
96
+ $active = array_filter( $condition['active-modules'], 'ITSEC_Modules::is_active' );
97
+
98
+ if ( count( $active ) !== count( $condition['active-modules'] ) ) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ if ( isset( $condition['settings'] ) ) {
104
+ foreach ( $condition['settings'] as $module => $schema ) {
105
+ $settings = \ITSEC_Modules::get_settings( $module );
106
+
107
+ if ( is_wp_error( rest_validate_value_from_schema( $settings, $schema ) ) ) {
108
+ return false;
109
+ }
110
+
111
+ if ( is_wp_error( rest_sanitize_value_from_schema( $settings, $schema ) ) ) {
112
+ return false;
113
+ }
114
+ }
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Checks if a toggleable tool is enabled.
122
+ *
123
+ * @param Tool $tool
124
+ *
125
+ * @return bool
126
+ */
127
+ public function is_enabled( Tool $tool ): bool {
128
+ $enabled = \ITSEC_Modules::get_setting( 'global', 'enabled_tools' );
129
+
130
+ return $tool->is_toggleable() && in_array( $tool->get_slug(), $enabled, true );
131
+ }
132
+
133
+ /**
134
+ * Enables a tool.
135
+ *
136
+ * @param Tool $tool
137
+ *
138
+ * @return true|\WP_Error
139
+ */
140
+ public function enable_tool( Tool $tool ) {
141
+ if ( ! $tool->is_toggleable() ) {
142
+ return new \WP_Error(
143
+ 'itsec.tools.enable-non-toggleable',
144
+ __( 'This tool cannot be enabled because it isn‘t a toggleable tool.', 'better-wp-security' )
145
+ );
146
+ }
147
+
148
+ $enabled = \ITSEC_Modules::get_setting( 'global', 'enabled_tools' );
149
+
150
+ if ( in_array( $tool->get_slug(), $enabled, true ) ) {
151
+ return true;
152
+ }
153
+
154
+ $enabled[] = $tool->get_slug();
155
+ $updated = \ITSEC_Modules::set_setting( 'global', 'enabled_tools', $enabled );
156
+
157
+ $error = \ITSEC_Lib::updated_settings_to_wp_error( $updated );
158
+
159
+ if ( is_wp_error( $error ) ) {
160
+ return $error;
161
+ }
162
+
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Disables a tool.
168
+ *
169
+ * @param Tool $tool
170
+ *
171
+ * @return true|\WP_Error
172
+ */
173
+ public function disable_tool( Tool $tool ) {
174
+ if ( ! $tool->is_toggleable() ) {
175
+ return new \WP_Error(
176
+ 'itsec.tools.disable-non-toggleable',
177
+ __( 'This tool cannot be disabled because it isn‘t a toggleable tool.', 'better-wp-security' )
178
+ );
179
+ }
180
+
181
+ $enabled = \ITSEC_Modules::get_setting( 'global', 'enabled_tools' );
182
+ $found = array_search( $tool->get_slug(), $enabled, true );
183
+
184
+ if ( $found === false ) {
185
+ return true;
186
+ }
187
+
188
+ unset( $enabled[ $found ] );
189
+ $updated = \ITSEC_Modules::set_setting( 'global', 'enabled_tools', $enabled );
190
+ $error = \ITSEC_Lib::updated_settings_to_wp_error( $updated );
191
+
192
+ if ( is_wp_error( $error ) ) {
193
+ return $error;
194
+ }
195
+
196
+ return true;
197
+ }
198
+ }
core/lib/tools/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/validator.php CHANGED
@@ -1,14 +1,22 @@
1
  <?php
2
 
 
3
  use iThemesSecurity\User_Groups\Matchables_Source;
4
 
5
  abstract class ITSEC_Validator {
6
  protected $run_validate_matching_fields = true;
7
  protected $run_validate_matching_types = true;
8
 
 
 
 
 
9
  protected $settings_obj;
10
- protected $defaults;
 
11
  protected $settings;
 
 
12
  protected $previous_settings;
13
 
14
  protected $can_save = true;
@@ -18,15 +26,14 @@ abstract class ITSEC_Validator {
18
  protected $vars_to_skip_validate_matching_fields = array();
19
  protected $vars_to_skip_validate_matching_types = array();
20
 
21
-
22
- public function __construct() {
 
 
 
 
 
23
  $this->settings_obj = ITSEC_Modules::get_settings_obj( $this->get_id() );
24
-
25
- if ( ! is_callable( array( $this->settings_obj, 'get_defaults' ) ) ) {
26
- return;
27
- }
28
-
29
- $this->defaults = $this->settings_obj->get_defaults();
30
  }
31
 
32
  abstract public function get_id();
@@ -55,15 +62,15 @@ abstract class ITSEC_Validator {
55
  protected function validate_matching_fields() {
56
  $id = $this->get_id();
57
 
58
- foreach ( array_keys( $this->defaults ) as $name ) {
59
- if ( ! isset( $this->settings[ $name ] ) && ! in_array( $name, $this->vars_to_skip_validate_matching_fields ) ) {
60
  $this->add_error( new WP_Error( "itsec-validator-$id-validate_matching_fields-missing-name-$name", sprintf( __( 'A validation function for %1$s received data that did not have the required entry for %2$s.', 'better-wp-security' ), $id, $name ) ) );
61
  $this->set_can_save( false );
62
  }
63
  }
64
 
65
- foreach ( array_keys( $this->settings ) as $name ) {
66
- if ( ! isset( $this->defaults[ $name ] ) && ! in_array( $name, $this->vars_to_skip_validate_matching_fields ) ) {
67
  $this->add_error( new WP_Error( "itsec-validator-$id-validate_matching_fields-unknown-name-$name", sprintf( __( 'A validation function for %1$s received data that has an entry for %2$s when no such entry exists.', 'better-wp-security' ), $id, $name ) ) );
68
  $this->set_can_save( false );
69
  }
@@ -73,29 +80,55 @@ abstract class ITSEC_Validator {
73
  protected function validate_matching_types() {
74
  $id = $this->get_id();
75
 
76
- foreach ( $this->defaults as $name => $value ) {
77
- if ( in_array( $name, $this->vars_to_skip_validate_matching_types ) ) {
78
  // This is to prevent errors for a specific var appearing twice.
79
  continue;
80
  }
81
 
82
- if ( ! isset( $this->settings[ $name ] ) ) {
83
- // Skip missing entries to allow implementations that use validate_matching_types() but not
84
- // validate_matching_fields().
85
- continue;
86
- }
87
-
88
- if ( gettype( $value ) !== gettype( $this->settings[ $name ] ) ) {
89
- $this->add_error( new WP_Error( "itsec-validator-$id-validate_matching_types-inmatching-type-$name", sprintf( __( 'A validation function for %1$s received data that does not match the expected data type for the %2$s entry. A data type of %3$s was expected, but a data type of %4$s was received.', 'better-wp-security' ), $id, $name, gettype( $value ), gettype( $this->settings[ $name ] ) ) ) );
 
 
 
 
 
 
 
90
  $this->set_can_save( false );
91
  }
92
  }
93
  }
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  final protected function set_default_if_empty( $vars ) {
96
  foreach ( (array) $vars as $var ) {
97
  if ( ! isset( $this->settings[ $var ] ) || '' === $this->settings[ $var ] ) {
98
- $this->settings[ $var ] = $this->defaults[ $var ];
99
  }
100
  }
101
  }
@@ -108,6 +141,14 @@ abstract class ITSEC_Validator {
108
  }
109
  }
110
 
 
 
 
 
 
 
 
 
111
  final protected function preserve_setting_if_exists( $vars ) {
112
  foreach ( (array) $vars as $var ) {
113
  if ( array_key_exists( $var, $this->previous_settings ) && ( ! isset( $this->settings[ $var ] ) || '' === $this->settings[ $var ] ) ) {
@@ -336,6 +377,7 @@ abstract class ITSEC_Validator {
336
 
337
  if ( ! empty( $invalid_entries ) ) {
338
  $error = wp_sprintf( _n( 'The following entry in %1$s is invalid: %2$l', 'The following entries in %1$s are invalid: %2$l', count( $invalid_entries ), 'better-wp-security' ), $name, $invalid_entries );
 
339
  }
340
  } elseif ( ! in_array( $this->settings[ $var ], $type, true ) ) {
341
  $error = wp_sprintf( _n( 'The valid value for %1$s is: %2$l.', 'The valid values for %1$s are: %2$l.', count( $type ), 'better-wp-security' ), $name, $type );
@@ -539,6 +581,54 @@ abstract class ITSEC_Validator {
539
  return true;
540
  }
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  protected function generate_error( $id, $var, $type, $error ) {
543
  return new WP_Error( "itsec-validator-$id-invalid-type-$var-$type", $error );
544
  }
@@ -575,11 +665,7 @@ abstract class ITSEC_Validator {
575
  }
576
 
577
  final public function found_errors() {
578
- if ( empty( $this->errors ) ) {
579
- return false;
580
- } else {
581
- return true;
582
- }
583
  }
584
 
585
  final public function get_errors() {
@@ -613,4 +699,20 @@ abstract class ITSEC_Validator {
613
  final public function get_settings() {
614
  return $this->settings;
615
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  }
1
  <?php
2
 
3
+ use iThemesSecurity\Module_Config;
4
  use iThemesSecurity\User_Groups\Matchables_Source;
5
 
6
  abstract class ITSEC_Validator {
7
  protected $run_validate_matching_fields = true;
8
  protected $run_validate_matching_types = true;
9
 
10
+ /** @var Module_Config */
11
+ protected $config;
12
+
13
+ /** @var ITSEC_Settings */
14
  protected $settings_obj;
15
+
16
+ /** @var array */
17
  protected $settings;
18
+
19
+ /** @var array */
20
  protected $previous_settings;
21
 
22
  protected $can_save = true;
26
  protected $vars_to_skip_validate_matching_fields = array();
27
  protected $vars_to_skip_validate_matching_types = array();
28
 
29
+ /**
30
+ * ITSEC_Validator constructor.
31
+ *
32
+ * @param Module_Config|null $config The configuration object. If omitted, will attempt to retrieve it.
33
+ */
34
+ public function __construct( Module_Config $config = null ) {
35
+ $this->config = $config ?: ITSEC_Modules::get_config( $this->get_id() );
36
  $this->settings_obj = ITSEC_Modules::get_settings_obj( $this->get_id() );
 
 
 
 
 
 
37
  }
38
 
39
  abstract public function get_id();
62
  protected function validate_matching_fields() {
63
  $id = $this->get_id();
64
 
65
+ foreach ( $this->settings_obj->get_known_settings() as $name ) {
66
+ if ( ! array_key_exists( $name, $this->settings ) && ! in_array( $name, $this->vars_to_skip_validate_matching_fields, true ) ) {
67
  $this->add_error( new WP_Error( "itsec-validator-$id-validate_matching_fields-missing-name-$name", sprintf( __( 'A validation function for %1$s received data that did not have the required entry for %2$s.', 'better-wp-security' ), $id, $name ) ) );
68
  $this->set_can_save( false );
69
  }
70
  }
71
 
72
+ foreach ( $this->settings as $name => $value ) {
73
+ if ( ! $this->settings_obj->is_known_setting( $name ) && ! in_array( $name, $this->vars_to_skip_validate_matching_fields, true ) && ! $this->is_extended_setting_from_inactive_module( $name ) ) {
74
  $this->add_error( new WP_Error( "itsec-validator-$id-validate_matching_fields-unknown-name-$name", sprintf( __( 'A validation function for %1$s received data that has an entry for %2$s when no such entry exists.', 'better-wp-security' ), $id, $name ) ) );
75
  $this->set_can_save( false );
76
  }
80
  protected function validate_matching_types() {
81
  $id = $this->get_id();
82
 
83
+ foreach ( $this->settings as $name => $value ) {
84
+ if ( in_array( $name, $this->vars_to_skip_validate_matching_types, true ) || ! $this->settings_obj->is_known_setting( $name ) ) {
85
  // This is to prevent errors for a specific var appearing twice.
86
  continue;
87
  }
88
 
89
+ $default = $this->settings_obj->get_default( $name );
90
+
91
+ if ( gettype( $default ) !== gettype( $value ) ) {
92
+ $this->add_error(
93
+ new WP_Error(
94
+ "itsec-validator-$id-validate_matching_types-inmatching-type-$name",
95
+ sprintf(
96
+ __( 'A validation function for %1$s received data that does not match the expected data type for the %2$s entry. A data type of %3$s was expected, but a data type of %4$s was received.', 'better-wp-security' ),
97
+ $id,
98
+ $name,
99
+ gettype( $default ),
100
+ gettype( $value )
101
+ )
102
+ )
103
+ );
104
  $this->set_can_save( false );
105
  }
106
  }
107
  }
108
 
109
+ /**
110
+ * Checks if the given setting is from a now deactivated module that extends this module.
111
+ *
112
+ * This allow for preserving settings from pro modules if the user downgrades to free.
113
+ *
114
+ * @param string $setting
115
+ *
116
+ * @return bool
117
+ */
118
+ final protected function is_extended_setting_from_inactive_module( string $setting ): bool {
119
+ $extended = ITSEC_Storage::get( '__extended' );
120
+
121
+ if ( empty( $extended[ $this->get_id() ] ) ) {
122
+ return false;
123
+ }
124
+
125
+ return in_array( $setting, $extended[ $this->get_id() ], true );
126
+ }
127
+
128
  final protected function set_default_if_empty( $vars ) {
129
  foreach ( (array) $vars as $var ) {
130
  if ( ! isset( $this->settings[ $var ] ) || '' === $this->settings[ $var ] ) {
131
+ $this->settings[ $var ] = $this->settings_obj->get_default( $var );
132
  }
133
  }
134
  }
141
  }
142
  }
143
 
144
+ final protected function set_previous_if_missing( $vars ) {
145
+ foreach ( (array) $vars as $var ) {
146
+ if ( ! isset( $this->settings[ $var ] ) ) {
147
+ $this->settings[ $var ] = $this->previous_settings[ $var ];
148
+ }
149
+ }
150
+ }
151
+
152
  final protected function preserve_setting_if_exists( $vars ) {
153
  foreach ( (array) $vars as $var ) {
154
  if ( array_key_exists( $var, $this->previous_settings ) && ( ! isset( $this->settings[ $var ] ) || '' === $this->settings[ $var ] ) ) {
377
 
378
  if ( ! empty( $invalid_entries ) ) {
379
  $error = wp_sprintf( _n( 'The following entry in %1$s is invalid: %2$l', 'The following entries in %1$s are invalid: %2$l', count( $invalid_entries ), 'better-wp-security' ), $name, $invalid_entries );
380
+ $type = 'array';
381
  }
382
  } elseif ( ! in_array( $this->settings[ $var ], $type, true ) ) {
383
  $error = wp_sprintf( _n( 'The valid value for %1$s is: %2$l.', 'The valid values for %1$s are: %2$l.', count( $type ), 'better-wp-security' ), $name, $type );
581
  return true;
582
  }
583
 
584
+ /**
585
+ * Validates a user groups setting.
586
+ *
587
+ * @param string $name The setting label.
588
+ * @param string $var The setting var.
589
+ *
590
+ * @return array|WP_Error The sanitized user groups, or a WP_Error.
591
+ */
592
+ protected function validate_user_groups( $name, $var ) {
593
+ $new_value = ITSEC_Lib::array_get( $this->settings, $var );
594
+ $previous_value = ITSEC_Lib::array_get( $this->previous_settings, $var );
595
+
596
+ $source = ITSEC_Modules::get_container()->get( Matchables_Source::class );
597
+ $invalid_user_groups = [];
598
+
599
+ foreach ( $new_value as $i => $group ) {
600
+ if ( ! is_string( $group ) ) {
601
+ unset( $new_value[ $i ] );
602
+
603
+ continue;
604
+ }
605
+
606
+ if ( $source->has( $group ) ) {
607
+ continue;
608
+ }
609
+
610
+ if ( in_array( $group, $previous_value, true ) ) {
611
+ unset( $new_value[ $i ] );
612
+ } else {
613
+ $invalid_user_groups[] = $group;
614
+ }
615
+ }
616
+
617
+ $new_value = wp_is_numeric_array( $new_value ) ? array_values( $new_value ) : $new_value;
618
+
619
+ if ( ! $invalid_user_groups ) {
620
+ return $new_value;
621
+ }
622
+
623
+ $error = wp_sprintf(
624
+ _n( 'The following entry in %1$s is invalid: %2$l', 'The following entries in %1$s are invalid: %2$l', count( $invalid_user_groups ), 'better-wp-security' ),
625
+ $name,
626
+ $invalid_user_groups
627
+ );
628
+
629
+ return $this->generate_error( $this->get_id(), $var, 'user-groups', $error );
630
+ }
631
+
632
  protected function generate_error( $id, $var, $type, $error ) {
633
  return new WP_Error( "itsec-validator-$id-invalid-type-$var-$type", $error );
634
  }
665
  }
666
 
667
  final public function found_errors() {
668
+ return ! empty( $this->errors );
 
 
 
 
669
  }
670
 
671
  final public function get_errors() {
699
  final public function get_settings() {
700
  return $this->settings;
701
  }
702
+
703
+ public function __get( $name ) {
704
+ if ( 'defaults' === $name ) {
705
+ _deprecated_function( static::class . '::$defaults', '7.0.0', 'ITSEC_Modules::get_default' );
706
+
707
+ return $this->settings_obj->get_defaults();
708
+ }
709
+
710
+ trigger_error( sprintf( 'Undefined property: %s::$%s', static::class, $name ) );
711
+
712
+ return null;
713
+ }
714
+
715
+ public function __isset( $name ) {
716
+ return 'defaults' === $name;
717
+ }
718
  }
core/lockout.php CHANGED
@@ -76,7 +76,6 @@ final class ITSEC_Lockout {
76
  }
77
 
78
  public function run() {
79
- add_action( 'itsec_scheduler_register_events', array( $this, 'register_events' ) );
80
  add_action( 'itsec_scheduled_purge-lockouts', array( $this, 'purge_lockouts' ) );
81
 
82
  //Check for host lockouts
@@ -86,7 +85,7 @@ final class ITSEC_Lockout {
86
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
87
 
88
  // Updated temp whitelist to ensure that admin users are automatically added.
89
- if ( ! defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) || ! ITSEC_DISABLE_TEMP_WHITELIST ) {
90
  add_action( 'init', array( $this, 'update_temp_whitelist' ), 0 );
91
  }
92
 
@@ -96,19 +95,12 @@ final class ITSEC_Lockout {
96
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
97
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
98
 
99
- add_action( 'itsec-settings-page-init', array( $this, 'init_settings_page' ) );
100
- add_action( 'itsec-logs-page-init', array( $this, 'init_settings_page' ) );
101
-
102
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
103
  add_filter( 'itsec_lockout_notification_strings', array( $this, 'notification_strings' ) );
104
 
105
  add_filter( 'itsec_logs_prepare_lockout_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
106
  }
107
 
108
- public function init_settings_page() {
109
- require_once( dirname( __FILE__ ) . '/sidebar-widget-active-lockouts.php' );
110
- }
111
-
112
  /**
113
  * Check if a user has successfully logged-in, and prevent them from accessing the site if they
114
  * still have a lockout in effect.
@@ -829,33 +821,9 @@ final class ITSEC_Lockout {
829
  * @return string the description of settings.
830
  */
831
  public function get_lockout_description() {
832
- $global_settings_url = add_query_arg( array( 'module' => 'global' ), ITSEC_Core::get_settings_page_url() ) . '#itsec-global-blacklist';
833
- // If the user is currently viewing "all" then let them keep viewing all
834
- if ( ! empty( $_GET['module_type'] ) && 'all' === $_GET['module_type'] ) {
835
- $global_settings_url = add_query_arg( array( 'module_type', 'all' ), $global_settings_url );
836
- }
837
-
838
- $description = '<h4>' . __( 'About Lockouts', 'better-wp-security' ) . '</h4>';
839
- $description .= '<p>';
840
- $description .= sprintf( __( 'Your lockout settings can be configured in <a href="%s" data-module-link="global">Global Settings</a>.', 'better-wp-security' ), esc_url( $global_settings_url ) );
841
- $description .= '<br />';
842
- $description .= __( 'Your current settings are configured as follows:', 'better-wp-security' );
843
- $description .= '<ul><li>';
844
- $description .= sprintf( __( '<strong>Permanently ban:</strong> %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist' ) === true ? __( 'yes', 'better-wp-security' ) : __( 'no', 'better-wp-security' ) );
845
- $description .= '</li><li>';
846
- $description .= sprintf( __( '<strong>Number of lockouts before permanent ban:</strong> %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist_count' ) );
847
- $description .= '</li><li>';
848
- $description .= sprintf( __( '<strong>How long lockouts will be remembered for ban:</strong> %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) );
849
- $description .= '</li><li>';
850
- $description .= sprintf( __( '<strong>Host lockout message:</strong> %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'lockout_message' ) );
851
- $description .= '</li><li>';
852
- $description .= sprintf( __( '<strong>User lockout message:</strong> %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'user_lockout_message' ) );
853
- $description .= '</li><li>';
854
- $description .= sprintf( __( '<strong>Is this computer an authorized host:</strong> %s', 'better-wp-security' ), ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) === true ? __( 'yes', 'better-wp-security' ) : __( 'no', 'better-wp-security' ) );
855
- $description .= '</li></ul>';
856
-
857
- return $description;
858
 
 
859
  }
860
 
861
  /**
@@ -975,6 +943,19 @@ final class ITSEC_Lockout {
975
  return $results;
976
  }
977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  /**
979
  * Retrieve a list of the temporary whitelisted IP addresses.
980
  *
@@ -1109,8 +1090,7 @@ final class ITSEC_Lockout {
1109
  * @return bool
1110
  */
1111
  public function is_visitor_temp_whitelisted() {
1112
-
1113
- if ( defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) && ITSEC_DISABLE_TEMP_WHITELIST ) {
1114
  return false;
1115
  }
1116
 
@@ -1124,15 +1104,6 @@ final class ITSEC_Lockout {
1124
  return false;
1125
  }
1126
 
1127
- /**
1128
- * Register the purge lockout event.
1129
- *
1130
- * @param ITSEC_Scheduler $scheduler
1131
- */
1132
- public function register_events( $scheduler ) {
1133
- $scheduler->schedule( ITSEC_Scheduler::S_DAILY, 'purge-lockouts' );
1134
- }
1135
-
1136
  /**
1137
  * Purges lockouts more than 7 days old from the database
1138
  *
@@ -1272,7 +1243,7 @@ final class ITSEC_Lockout {
1272
  public function register_notification( $notifications ) {
1273
  $notifications['lockout'] = array(
1274
  'subject_editable' => true,
1275
- 'recipient' => ITSEC_Notification_Center::R_USER_LIST_ADMIN_UPGRADE,
1276
  'schedule' => ITSEC_Notification_Center::S_NONE,
1277
  'optional' => true,
1278
  );
@@ -1287,9 +1258,9 @@ final class ITSEC_Lockout {
1287
  */
1288
  public function notification_strings() {
1289
  return array(
1290
- 'label' => esc_html__( 'Site Lockouts', 'better-wp-security' ),
1291
- 'description' => esc_html__( 'Various modules send emails to notify you when a user or host is locked out of your website.', 'better-wp-security' ),
1292
- 'subject' => esc_html__( 'Site Lockout Notification', 'better-wp-security' ),
1293
  );
1294
  }
1295
 
76
  }
77
 
78
  public function run() {
 
79
  add_action( 'itsec_scheduled_purge-lockouts', array( $this, 'purge_lockouts' ) );
80
 
81
  //Check for host lockouts
85
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
86
 
87
  // Updated temp whitelist to ensure that admin users are automatically added.
88
+ if ( $this->is_temp_authorization_enabled() ) {
89
  add_action( 'init', array( $this, 'update_temp_whitelist' ), 0 );
90
  }
91
 
95
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
96
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
97
 
 
 
 
98
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
99
  add_filter( 'itsec_lockout_notification_strings', array( $this, 'notification_strings' ) );
100
 
101
  add_filter( 'itsec_logs_prepare_lockout_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
102
  }
103
 
 
 
 
 
104
  /**
105
  * Check if a user has successfully logged-in, and prevent them from accessing the site if they
106
  * still have a lockout in effect.
821
  * @return string the description of settings.
822
  */
823
  public function get_lockout_description() {
824
+ _deprecated_function( __METHOD__, '7.0.0' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
 
826
+ return '';
827
  }
828
 
829
  /**
943
  return $results;
944
  }
945
 
946
+ /**
947
+ * Checks if temp host authorization is enabled.
948
+ *
949
+ * @return bool
950
+ */
951
+ public function is_temp_authorization_enabled() {
952
+ if ( defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) && ITSEC_DISABLE_TEMP_WHITELIST ) {
953
+ return false;
954
+ }
955
+
956
+ return ITSEC_Modules::get_setting( 'global', 'automatic_temp_auth' );
957
+ }
958
+
959
  /**
960
  * Retrieve a list of the temporary whitelisted IP addresses.
961
  *
1090
  * @return bool
1091
  */
1092
  public function is_visitor_temp_whitelisted() {
1093
+ if ( ! $this->is_temp_authorization_enabled() ) {
 
1094
  return false;
1095
  }
1096
 
1104
  return false;
1105
  }
1106
 
 
 
 
 
 
 
 
 
 
1107
  /**
1108
  * Purges lockouts more than 7 days old from the database
1109
  *
1243
  public function register_notification( $notifications ) {
1244
  $notifications['lockout'] = array(
1245
  'subject_editable' => true,
1246
+ 'recipient' => ITSEC_Notification_Center::R_USER_LIST,
1247
  'schedule' => ITSEC_Notification_Center::S_NONE,
1248
  'optional' => true,
1249
  );
1258
  */
1259
  public function notification_strings() {
1260
  return array(
1261
+ 'label' => __( 'Site Lockouts', 'better-wp-security' ),
1262
+ 'description' => __( 'Various modules send emails to notify you when a user or host is locked out of your website.', 'better-wp-security' ),
1263
+ 'subject' => __( 'Site Lockout Notification', 'better-wp-security' ),
1264
  );
1265
  }
1266
 
core/module-schema.json ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "object",
3
+ "required": [
4
+ "id",
5
+ "status",
6
+ "type"
7
+ ],
8
+ "properties": {
9
+ "id": {
10
+ "type": "string"
11
+ },
12
+ "status": {
13
+ "oneOf": [
14
+ {
15
+ "type": "string",
16
+ "enum": [
17
+ "always-active",
18
+ "default-active",
19
+ "default-inactive",
20
+ "inherit"
21
+ ]
22
+ },
23
+ {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "required": [
27
+ "free",
28
+ "pro"
29
+ ],
30
+ "properties": {
31
+ "free": {
32
+ "type": "string",
33
+ "enum": [
34
+ "always-active",
35
+ "default-active",
36
+ "default-inactive"
37
+ ]
38
+ },
39
+ "pro": {
40
+ "type": "string",
41
+ "enum": [
42
+ "always-active",
43
+ "default-active",
44
+ "default-inactive"
45
+ ]
46
+ }
47
+ }
48
+ }
49
+ ]
50
+ },
51
+ "type": {
52
+ "type": "string",
53
+ "enum": [
54
+ "login",
55
+ "lockout",
56
+ "utility",
57
+ "site-check",
58
+ "recommended",
59
+ "advanced",
60
+ "tool",
61
+ "custom"
62
+ ]
63
+ },
64
+ "onboard": {
65
+ "type": "boolean"
66
+ },
67
+ "side-effects": {
68
+ "type": "boolean"
69
+ },
70
+ "deprecated": {
71
+ "title": "Version Deprecated",
72
+ "description": "Deprecated modules will issue a warning when a module file is loaded.",
73
+ "type": "string"
74
+ },
75
+ "keywords": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": "string"
79
+ },
80
+ "uniqueItems": true
81
+ },
82
+ "title": {
83
+ "type": "string"
84
+ },
85
+ "description": {
86
+ "type": "string"
87
+ },
88
+ "help": {
89
+ "type": "string"
90
+ },
91
+ "user-groups": {
92
+ "type": "object",
93
+ "additionalProperties": {
94
+ "type": "object",
95
+ "required": [
96
+ "type",
97
+ "title",
98
+ "description"
99
+ ],
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "type": {
103
+ "type": "string",
104
+ "enum": [
105
+ "multiple",
106
+ "single"
107
+ ]
108
+ },
109
+ "title": {
110
+ "type": "string"
111
+ },
112
+ "description": {
113
+ "type": "string"
114
+ },
115
+ "keywords": {
116
+ "type": "array",
117
+ "items": {
118
+ "type": "string"
119
+ },
120
+ "uniqueItems": true
121
+ },
122
+ "default": {
123
+ "anyOf": [
124
+ {
125
+ "type": "string",
126
+ "enum": [
127
+ "all",
128
+ "administrator",
129
+ "editor",
130
+ "author",
131
+ "contributor",
132
+ "subscriber"
133
+ ]
134
+ },
135
+ {
136
+ "type": "array",
137
+ "items": {
138
+ "type": "string",
139
+ "enum": [
140
+ "administrator",
141
+ "editor",
142
+ "author",
143
+ "contributor",
144
+ "subscriber"
145
+ ]
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ "conditional": {
151
+ "type": "object",
152
+ "properties": {
153
+ "settings": {
154
+ "description": "The entire settings object must validate against the given JSON Schema.",
155
+ "type": "object"
156
+ },
157
+ "active-modules": {
158
+ "description": "The given modules must be active.",
159
+ "type": "array",
160
+ "uniqueItems": true,
161
+ "minItems": 1,
162
+ "items": {
163
+ "type": "string"
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ },
171
+ "password-requirements": {
172
+ "type": "object",
173
+ "additionalProperties": {
174
+ "type": "object",
175
+ "properties": {
176
+ "title": {
177
+ "type": "string"
178
+ },
179
+ "description": {
180
+ "type": "string"
181
+ },
182
+ "user-group": {
183
+ "type": "boolean"
184
+ },
185
+ "settings": {
186
+ "type": "object"
187
+ }
188
+ }
189
+ }
190
+ },
191
+ "tools": {
192
+ "type": "object",
193
+ "additionalProperties": {
194
+ "type": "object",
195
+ "properties": {
196
+ "title": {
197
+ "type": "string"
198
+ },
199
+ "description": {
200
+ "type": "string"
201
+ },
202
+ "help": {
203
+ "type": "string"
204
+ },
205
+ "keywords": {
206
+ "type": "array",
207
+ "items": {
208
+ "type": "string"
209
+ },
210
+ "uniqueItems": true
211
+ },
212
+ "schedule": {
213
+ "description": "If this tool should run automatically, this should contain the schedule id defined in the scheduling config.",
214
+ "type": "string"
215
+ },
216
+ "toggle": {
217
+ "description": "If this tool can be toggled on or off.",
218
+ "type": "boolean"
219
+ },
220
+ "form": {
221
+ "description": "Optionally, specify a JSON Schema used to collect data from the user before running the tool.",
222
+ "type": "object",
223
+ "properties": {
224
+ "type": {
225
+ "type": "string",
226
+ "enum": [
227
+ "object"
228
+ ]
229
+ }
230
+ },
231
+ "additionalProperties": true
232
+ },
233
+ "condition": {
234
+ "description": "Describe the conditions in which this tool can be run.",
235
+ "type": "object",
236
+ "properties": {
237
+ "description": {
238
+ "type": "string",
239
+ "description": "User-facing string describing the conditional requirements."
240
+ },
241
+ "settings": {
242
+ "description": "A map of module names, to a settings schema that the module's settings must validate against.",
243
+ "type": "object"
244
+ },
245
+ "active-modules": {
246
+ "description": "The given modules must be active.",
247
+ "type": "array",
248
+ "uniqueItems": true,
249
+ "minItems": 1,
250
+ "items": {
251
+ "type": "string"
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ },
259
+ "scheduling": {
260
+ "type": "object",
261
+ "additionalProperties": {
262
+ "type": "object",
263
+ "required": [
264
+ "schedule",
265
+ "type"
266
+ ],
267
+ "properties": {
268
+ "schedule": {
269
+ "type": "string"
270
+ },
271
+ "type": {
272
+ "type": "string",
273
+ "enum": [
274
+ "recurring"
275
+ ]
276
+ },
277
+ "data": {
278
+ "type": "object",
279
+ "additionalProperties": true
280
+ },
281
+ "opts": {
282
+ "type": "object",
283
+ "properties": {
284
+ "fire_at": {
285
+ "title": "First Fire At",
286
+ "description": "The number of seconds from now that the first evetn should be fired.",
287
+ "type": "integer",
288
+ "minimum": 0
289
+ }
290
+ },
291
+ "additionalProperties": true
292
+ },
293
+ "conditional": {
294
+ "description": "A JSON schema. The event will be registered if the entire settings object validates against the given schema.",
295
+ "type": "object"
296
+ }
297
+ }
298
+ }
299
+ },
300
+ "settings": {
301
+ "type": "object",
302
+ "properties": {
303
+ "type": {
304
+ "type": "string",
305
+ "enum": [
306
+ "object"
307
+ ]
308
+ },
309
+ "properties": {
310
+ "type": "object",
311
+ "properties": {
312
+ },
313
+ "additionalProperties": {
314
+ "type": "object",
315
+ "required": [
316
+ "default"
317
+ ]
318
+ }
319
+ },
320
+ "uiSchema": {
321
+ "type": "object",
322
+ "properties": {
323
+ "ui:sections": {
324
+ "type": "array",
325
+ "items": {
326
+ "type": "object",
327
+ "additionalProperties": false,
328
+ "properties": {
329
+ "title": {
330
+ "type": "string"
331
+ },
332
+ "description": {
333
+ "type": "string"
334
+ },
335
+ "fields": {
336
+ "type": "array",
337
+ "items": {
338
+ "type": "string"
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ },
347
+ "additionalProperties": true
348
+ },
349
+ "conditional-settings": {
350
+ "description": "An object of setting names to conditional definitions.",
351
+ "type": "object",
352
+ "additionalProperties": {
353
+ "type": "object",
354
+ "additionalProperties": false,
355
+ "properties": {
356
+ "settings": {
357
+ "description": "The entire settings object must validate against the given JSON Schema.",
358
+ "type": "object"
359
+ },
360
+ "server-type": {
361
+ "description": "The server type must be one of the given values.",
362
+ "type": "array",
363
+ "items": {
364
+ "type": "string",
365
+ "enum": [
366
+ "apache",
367
+ "nginx",
368
+ "litespeed",
369
+ "thttpd",
370
+ "iis"
371
+ ]
372
+ }
373
+ },
374
+ "active-modules": {
375
+ "description": "The given modules must be active.",
376
+ "type": "array",
377
+ "uniqueItems": true,
378
+ "minItems": 1,
379
+ "items": {
380
+ "type": "string"
381
+ }
382
+ },
383
+ "user-groups": {
384
+ "description": "List of user groups setting names that must contain at least one user group. Settings must be members of this module.",
385
+ "type": "array",
386
+ "uniqueItems": true,
387
+ "minItems": 1,
388
+ "items": {
389
+ "type": "string"
390
+ }
391
+ },
392
+ "install-type": {
393
+ "description": "The installation type the setting is limited to.",
394
+ "type": "string",
395
+ "enum": [
396
+ "pro",
397
+ "free"
398
+ ]
399
+ }
400
+ }
401
+ }
402
+ },
403
+ "removed-settings": {
404
+ "description": "List of settings that used to exist, but no longer do and should be removed.",
405
+ "type": "array",
406
+ "uniqueItems": true,
407
+ "items": {
408
+ "type": "string"
409
+ }
410
+ },
411
+ "deprecated-settings": {
412
+ "description": "List of settings that are no longer used, but if set should be preserved.",
413
+ "type": "array",
414
+ "uniqueItems": true,
415
+ "items": {
416
+ "type": "string"
417
+ }
418
+ },
419
+ "onboard-settings": {
420
+ "description": "List of settings to present in the onboard sequence.",
421
+ "type": "array",
422
+ "uniqueItems": true,
423
+ "items": {
424
+ "type": "string"
425
+ }
426
+ },
427
+ "requirements": {
428
+ "type": "object",
429
+ "properties": {
430
+ "ssl": {
431
+ "$ref": "#/definitions/requirement"
432
+ }
433
+ },
434
+ "additionalProperties": false
435
+ }
436
+ },
437
+ "definitions": {
438
+ "requirement": {
439
+ "type": "object",
440
+ "required": [
441
+ "validate"
442
+ ],
443
+ "properties": {
444
+ "validate": {
445
+ "type": "string",
446
+ "enum": [
447
+ "activate",
448
+ "run"
449
+ ]
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
core/modules.php CHANGED
@@ -1,10 +1,17 @@
1
  <?php
2
 
 
 
3
  use iThemesSecurity\Contracts\Runnable;
4
  use iThemesSecurity\Exception\Unsatisfied_Module_Dependencies_Exception;
 
5
  use Pimple\Container;
6
 
7
  final class ITSEC_Modules {
 
 
 
 
8
  /**
9
  * @var ITSEC_Modules - Static property to hold our singleton instance
10
  */
@@ -12,16 +19,25 @@ final class ITSEC_Modules {
12
 
13
  private $_available_modules = false;
14
  private $_module_paths = array();
 
 
 
15
  private $_default_active_modules = array();
16
  private $_always_active_modules = array();
 
17
  private $_active_modules = false;
18
  private $_active_modules_list = false;
19
- private $_module_settings = false;
20
- private $_module_validators = false;
21
- private $_settings_files_loaded = false;
 
 
 
 
22
  private $loaded_containers = [];
23
  private $labels = array();
24
  private $returned_files = array();
 
25
 
26
  /** @var Container */
27
  private $pimple;
@@ -33,9 +49,7 @@ final class ITSEC_Modules {
33
  private $initialized_container = false;
34
 
35
  protected function __construct() {
36
- // Action triggered from another part of Security which runs when the settings page is loaded.
37
- add_action( 'itsec-settings-page-init', array( $this, 'load_settings_page' ) );
38
- add_action( 'itsec-logs-page-init', array( $this, 'load_settings_page' ) );
39
 
40
  $this->pimple = new Container();
41
  $this->container = new Pimple\Psr11\Container( $this->pimple );
@@ -75,6 +89,47 @@ final class ITSEC_Modules {
75
  return false;
76
  }
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  $self->_module_paths[ $slug ] = $path;
79
  $self->_available_modules = array_keys( $self->_module_paths );
80
 
@@ -153,6 +208,10 @@ final class ITSEC_Modules {
153
  self::load_module_file( 'settings.php', $slug );
154
  }
155
 
 
 
 
 
156
  if ( ! isset( $self->_module_settings[ $slug ] ) ) {
157
  return null;
158
  }
@@ -170,11 +229,11 @@ final class ITSEC_Modules {
170
  public static function get_defaults( $slug ) {
171
  $settings_obj = self::get_settings_obj( $slug );
172
 
173
- if ( is_null( $settings_obj ) || ! is_callable( array( $settings_obj, 'get_defaults' ) ) ) {
174
- return array();
175
  }
176
 
177
- return $settings_obj->get_defaults();
178
  }
179
 
180
  /**
@@ -187,10 +246,10 @@ final class ITSEC_Modules {
187
  * @return mixed
188
  */
189
  public static function get_default( $slug, $name, $default = null ) {
190
- $defaults = self::get_defaults( $slug );
191
 
192
- if ( isset( $defaults[ $name ] ) ) {
193
- return $defaults[ $name ];
194
  }
195
 
196
  return $default;
@@ -206,11 +265,11 @@ final class ITSEC_Modules {
206
  public static function get_settings( $slug ) {
207
  $settings_obj = self::get_settings_obj( $slug );
208
 
209
- if ( is_null( $settings_obj ) || ! is_callable( array( $settings_obj, 'get_all' ) ) ) {
210
- return array();
211
  }
212
 
213
- return $settings_obj->get_all();
214
  }
215
 
216
  /**
@@ -226,11 +285,11 @@ final class ITSEC_Modules {
226
  public static function get_setting( $slug, $name, $default = null ) {
227
  $settings_obj = self::get_settings_obj( $slug );
228
 
229
- if ( is_null( $settings_obj ) || ! is_callable( array( $settings_obj, 'get' ) ) ) {
230
- return $default;
231
  }
232
 
233
- return $settings_obj->get( $name, $default );
234
  }
235
 
236
  /**
@@ -246,7 +305,7 @@ final class ITSEC_Modules {
246
  public static function set_settings( $slug, $settings ) {
247
  $settings_obj = self::get_settings_obj( $slug );
248
 
249
- if ( is_null( $settings_obj ) || ! is_callable( array( $settings_obj, 'set_all' ) ) ) {
250
  $error = new WP_Error( 'itsec-modules-invalid-settings-object', sprintf( __( 'Unable to find a valid settings object for %s. Settings were unable to be saved.', 'better-wp-security' ), $slug ) );
251
  ITSEC_Response::add_error( $error );
252
 
@@ -271,7 +330,7 @@ final class ITSEC_Modules {
271
  public static function set_setting( $slug, $name, $value ) {
272
  $settings_obj = self::get_settings_obj( $slug );
273
 
274
- if ( is_null( $settings_obj ) || ! is_callable( array( $settings_obj, 'set_all' ) ) ) {
275
  trigger_error( sprintf( __( 'Unable to find a valid settings object for %s. Setting was unable to be saved.', 'better-wp-security' ), $slug ) );
276
 
277
  return false;
@@ -280,6 +339,15 @@ final class ITSEC_Modules {
280
  return $settings_obj->set( $name, $value );
281
  }
282
 
 
 
 
 
 
 
 
 
 
283
  /**
284
  * Register a module's validator controller.
285
  *
@@ -308,6 +376,10 @@ final class ITSEC_Modules {
308
  self::load_module_file( 'validator.php', $slug );
309
  }
310
 
 
 
 
 
311
  if ( ! isset( $self->_module_validators[ $slug ] ) ) {
312
  return null;
313
  }
@@ -315,6 +387,28 @@ final class ITSEC_Modules {
315
  return $self->_module_validators[ $slug ];
316
  }
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  /**
319
  * Retrieve the slugs of all modules available to the plugin.
320
  *
@@ -338,6 +432,17 @@ final class ITSEC_Modules {
338
  return $self->_available_modules;
339
  }
340
 
 
 
 
 
 
 
 
 
 
 
 
341
  /**
342
  * Retrieve the slugs of all active modules.
343
  *
@@ -455,17 +560,34 @@ final class ITSEC_Modules {
455
  * Activate a single module using its ID
456
  *
457
  * @param string $module_id The ID of the module to activate
 
 
458
  *
459
  * @return bool|WP_Error If the module can be activated, true if it was previously active and false if it was
460
  * previously inactive. If the module cannot be activated, a WP_Error object is returned.
461
  */
462
- public static function activate( $module_id ) {
463
  $self = self::get_instance();
464
 
465
  if ( self::is_always_active( $module_id ) ) {
466
  return new WP_Error( 'itsec-modules-cannot-activate-always-active-module', sprintf( __( 'The %s module is a Core module and cannot be activated or deactivated.', 'better-wp-security' ), $module_id ) );
467
  }
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  if ( ! is_array( $self->_active_modules ) ) {
470
  self::get_active_modules();
471
  }
@@ -479,7 +601,9 @@ final class ITSEC_Modules {
479
  try {
480
  self::load_module_file( 'activate.php', $module_id );
481
  } catch ( Unsatisfied_Module_Dependencies_Exception $e ) {
482
- return new WP_Error( 'itsec-modules-cannot-activate-module-unsatisfied-dependencies', $e->getMessage() );
 
 
483
  }
484
 
485
  $self->_active_modules[ $module_id ] = true;
@@ -520,6 +644,10 @@ final class ITSEC_Modules {
520
  $self->_active_modules[ $module_id ] = false;
521
  self::set_active_modules( $self->_active_modules );
522
 
 
 
 
 
523
  return $was_active;
524
  }
525
 
@@ -574,15 +702,15 @@ final class ITSEC_Modules {
574
  * @return bool|WP_Error True if a module matching the $modules parameter is found, false otherwise.
575
  */
576
  public static function load_module_file( $file, $modules = ':all', callable $process = null ) {
 
 
 
 
 
 
577
  $self = self::get_instance();
578
 
579
- if ( ':all' === $modules ) {
580
- $modules = self::get_available_modules();
581
- } elseif ( ':active' === $modules ) {
582
- $modules = self::get_active_modules_to_run();
583
- } elseif ( is_string( $modules ) ) {
584
- $modules = array( $modules );
585
- } elseif ( ! is_array( $modules ) ) {
586
  return false;
587
  }
588
 
@@ -591,6 +719,12 @@ final class ITSEC_Modules {
591
  continue;
592
  }
593
 
 
 
 
 
 
 
594
  $self->load_container_definitions( $module );
595
  $returned = null;
596
 
@@ -612,7 +746,7 @@ final class ITSEC_Modules {
612
  if ( array_key_exists( $path, $self->returned_files ) ) {
613
  $returned = $self->returned_files[ $path ];
614
  } else {
615
- $returned = include_once( $path );
616
  $self->returned_files[ $path ] = $returned;
617
  }
618
  }
@@ -629,21 +763,40 @@ final class ITSEC_Modules {
629
  return true;
630
  }
631
 
 
 
 
 
 
 
 
 
 
 
 
632
  /**
633
  * Get a list of the active modules to run.
634
  *
635
  * @return string[]
636
  */
637
- protected static function get_active_modules_to_run() {
638
  if ( ITSEC_Core::is_temp_disable_modules_set() ) {
639
  $modules = array();
640
  } else {
641
- $modules = self::get_active_modules();
 
 
642
  }
643
 
644
  $modules = array_merge( $modules, array_keys( self::get_instance()->_always_active_modules ) );
645
  $modules = array_unique( $modules );
646
 
 
 
 
 
 
 
647
  return $modules;
648
  }
649
 
@@ -738,13 +891,7 @@ final class ITSEC_Modules {
738
  * This function can only be run once per-request.
739
  */
740
  public function load_settings_page() {
741
- if ( $this->_settings_files_loaded ) {
742
- return;
743
- }
744
-
745
  self::load_module_file( 'settings-page.php' );
746
-
747
- $this->_settings_files_loaded = true;
748
  }
749
 
750
  /**
@@ -755,6 +902,15 @@ final class ITSEC_Modules {
755
  * @return array
756
  */
757
  public static function get_labels( $module ) {
 
 
 
 
 
 
 
 
 
758
  if ( ! isset( self::get_instance()->labels[ $module ] ) ) {
759
  self::get_instance()->labels[ $module ] = [];
760
  self::load_module_file( 'labels.php', $module, function ( $labels, $module ) {
@@ -767,6 +923,93 @@ final class ITSEC_Modules {
767
  return self::get_instance()->labels[ $module ];
768
  }
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  private function run( $definition ) {
771
  if ( $definition && is_string( $definition ) ) {
772
  $object = $this->container->get( $definition );
1
  <?php
2
 
3
+ use iThemesSecurity\Config_Settings;
4
+ use iThemesSecurity\Config_Validator;
5
  use iThemesSecurity\Contracts\Runnable;
6
  use iThemesSecurity\Exception\Unsatisfied_Module_Dependencies_Exception;
7
+ use iThemesSecurity\Module_Config;
8
  use Pimple\Container;
9
 
10
  final class ITSEC_Modules {
11
+ const DEPRECATED = [
12
+ 'settings-page.php' => '7.0.0',
13
+ ];
14
+
15
  /**
16
  * @var ITSEC_Modules - Static property to hold our singleton instance
17
  */
19
 
20
  private $_available_modules = false;
21
  private $_module_paths = array();
22
+
23
+ /** @var Module_Config[] */
24
+ private $module_config = [];
25
  private $_default_active_modules = array();
26
  private $_always_active_modules = array();
27
+ private $inherited_modules = array();
28
  private $_active_modules = false;
29
  private $_active_modules_list = false;
30
+
31
+ /** @var ITSEC_Settings[] */
32
+ private $_module_settings = [];
33
+
34
+ /** @var ITSEC_Validator[] */
35
+ private $_module_validators = [];
36
+
37
  private $loaded_containers = [];
38
  private $labels = array();
39
  private $returned_files = array();
40
+ private $module_schema = array();
41
 
42
  /** @var Container */
43
  private $pimple;
49
  private $initialized_container = false;
50
 
51
  protected function __construct() {
52
+ add_action( 'itsec-lib-clear-caches', array( $this, 'reload_settings' ), 0 );
 
 
53
 
54
  $this->pimple = new Container();
55
  $this->container = new Pimple\Psr11\Container( $this->pimple );
89
  return false;
90
  }
91
 
92
+ if ( file_exists( $path . '/module.json' ) ) {
93
+ $json = file_get_contents( $path . '/module.json' );
94
+
95
+ if ( ! $json ) {
96
+ trigger_error( sprintf( __( 'An attempt to register the %1$s module failed because it\'s configuration file is empty.', 'better-wp-security' ), $slug ) );
97
+
98
+ return false;
99
+ }
100
+
101
+ $config = json_decode( $json, true );
102
+
103
+ if ( ITSEC_Core::is_development() && ( $valid = static::validate_module_config( $config ) ) !== true ) {
104
+ trigger_error( wp_sprintf( __( 'An attempt to register the %1$s module failed because it has an invalid configuration: %2$l', 'better-wp-security' ), $slug, ITSEC_Lib::get_error_strings( $valid ) ) );
105
+
106
+ return false;
107
+ }
108
+
109
+ $config = new Module_Config( $config );
110
+ $type = $config->get_status();
111
+
112
+ $self->module_config[ $slug ] = $config;
113
+
114
+ if ( $extends = $config->get_extends() ) {
115
+ $extends = self::get_config( $extends );
116
+
117
+ if ( ! $extends && ITSEC_Core::is_development() ) {
118
+ trigger_error( wp_sprintf( __( 'An attempt to register the %1$s module failed because it extends a non-registered module: %2$l', 'better-wp-security' ), $slug, $config->get_extends() ) );
119
+
120
+ return false;
121
+ }
122
+
123
+ $self->module_config[ $extends->get_id() ] = $extends->extend( $config );
124
+
125
+ if ( $config->get_status() === 'inherit' ) {
126
+ $self->inherited_modules[ $slug ] = true;
127
+ }
128
+ }
129
+ } else {
130
+ _doing_it_wrong( __METHOD__, sprintf( __( 'Registering a module without a module.json definition is deprecated. Module: %s.', 'better-wp-security' ), $slug ), '7.0.0' );
131
+ }
132
+
133
  $self->_module_paths[ $slug ] = $path;
134
  $self->_available_modules = array_keys( $self->_module_paths );
135
 
208
  self::load_module_file( 'settings.php', $slug );
209
  }
210
 
211
+ if ( ! isset( $self->_module_settings[ $slug ] ) && ( $config = self::get_module_config( $slug ) ) && $config->get_settings() ) {
212
+ $self->_module_settings[ $slug ] = new Config_Settings( $config );
213
+ }
214
+
215
  if ( ! isset( $self->_module_settings[ $slug ] ) ) {
216
  return null;
217
  }
229
  public static function get_defaults( $slug ) {
230
  $settings_obj = self::get_settings_obj( $slug );
231
 
232
+ if ( $settings_obj ) {
233
+ return $settings_obj->get_defaults();
234
  }
235
 
236
+ return array();
237
  }
238
 
239
  /**
246
  * @return mixed
247
  */
248
  public static function get_default( $slug, $name, $default = null ) {
249
+ $settings_obj = self::get_settings_obj( $slug );
250
 
251
+ if ( $settings_obj ) {
252
+ return $settings_obj->get_default( $name, $default );
253
  }
254
 
255
  return $default;
265
  public static function get_settings( $slug ) {
266
  $settings_obj = self::get_settings_obj( $slug );
267
 
268
+ if ( $settings_obj ) {
269
+ return $settings_obj->get_all();
270
  }
271
 
272
+ return array();
273
  }
274
 
275
  /**
285
  public static function get_setting( $slug, $name, $default = null ) {
286
  $settings_obj = self::get_settings_obj( $slug );
287
 
288
+ if ( $settings_obj ) {
289
+ return $settings_obj->get( $name, $default );
290
  }
291
 
292
+ return $default;
293
  }
294
 
295
  /**
305
  public static function set_settings( $slug, $settings ) {
306
  $settings_obj = self::get_settings_obj( $slug );
307
 
308
+ if ( ! $settings_obj ) {
309
  $error = new WP_Error( 'itsec-modules-invalid-settings-object', sprintf( __( 'Unable to find a valid settings object for %s. Settings were unable to be saved.', 'better-wp-security' ), $slug ) );
310
  ITSEC_Response::add_error( $error );
311
 
330
  public static function set_setting( $slug, $name, $value ) {
331
  $settings_obj = self::get_settings_obj( $slug );
332
 
333
+ if ( ! $settings_obj ) {
334
  trigger_error( sprintf( __( 'Unable to find a valid settings object for %s. Setting was unable to be saved.', 'better-wp-security' ), $slug ) );
335
 
336
  return false;
339
  return $settings_obj->set( $name, $value );
340
  }
341
 
342
+ /**
343
+ * Reloads settings values from the database.
344
+ */
345
+ public static function reload_settings() {
346
+ foreach ( self::get_instance()->_module_settings as $settings_obj ) {
347
+ $settings_obj->load();
348
+ }
349
+ }
350
+
351
  /**
352
  * Register a module's validator controller.
353
  *
376
  self::load_module_file( 'validator.php', $slug );
377
  }
378
 
379
+ if ( ! isset( $self->_module_validators[ $slug ] ) && ( $config = self::get_config( $slug ) ) && $config->get_settings() ) {
380
+ $self->_module_validators[ $slug ] = new Config_Validator( $config );
381
+ }
382
+
383
  if ( ! isset( $self->_module_validators[ $slug ] ) ) {
384
  return null;
385
  }
387
  return $self->_module_validators[ $slug ];
388
  }
389
 
390
+ /**
391
+ * Get's the config for a module.
392
+ *
393
+ * @param string $slug
394
+ *
395
+ * @return Module_Config|null
396
+ */
397
+ public static function get_config( $slug ) {
398
+ return isset( self::get_instance()->module_config[ $slug ] ) ? self::get_instance()->module_config[ $slug ] : null;
399
+ }
400
+
401
+ /**
402
+ * Gets a list of module config objects.
403
+ *
404
+ * @param string $modules The module specifier.
405
+ *
406
+ * @return Module_Config[]
407
+ */
408
+ public static function get_config_list( $modules = ':active' ): array {
409
+ return array_filter( array_map( [ static::class, 'get_config' ], self::transform_modules_specifier( $modules ) ) );
410
+ }
411
+
412
  /**
413
  * Retrieve the slugs of all modules available to the plugin.
414
  *
432
  return $self->_available_modules;
433
  }
434
 
435
+ /**
436
+ * Checks if the given module is available for use.
437
+ *
438
+ * @param string $module
439
+ *
440
+ * @return bool
441
+ */
442
+ public static function is_available( $module ) {
443
+ return isset( self::get_instance()->_module_paths[ $module ] );
444
+ }
445
+
446
  /**
447
  * Retrieve the slugs of all active modules.
448
  *
560
  * Activate a single module using its ID
561
  *
562
  * @param string $module_id The ID of the module to activate
563
+ * @param array $args Additional arguments to customize behavior.
564
+ * @type bool $ignore_requirements Whether to skip evaluating module requirements.
565
  *
566
  * @return bool|WP_Error If the module can be activated, true if it was previously active and false if it was
567
  * previously inactive. If the module cannot be activated, a WP_Error object is returned.
568
  */
569
+ public static function activate( $module_id, array $args = [] ) {
570
  $self = self::get_instance();
571
 
572
  if ( self::is_always_active( $module_id ) ) {
573
  return new WP_Error( 'itsec-modules-cannot-activate-always-active-module', sprintf( __( 'The %s module is a Core module and cannot be activated or deactivated.', 'better-wp-security' ), $module_id ) );
574
  }
575
 
576
+ if ( empty( $args['ignore_requirements'] ) ) {
577
+ $validated = self::validate_module_requirements( $module_id, 'activate' );
578
+
579
+ if ( $validated->has_errors() ) {
580
+ return new WP_Error(
581
+ 'itsec-modules-cannot-activate-module-unsatisfied-requirements',
582
+ __( 'Cannot activate module.', 'better-wp-security' ) . ' ' .
583
+ implode( ' ', ITSEC_Lib::get_error_strings( $validated ) ),
584
+ [
585
+ 'status' => WP_Http::BAD_REQUEST
586
+ ]
587
+ );
588
+ }
589
+ }
590
+
591
  if ( ! is_array( $self->_active_modules ) ) {
592
  self::get_active_modules();
593
  }
601
  try {
602
  self::load_module_file( 'activate.php', $module_id );
603
  } catch ( Unsatisfied_Module_Dependencies_Exception $e ) {
604
+ return new WP_Error( 'itsec-modules-cannot-activate-module-unsatisfied-dependencies', $e->getMessage(), [
605
+ 'status' => WP_Http::INTERNAL_SERVER_ERROR,
606
+ ] );
607
  }
608
 
609
  $self->_active_modules[ $module_id ] = true;
644
  $self->_active_modules[ $module_id ] = false;
645
  self::set_active_modules( $self->_active_modules );
646
 
647
+ if ( $config = self::get_config( $module_id ) ) {
648
+ ITSEC_Core::get_scheduler()->unregister_events_for_config( $config );
649
+ }
650
+
651
  return $was_active;
652
  }
653
 
702
  * @return bool|WP_Error True if a module matching the $modules parameter is found, false otherwise.
703
  */
704
  public static function load_module_file( $file, $modules = ':all', callable $process = null ) {
705
+ if ( isset( self::DEPRECATED[ $file ] ) ) {
706
+ _deprecated_file( $file, self::DEPRECATED[ $file ] );
707
+
708
+ return false;
709
+ }
710
+
711
  $self = self::get_instance();
712
 
713
+ if ( ! $modules = self::transform_modules_specifier( $modules ) ) {
 
 
 
 
 
 
714
  return false;
715
  }
716
 
719
  continue;
720
  }
721
 
722
+ $config = self::get_config( $module );
723
+
724
+ if ( $config && $config->is_deprecated() ) {
725
+ _deprecated_file( "{$self->_module_paths[ $module ]}/{$file}", $config->get_deprecated_version() );
726
+ }
727
+
728
  $self->load_container_definitions( $module );
729
  $returned = null;
730
 
746
  if ( array_key_exists( $path, $self->returned_files ) ) {
747
  $returned = $self->returned_files[ $path ];
748
  } else {
749
+ $returned = include_once( $path );
750
  $self->returned_files[ $path ] = $returned;
751
  }
752
  }
763
  return true;
764
  }
765
 
766
+ /**
767
+ * Gets the module's config definition.
768
+ *
769
+ * @param string $id The module id.
770
+ *
771
+ * @return Module_Config|null
772
+ */
773
+ public static function get_module_config( $id ) {
774
+ return isset( self::get_instance()->module_config[ $id ] ) ? self::get_instance()->module_config[ $id ] : null;
775
+ }
776
+
777
  /**
778
  * Get a list of the active modules to run.
779
  *
780
  * @return string[]
781
  */
782
+ public static function get_active_modules_to_run() {
783
  if ( ITSEC_Core::is_temp_disable_modules_set() ) {
784
  $modules = array();
785
  } else {
786
+ $modules = array_filter( self::get_active_modules(), function ( $module ) {
787
+ return ! self::validate_module_requirements( $module, 'run' )->has_errors();
788
+ } );
789
  }
790
 
791
  $modules = array_merge( $modules, array_keys( self::get_instance()->_always_active_modules ) );
792
  $modules = array_unique( $modules );
793
 
794
+ foreach ( self::get_instance()->inherited_modules as $slug => $_ ) {
795
+ if ( self::is_active( self::get_config( $slug )->get_extends() ) ) {
796
+ $modules[] = $slug;
797
+ }
798
+ }
799
+
800
  return $modules;
801
  }
802
 
891
  * This function can only be run once per-request.
892
  */
893
  public function load_settings_page() {
 
 
 
 
894
  self::load_module_file( 'settings-page.php' );
 
 
895
  }
896
 
897
  /**
902
  * @return array
903
  */
904
  public static function get_labels( $module ) {
905
+ $config = self::get_config( $module );
906
+
907
+ if ( $config ) {
908
+ return [
909
+ 'title' => $config->translate( Module_Config::T_ABOUT )->get_title(),
910
+ 'description' => $config->translate( Module_Config::T_ABOUT )->get_description(),
911
+ ];
912
+ }
913
+
914
  if ( ! isset( self::get_instance()->labels[ $module ] ) ) {
915
  self::get_instance()->labels[ $module ] = [];
916
  self::load_module_file( 'labels.php', $module, function ( $labels, $module ) {
923
  return self::get_instance()->labels[ $module ];
924
  }
925
 
926
+ /**
927
+ * Validates a module's requirements.
928
+ *
929
+ * @param string $module
930
+ * @param string $mode The mode to evaluate for. Either 'activate' or 'run'.
931
+ *
932
+ * @return WP_Error
933
+ */
934
+ public static function validate_module_requirements( string $module, string $mode ): WP_Error {
935
+ if ( defined( 'ITSEC_IGNORE_MODULE_REQUIREMENTS' ) && ITSEC_IGNORE_MODULE_REQUIREMENTS ) {
936
+ return new WP_Error();
937
+ }
938
+
939
+ $config = self::get_config( $module );
940
+
941
+ if ( ! $config || ! $config->get_requirements() ) {
942
+ return new WP_Error();
943
+ }
944
+
945
+ $requirements = $config->get_requirements();
946
+ $check = [];
947
+
948
+ if ( isset( $requirements['ssl'] ) && ( $mode === 'activate' || $requirements['ssl']['validate'] === $mode ) ) {
949
+ $check['ssl'] = true;
950
+ }
951
+
952
+ return ITSEC_Lib::evaluate_requirements( $check );
953
+ }
954
+
955
+ /**
956
+ * Validates a module's schema.
957
+ *
958
+ * @param array $config
959
+ *
960
+ * @return true|WP_Error
961
+ */
962
+ private static function validate_module_config( $config ) {
963
+ $self = self::get_instance();
964
+
965
+ if ( ! $self->module_schema ) {
966
+ $self->module_schema = ITSEC_Lib::resolve_schema_refs(
967
+ json_decode( file_get_contents( __DIR__ . '/module-schema.json' ), true )
968
+ );
969
+ }
970
+
971
+ return rest_validate_value_from_schema( $config, $self->module_schema, 'config' );
972
+ }
973
+
974
+ /**
975
+ * Checks if the given module has the requested file.
976
+ *
977
+ * @param string $module The module id.
978
+ * @param string $file The filename to check.
979
+ *
980
+ * @return bool
981
+ */
982
+ private static function module_has_file( $module, $file ) {
983
+ return file_exists( self::get_instance()->_module_paths[ $module ] . '/' . $file );
984
+ }
985
+
986
+ /**
987
+ * Transforms a module specifier to a list of modules.
988
+ *
989
+ * @param string|array $modules The modules specifier.
990
+ *
991
+ * @return string[]
992
+ */
993
+ private static function transform_modules_specifier( $modules ) {
994
+ if ( ':all' === $modules ) {
995
+ return self::get_available_modules();
996
+ }
997
+
998
+ if ( ':active' === $modules ) {
999
+ return self::get_active_modules_to_run();
1000
+ }
1001
+
1002
+ if ( is_string( $modules ) ) {
1003
+ return [ $modules ];
1004
+ }
1005
+
1006
+ if ( is_array( $modules ) ) {
1007
+ return $modules;
1008
+ }
1009
+
1010
+ return [];
1011
+ }
1012
+
1013
  private function run( $definition ) {
1014
  if ( $definition && is_string( $definition ) ) {
1015
  $object = $this->container->get( $definition );
core/modules/404-detection/active.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- require_once( 'class-itsec-four-oh-four.php' );
4
- $itsec_404_detection = new ITSEC_Four_Oh_Four();
5
- $itsec_404_detection->run();
 
 
 
 
 
core/modules/404-detection/class-itsec-four-oh-four.php DELETED
@@ -1,66 +0,0 @@
1
- <?php
2
-
3
- use iThemesSecurity\Lib\Lockout\Host_Context;
4
-
5
- class ITSEC_Four_Oh_Four {
6
-
7
- private $settings;
8
-
9
- function run() {
10
-
11
- $this->settings = ITSEC_Modules::get_settings( '404-detection' );
12
-
13
- add_filter( 'itsec_lockout_modules', array( $this, 'register_lockout' ) );
14
-
15
- add_action( 'template_redirect', array( $this, 'check_404' ), 9999 );
16
- }
17
-
18
- /**
19
- * If the page is a WordPress 404 error log it and register for lockout
20
- *
21
- * @return void
22
- */
23
- public function check_404() {
24
-
25
- /** @var ITSEC_Lockout $itsec_lockout */
26
- global $itsec_lockout;
27
-
28
- if ( ! is_404() ) {
29
- return;
30
- }
31
-
32
- $uri = explode( '?', $_SERVER['REQUEST_URI'] );
33
-
34
- if (
35
- ! in_array( '/' . ITSEC_Lib::get_request_path(), $this->settings['white_list'], true ) &&
36
- ! in_array( '.' . pathinfo( $uri[0], PATHINFO_EXTENSION ), $this->settings['types'], true )
37
- ) {
38
- ITSEC_Log::add_notice( 'four_oh_four', 'found_404', array( 'SERVER' => ITSEC_Lib::get_server_snapshot() ) );
39
- $itsec_lockout->do_lockout( new Host_Context( 'four_oh_four' ) );
40
- } else {
41
- do_action( 'itsec_four_oh_four_whitelisted', $uri );
42
- }
43
- }
44
-
45
- /**
46
- * Register 404 detection for lockout
47
- *
48
- * @param array $lockout_modules array of lockout modules
49
- *
50
- * @return array
51
- */
52
- public function register_lockout( $lockout_modules ) {
53
-
54
- $lockout_modules['four_oh_four'] = array(
55
- 'type' => 'four_oh_four',
56
- 'reason' => __( 'too many attempts to access a file that does not exist', 'better-wp-security' ),
57
- 'label' => __( '404', 'better-wp-security' ),
58
- 'host' => $this->settings['error_threshold'],
59
- 'period' => $this->settings['check_period']
60
- );
61
-
62
- return $lockout_modules;
63
-
64
- }
65
-
66
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/404-detection/js/admin-four-oh-four.js DELETED
@@ -1,22 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( "#itsec_four_oh_four_enabled" ).change(function () {
4
-
5
- if ( jQuery( "#itsec_four_oh_four_enabled" ).is( ':checked' ) ) {
6
-
7
- jQuery( "#four_oh_four-settings" ).show();
8
-
9
- } else {
10
-
11
- jQuery( "#four_oh_four-settings" ).hide();
12
-
13
- }
14
-
15
- } ).change();
16
-
17
- if ( jQuery( 'p.noPermalinks' ).length ) {
18
- jQuery( "#four_oh_four-settings" ).hide();
19
- }
20
-
21
- } );
22
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/404-detection/labels.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- return [
4
- 'title' => __( '404 Detection', 'better-wp-security' ),
5
- ];
 
 
 
 
 
core/modules/404-detection/logs.php DELETED
@@ -1,33 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Four_Oh_Four_Logs {
4
- public function __construct() {
5
- add_filter( 'itsec_logs_prepare_four_oh_four_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
- add_filter( 'itsec_logs_prepare_four_oh_four_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
- add_filter( 'itsec_logs_prepare_four_oh_four_filter_row_action_for_code', array( $this, 'code_row_action' ), 10, 2 );
8
- }
9
-
10
- public function filter_entry_for_list_display( $entry ) {
11
- $entry['module_display'] = esc_html__( '404 Detection', 'better-wp-security' );
12
-
13
- if ( 'found_404' === $entry['code'] ) {
14
- $entry['description'] = esc_html( $entry['url'] );
15
- }
16
-
17
- return $entry;
18
- }
19
-
20
- public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
21
- $entry = $this->filter_entry_for_list_display( $entry, $code, $code_data );
22
-
23
- $details['module']['content'] = $entry['module_display'];
24
- $details['description']['content'] = $entry['description'];
25
-
26
- return $details;
27
- }
28
-
29
- public function code_row_action( $vars, $entry ) {
30
- return array( 'filters[10]' => "url|{$entry['url']}", 'filters[11]' => 'module|four_oh_four' );
31
- }
32
- }
33
- new ITSEC_Four_Oh_Four_Logs();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/settings-page.php DELETED
@@ -1,64 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_404_Detection_Settings_Page extends ITSEC_Module_Settings_Page {
4
- public function __construct() {
5
- $this->id = '404-detection';
6
- $this->title = __( '404 Detection', 'better-wp-security' );
7
- $this->description = __( 'Automatically block users snooping around for pages to exploit.', 'better-wp-security' );
8
- $this->type = 'recommended';
9
-
10
- parent::__construct();
11
- }
12
-
13
- protected function render_description( $form ) {
14
-
15
- ?>
16
- <p><?php _e( '404 detection looks at a user who is hitting a large number of non-existent pages and getting a large number of 404 errors. 404 detection assumes that a user who hits a lot of 404 errors in a short period of time is scanning for something (presumably a vulnerability) and locks them out accordingly. This also gives the added benefit of helping you find hidden problems causing 404 errors on unseen parts of your site. All errors will be logged in the "View Logs" page. You can set thresholds for this feature below.', 'better-wp-security' ); ?></p>
17
- <?php
18
-
19
- }
20
-
21
- protected function render_settings( $form ) {
22
-
23
- /** @var ITSEC_Lockout $itsec_lockout */
24
- global $itsec_lockout;
25
- ?>
26
- <?php echo $itsec_lockout->get_lockout_description(); ?>
27
- <table class="form-table">
28
- <tr>
29
- <th scope="row"><label for="itsec-404-detection-check_period"><?php _e( 'Minutes to Remember 404 Error (Check Period)', 'better-wp-security' ); ?></label></th>
30
- <td>
31
- <?php $form->add_text( 'check_period', array( 'class' => 'small-text' ) ); ?>
32
- <label for="itsec-404-detection-check_period"><?php _e( 'Minutes', 'better-wp-security' ); ?></label>
33
- <p class="description"><?php _e( 'The number of minutes in which 404 errors should be remembered and counted towards lockouts.', 'better-wp-security' ); ?></p>
34
- </td>
35
- </tr>
36
- <tr>
37
- <th scope="row"><label for="itsec-404-detection-error_threshold"><?php _e( 'Error Threshold', 'better-wp-security' ); ?></label></th>
38
- <td>
39
- <?php $form->add_text( 'error_threshold', array( 'class' => 'small-text' ) ); ?>
40
- <label for="itsec-404-detection-error_threshold"><?php _e( 'Errors', 'better-wp-security' ); ?></label>
41
- <p class="description"><?php _e( 'The numbers of errors (within the check period time frame) that will trigger a lockout. Set to zero (0) to record 404 errors without locking out users. This can be useful for troubleshooting content or other errors. The default is 20.', 'better-wp-security' ); ?></p>
42
- </td>
43
- </tr>
44
- <tr>
45
- <th scope="row"><label for="itsec-404-detection-white_list"><?php _e( '404 File/Folder Ignore List', 'better-wp-security' ); ?></label></th>
46
- <td>
47
- <?php $form->add_textarea( 'white_list', array( 'wrap' => 'off' ) ); ?>
48
- <p class="description"><?php _e( 'Use the list above to prevent recording common 404 errors. If you know a common file on your site is missing and you do not want it to count towards a lockout record it here. You must list the full path beginning with the "/".', 'better-wp-security' ); ?></p>
49
- </td>
50
- </tr>
51
- <tr>
52
- <th scope="row"><label for="itsec-404-detection-types"><?php _e( 'Ignored File Types', 'better-wp-security' ); ?></label></th>
53
- <td>
54
- <?php $form->add_textarea( 'types', array( 'wrap' => 'off' ) ); ?>
55
- <p class="description"><?php _e( 'File types listed here will be recorded as 404 errors but will not lead to lockouts.', 'better-wp-security' ); ?></p>
56
- </td>
57
- </tr>
58
- </table>
59
- <?php
60
-
61
- }
62
- }
63
-
64
- new ITSEC_404_Detection_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/settings.php DELETED
@@ -1,34 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Four_Oh_Four_Settings extends ITSEC_Settings {
4
- public function get_id() {
5
- return '404-detection';
6
- }
7
-
8
- public function get_defaults() {
9
- return array(
10
- 'check_period' => 5,
11
- 'error_threshold' => 20,
12
- 'white_list' => array(
13
- '/favicon.ico',
14
- '/robots.txt',
15
- '/apple-touch-icon.png',
16
- '/apple-touch-icon-precomposed.png',
17
- '/wp-content/cache',
18
- '/browserconfig.xml',
19
- '/crossdomain.xml',
20
- '/labels.rdf',
21
- '/trafficbasedsspsitemap.xml',
22
- ),
23
- 'types' => array(
24
- '.jpg',
25
- '.jpeg',
26
- '.png',
27
- '.gif',
28
- '.css',
29
- ),
30
- );
31
- }
32
- }
33
-
34
- ITSEC_Modules::register_settings( new ITSEC_Four_Oh_Four_Settings() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/setup.php DELETED
@@ -1,104 +0,0 @@
1
- <?php
2
-
3
- if ( ! class_exists( 'ITSEC_Four_Oh_Four_Setup' ) ) {
4
-
5
- class ITSEC_Four_Oh_Four_Setup {
6
-
7
- private
8
- $defaults;
9
-
10
- public function __construct() {
11
-
12
- add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
13
- add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
14
- add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
15
- add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
16
-
17
- }
18
-
19
- /**
20
- * Execute module activation.
21
- *
22
- * @since 4.0
23
- *
24
- * @return void
25
- */
26
- public function execute_activate() {
27
- }
28
-
29
- /**
30
- * Execute module deactivation
31
- *
32
- * @return void
33
- */
34
- public function execute_deactivate() {
35
- }
36
-
37
- /**
38
- * Execute module uninstall
39
- *
40
- * @return void
41
- */
42
- public function execute_uninstall() {
43
-
44
- $this->execute_deactivate();
45
-
46
- delete_site_option( 'itsec_four_oh_four' );
47
-
48
- }
49
-
50
- /**
51
- * Execute module upgrade
52
- *
53
- * @return void
54
- */
55
- public function execute_upgrade( $itsec_old_version ) {
56
-
57
- if ( $itsec_old_version < 4000 ) {
58
-
59
- global $itsec_bwps_options;
60
-
61
- $current_options = get_site_option( 'itsec_four_oh_four' );
62
-
63
- // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
64
- if ( false !== $current_options ) {
65
-
66
- $current_options['enabled'] = isset( $itsec_bwps_options['id_enabled'] ) && $itsec_bwps_options['id_enabled'] == 1 ? true : false;
67
- $current_options['check_period'] = isset( $itsec_bwps_options['id_checkinterval'] ) ? intval( $itsec_bwps_options['id_checkinterval'] ) : 5;
68
- $current_options['error_threshold'] = isset( $itsec_bwps_options['id_threshold'] ) ? intval( $itsec_bwps_options['id_threshold'] ) : 20;
69
-
70
- if ( isset( $itsec_bwps_options['id_whitelist'] ) && ! is_array( $itsec_bwps_options['id_whitelist'] ) && strlen( $itsec_bwps_options['id_whitelist'] ) > 1 ) {
71
-
72
- $current_options['white_list'] .= explode( PHP_EOL, $itsec_bwps_options['id_whitelist'] );
73
-
74
- }
75
-
76
- update_site_option( 'itsec_four_oh_four', $current_options );
77
- }
78
- }
79
-
80
- if ( $itsec_old_version < 4041 ) {
81
- $current_options = get_site_option( 'itsec_four_oh_four' );
82
-
83
- // If there are no current options, go with the new defaults by not saving anything
84
- if ( is_array( $current_options ) ) {
85
- // Make sure the new module is properly activated or deactivated
86
- if ( $current_options['enabled'] ) {
87
- ITSEC_Modules::activate( '404-detection' );
88
- } else {
89
- ITSEC_Modules::deactivate( '404-detection' );
90
- }
91
-
92
- // remove 'enabled' which isn't use in the new module
93
- unset( $current_options['enabled'] );
94
- ITSEC_Modules::set_settings( '404-detection', $current_options );
95
- }
96
- }
97
-
98
- }
99
-
100
- }
101
-
102
- }
103
-
104
- new ITSEC_Four_Oh_Four_Setup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/404-detection/validator.php DELETED
@@ -1,33 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Four_Oh_Four_Validator extends ITSEC_Validator {
4
- public function get_id() {
5
- return '404-detection';
6
- }
7
-
8
- protected function sanitize_settings() {
9
- $this->sanitize_setting( 'positive-int', 'check_period', __( 'Minutes to Remember 404 Error (Check Period)', 'better-wp-security' ) );
10
- $this->sanitize_setting( 'positive-int', 'error_threshold', __( 'Error Threshold', 'better-wp-security' ) );
11
-
12
- $this->sanitize_setting( array( $this, 'sanitize_white_list_entry' ), 'white_list', __( '404 File/Folder Ignore List', 'better-wp-security' ) );
13
- $this->sanitize_setting( array( $this, 'sanitize_types_entry' ), 'types', __( '404 File/Folder Ignore List', 'better-wp-security' ) );
14
- }
15
-
16
- protected function sanitize_white_list_entry( $entry ) {
17
- if ( '/' !== substr( $entry, 0, 1 ) ) {
18
- return false;
19
- }
20
-
21
- return $entry;
22
- }
23
-
24
- protected function sanitize_types_entry( $entry ) {
25
- if ( '.' !== substr( $entry, 0, 1 ) ) {
26
- return false;
27
- }
28
-
29
- return $entry;
30
- }
31
- }
32
-
33
- ITSEC_Modules::register_validator( new ITSEC_Four_Oh_Four_Validator() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/admin-user/active.php CHANGED
@@ -1,102 +1,141 @@
1
  <?php
2
 
3
- /**
4
- * Changes Admin User.
5
- *
6
- * Changes the username and id of the 1st user
7
- *
8
- * @param string $username The username to change.
9
- * @param bool $id Whether to change the id.
10
- *
11
- * @return bool
12
- */
13
- function itsec_change_admin_user( $username = null, $id = false ) {
14
- global $wpdb;
15
 
16
- if ( ! ITSEC_Lib::get_lock( 'admin_user', 180 ) ) {
17
- return false;
18
- }
 
19
 
20
- //sanitize the username
21
- $new_user = sanitize_text_field( $username );
 
 
 
 
22
 
23
- //Get the full user object
24
- $user_object = get_user_by( 'id', '1' );
 
 
 
 
25
 
26
- if ( ! is_null( $username ) && validate_username( $new_user ) && false === username_exists( $new_user ) ) { //there is a valid username to change
27
- if ( $id === true ) { //we're changing the id too so we'll set the username
28
- $user_login = $new_user;
29
- } else { // we're only changing the username
 
 
30
 
31
- //query main user table
32
- $wpdb->query( $wpdb->prepare( "UPDATE `{$wpdb->users}` SET user_login = %s WHERE user_login = %s", $new_user, 'admin' ) );
33
 
34
- if ( is_multisite() ) { //process sitemeta if we're in a multi-site situation
 
35
 
 
 
36
  $old_admins = $wpdb->get_var( "SELECT meta_value FROM `" . $wpdb->sitemeta . "` WHERE meta_key = 'site_admins'" );
37
  // No need to escape the new username. It is already safe via validate_userame() which will check for quotes
38
- $new_admins = str_replace( '5:"admin"', strlen( $new_user ) . ':"' . $new_user . '"', $old_admins );
39
  $wpdb->query( $wpdb->prepare( "UPDATE `{$wpdb->sitemeta}` SET meta_value = %s WHERE meta_key = 'site_admins'", $new_admins ) );
40
-
41
  }
42
 
 
43
  ITSEC_Lib::release_lock( 'admin_user' );
44
 
45
- return true;
 
46
  }
47
- } elseif ( $username !== null ) { //username didn't validate
48
- ITSEC_Lib::release_lock( 'admin_user' );
49
 
50
- return false;
51
- } else { //only changing the id
52
- $user_login = $user_object->user_login;
53
- }
54
 
55
- if ( $id === true ) { //change the user id
 
56
 
57
- $wpdb->query( "DELETE FROM `" . $wpdb->users . "` WHERE ID = 1;" );
 
58
 
59
- $wpdb->insert( $wpdb->users, array(
60
- 'user_login' => $user_login,
61
- 'user_pass' => $user_object->user_pass,
62
- 'user_nicename' => $user_object->user_nicename,
63
- 'user_email' => $user_object->user_email,
64
- 'user_url' => $user_object->user_url,
65
- 'user_registered' => $user_object->user_registered,
66
- 'user_activation_key' => $user_object->user_activation_key,
67
- 'user_status' => $user_object->user_status,
68
- 'display_name' => $user_object->display_name
69
- ) );
70
 
71
- if ( is_multisite() && $username !== null && validate_username( $new_user ) ) { //process sitemeta if we're in a multi-site situation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- $old_admins = $wpdb->get_var( "SELECT meta_value FROM `{$wpdb->sitemeta}` WHERE meta_key = 'site_admins'" );
74
- // No need to escape the new username. It is already safe via validate_userame() which will check for quotes
75
- $new_admins = str_replace( '5:"admin"', strlen( $new_user ) . ':"' . $new_user . '"', $old_admins );
76
- $wpdb->query( $wpdb->prepare( "UPDATE `{$wpdb->sitemeta}` SET meta_value = %s WHERE meta_key = 'site_admins'", $new_admins ) );
77
 
 
78
  }
 
 
79
 
80
- $new_user = $wpdb->insert_id;
81
-
82
- $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_author = %d WHERE post_author = 1", $new_user ) );
83
- $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->usermeta} SET user_id = %d WHERE user_id = 1", $new_user ) );
84
- $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->comments} SET user_id = %d WHERE user_id = 1", $new_user ) );
85
- $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->links} SET link_owner = %d WHERE link_owner = 1", $new_user ) );
 
 
 
 
 
 
86
 
87
- /**
88
- * Fires when the admin user with id of #1 has been changed.
89
- *
90
- * @since 6.3.0
91
- *
92
- * @param int $new_user The new user's ID.
93
- */
94
- do_action( 'itsec_change_admin_user_id', $new_user );
95
 
96
- ITSEC_Lib::release_lock( 'admin_user' );
 
 
97
 
98
- return true;
 
99
  }
100
 
101
- return false;
102
  }
1
  <?php
2
 
3
+ use iThemesSecurity\Lib\Tools\Config_Tool;
4
+ use iThemesSecurity\Lib\Tools\Tools_Registry;
5
+ use \iThemesSecurity\Lib\Result;
6
+ use iThemesSecurity\Lib\Tools\Tools_Runner;
7
+
8
+ add_action( 'itsec_register_tools', function ( Tools_Registry $registry ) {
9
+ $registry->register( new class( 'change-admin-user', ITSEC_Modules::get_config( 'admin-user' ) ) extends Config_Tool {
10
+ public function is_available(): bool {
11
+ return (bool) username_exists( 'admin' );
12
+ }
 
 
13
 
14
+ public function run( array $form = [] ): Result {
15
+ global $wpdb;
16
+
17
+ $username = $form['new_username'];
18
 
19
+ if ( ! validate_username( $username ) ) {
20
+ return Result::error( new WP_Error(
21
+ 'itsec.tool.change-admin-user.validate',
22
+ __( 'Invalid username.', 'better-wp-security' )
23
+ ) );
24
+ }
25
 
26
+ if ( username_exists( $username ) ) {
27
+ return Result::error( new WP_Error(
28
+ 'itsec.tool.change-admin-user.duplicate',
29
+ __( 'A user already exists with that username.', 'better-wp-security' )
30
+ ) );
31
+ }
32
 
33
+ if ( ! ITSEC_Lib::get_lock( 'admin_user', 180 ) ) {
34
+ return Result::error( new WP_Error(
35
+ 'itsec.tool.change-admin-user.lock',
36
+ __( 'This tool is already running. Please try again in a few minutes.', 'better-wp-security' )
37
+ ) );
38
+ }
39
 
40
+ $user_id = username_exists( 'admin' );
 
41
 
42
+ // Query main user table
43
+ $wpdb->query( $wpdb->prepare( "UPDATE `{$wpdb->users}` SET user_login = %s WHERE user_login = %s", $username, 'admin' ) );
44
 
45
+ // Process sitemeta if we're in a multi-site situation
46
+ if ( is_multisite() ) {
47
  $old_admins = $wpdb->get_var( "SELECT meta_value FROM `" . $wpdb->sitemeta . "` WHERE meta_key = 'site_admins'" );
48
  // No need to escape the new username. It is already safe via validate_userame() which will check for quotes
49
+ $new_admins = str_replace( '5:"admin"', strlen( $username ) . ':"' . $username . '"', $old_admins );
50
  $wpdb->query( $wpdb->prepare( "UPDATE `{$wpdb->sitemeta}` SET meta_value = %s WHERE meta_key = 'site_admins'", $new_admins ) );
 
51
  }
52
 
53
+ clean_user_cache( $user_id );
54
  ITSEC_Lib::release_lock( 'admin_user' );
55
 
56
+ return Result::success()
57
+ ->add_success_message( sprintf( __( 'Updated “admin” username to “%s”', 'better-wp-security' ), $username ) );
58
  }
59
+ } );
 
60
 
61
+ $registry->register( new class( 'change-id-1-user', ITSEC_Modules::get_config( 'admin-user' ) ) extends Config_Tool {
62
+ public function is_available(): bool {
63
+ $user = get_userdata( 1 );
 
64
 
65
+ return $user instanceof WP_User && $user->exists();
66
+ }
67
 
68
+ public function run( array $form = [] ): Result {
69
+ global $wpdb;
70
 
71
+ if ( ! ITSEC_Lib::get_lock( 'admin_user', 180 ) ) {
72
+ return Result::error( new WP_Error(
73
+ 'itsec.tool.change-id-1-user.lock',
74
+ __( 'This tool is already running. Please try again in a few minutes.', 'better-wp-security' )
75
+ ) );
76
+ }
 
 
 
 
 
77
 
78
+ $user = get_userdata( 1 );
79
+
80
+ $wpdb->query( "DELETE FROM `{$wpdb->users}` WHERE ID = 1;" );
81
+ $wpdb->insert( $wpdb->users, array(
82
+ 'user_login' => $user->user_login,
83
+ 'user_pass' => $user->user_pass,
84
+ 'user_nicename' => $user->user_nicename,
85
+ 'user_email' => $user->user_email,
86
+ 'user_url' => $user->user_url,
87
+ 'user_registered' => $user->user_registered,
88
+ 'user_activation_key' => $user->user_activation_key,
89
+ 'user_status' => $user->user_status,
90
+ 'display_name' => $user->display_name
91
+ ) );
92
+
93
+ $new_user = $wpdb->insert_id;
94
+
95
+ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_author = %d WHERE post_author = 1", $new_user ) );
96
+ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->usermeta} SET user_id = %d WHERE user_id = 1", $new_user ) );
97
+ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->comments} SET user_id = %d WHERE user_id = 1", $new_user ) );
98
+ $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->links} SET link_owner = %d WHERE link_owner = 1", $new_user ) );
99
+
100
+ /**
101
+ * Fires when the admin user with id of #1 has been changed.
102
+ *
103
+ * @since 6.3.0
104
+ *
105
+ * @param int $new_user The new user's ID.
106
+ */
107
+ do_action( 'itsec_change_admin_user_id', $new_user );
108
 
109
+ ITSEC_Lib::release_lock( 'admin_user' );
 
 
 
110
 
111
+ return Result::success()->add_success_message( __( 'Updated user ID.', 'better-wp-security' ) );
112
  }
113
+ } );
114
+ } );
115
 
116
+ /**
117
+ * Changes Admin User.
118
+ *
119
+ * Changes the username and id of the 1st user
120
+ *
121
+ * @param string $username The username to change.
122
+ * @param bool $id Whether to change the id.
123
+ *
124
+ * @return bool
125
+ */
126
+ function itsec_change_admin_user( $username = null, $id = false ) {
127
+ _deprecated_function( __METHOD__, '7.0.0', Tools_Runner::class );
128
 
129
+ $runner = ITSEC_Modules::get_container()->get( Tools_Runner::class );
130
+ $registry = ITSEC_Modules::get_container()->get( Tools_Registry::class );
 
 
 
 
 
 
131
 
132
+ if ( $username && ! $runner->run_tool( $registry->get_tool( 'change-admin-user' ), [ 'new_username' => $username ] )->is_success() ) {
133
+ return false;
134
+ }
135
 
136
+ if ( $id && ! $runner->run_tool( $registry->get_tool( 'change-id-1-user' ) )->is_success() ) {
137
+ return false;
138
  }
139
 
140
+ return true;
141
  }
core/modules/admin-user/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/modules/admin-user/js/admin-admin-user.js DELETED
@@ -1,15 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( "#itsec_enable_admin_user" ).change(function () {
4
-
5
- if ( jQuery( "#itsec_enable_admin_user" ).is( ':checked' ) ) {
6
- jQuery( "#admin_user_username_field, #admin_user_id_field" ).show();
7
-
8
- } else {
9
- jQuery( "#admin_user_username_field, #admin_user_id_field" ).hide();
10
-
11
- }
12
-
13
- } ).change();
14
-
15
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/admin-user/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/admin-user/module.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "admin-user",
3
+ "status": "always-active",
4
+ "type": "tool",
5
+ "title": "Admin User",
6
+ "description": "An advanced tool that removes users with a username of “admin” or a user ID of “1”.",
7
+ "tools": {
8
+ "change-admin-user": {
9
+ "title": "Change Admin User",
10
+ "description": "Changes the username of the “admin” user.",
11
+ "help": "Run this tool to change the username of a user with the “admin” username. This may prevent unsophisticated attacks that assume the “admin” user exists.",
12
+ "form": {
13
+ "type": "object",
14
+ "required": [
15
+ "new_username"
16
+ ],
17
+ "properties": {
18
+ "new_username": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "pattern": "[\\w\\s\\-\\.\\@]+",
22
+ "default": "",
23
+ "title": "New Username",
24
+ "description": "Enter the new username for the “admin” user."
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "change-id-1-user": {
30
+ "title": "Change User ID 1",
31
+ "description": "Changes the user ID for the first WordPress user.",
32
+ "help": "Run this tool to change the user ID of a user with a user ID of “1”. This may prevent unsophisticated attacks that assume the user with an ID of “1” is an administrator."
33
+ }
34
+ }
35
+ }
core/modules/admin-user/settings-page.php DELETED
@@ -1,57 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Admin_User_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $version = 1;
5
-
6
-
7
- public function __construct() {
8
- $this->id = 'admin-user';
9
- $this->title = __( 'Admin User', 'better-wp-security' );
10
- $this->description = __( 'An advanced tool that removes users with a username of "admin" or a user ID of "1".', 'better-wp-security' );
11
- $this->type = 'advanced';
12
-
13
- parent::__construct();
14
- }
15
-
16
- protected function render_description( $form ) {
17
-
18
- ?>
19
- <p><?php _e( 'This feature will improve the security of your WordPress installation by removing common user attributes that can be used to target your site.', 'better-wp-security' ); ?></p>
20
- <?php
21
-
22
- }
23
-
24
- protected function render_settings( $form ) {
25
-
26
- ?>
27
- <div class="itsec-warning-message"><?php printf( __( '<span>Warning:</span> The changes made by this tool could cause compatibility issues with some plugins, themes, or customizations. Ensure that you <a href="%s">create a database backup</a> before using this tool.', 'better-wp-security' ), esc_url( ITSEC_Core::get_backup_creation_page_url() ) ); ?></div>
28
-
29
- <table class="form-table itsec-settings-section">
30
- <?php if ( username_exists( 'admin' ) ) : ?>
31
- <tr>
32
- <th scope="row"><label for="itsec-admin-user-new_username"><?php _e( 'New Admin Username', 'better-wp-security' ); ?></label></th>
33
- <td>
34
- <?php $form->add_text( 'new_username', array( 'class' => 'code' ) ); ?>
35
- <br />
36
- <p class="description"><?php _e( 'Enter a new username to replace "admin." Please note that if you are logged in as admin you will have to log in again.', 'better-wp-security' ); ?></p>
37
- </td>
38
- </tr>
39
- <?php endif; ?>
40
- <?php if ( ITSEC_Lib::user_id_exists( 1 ) ) { ?>
41
- <tr>
42
- <th scope="row"><label for="itsec-admin-user-change_id"><?php _e( 'Change User ID 1', 'better-wp-security' ); ?></label></th>
43
- <td>
44
- <?php $form->add_checkbox( 'change_id' ); ?>
45
- <label for="itsec-admin-user-change_id"><?php _e( 'Change the ID of the user with ID 1.', 'better-wp-security' ); ?></label>
46
- </td>
47
- </tr>
48
- <?php } ?>
49
- </table>
50
- <?php
51
-
52
- }
53
- }
54
-
55
- if ( username_exists( 'admin' ) || ITSEC_Lib::user_id_exists( 1 ) ) {
56
- new ITSEC_Admin_User_Settings_Page();
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/admin-user/settings.php DELETED
@@ -1,13 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Admin_User_Settings extends ITSEC_Settings {
4
- public function get_id() {
5
- return 'admin-user';
6
- }
7
-
8
- public function get_defaults() {
9
- return array();
10
- }
11
- }
12
-
13
- ITSEC_Modules::register_settings( new ITSEC_Admin_User_Settings() );
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/admin-user/validator.php DELETED
@@ -1,53 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Admin_User_Validator extends ITSEC_Validator {
4
- protected $run_validate_matching_fields = false;
5
- protected $run_validate_matching_types = false;
6
-
7
- public function get_id() {
8
- return 'admin-user';
9
- }
10
-
11
- protected function sanitize_settings() {
12
- // Only validate it if it exists
13
- if ( ! empty( $this->settings['new_username'] ) ) {
14
- $this->sanitize_setting( 'valid-username', 'new_username', __( 'New Admin Username', 'better-wp-security' ) );
15
- }
16
-
17
- // If the value wasn't sent for this, assume false (no change)
18
- if ( empty( $this->settings['change_id'] ) ) {
19
- $this->settings['change_id'] = false;
20
- } else {
21
- $this->sanitize_setting( 'bool', 'change_id', __( 'Change User ID 1', 'better-wp-security' ) );
22
- }
23
- }
24
-
25
- protected function validate_settings() {
26
- if ( ! $this->can_save() ) {
27
- return;
28
- }
29
-
30
- if ( empty( $this->settings['new_username'] ) || 'admin' === $this->settings['new_username'] ) {
31
- $this->settings['new_username'] = null;
32
- }
33
-
34
- if ( is_null( $this->settings['new_username'] ) && false === $this->settings['change_id'] ) {
35
- return;
36
- }
37
-
38
- $result = itsec_change_admin_user( $this->settings['new_username'], $this->settings['change_id'] );
39
-
40
- if ( $result ) {
41
- $this->add_message( __( 'The user was successfully updated.', 'better-wp-security' ) );
42
- ITSEC_Response::set_show_default_success_message( false );
43
-
44
- ITSEC_Response::force_logout();
45
- } else {
46
- $this->set_can_save( false );
47
- $this->add_error( new WP_Error( 'itsec-admin-user-failed-change-admin-user', __( 'The user was unable to be successfully updated. This could be due to a plugin or server configuration conflict.', 'better-wp-security' ) ) );
48
- ITSEC_Response::set_show_default_error_message( false );
49
- }
50
- }
51
- }
52
-
53
- ITSEC_Modules::register_validator( new ITSEC_Admin_User_Validator() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/activate.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- require_once( dirname( __FILE__ ) . '/utilities.php' );
4
-
5
- ITSEC_Away_Mode_Utilities::create_active_file();
 
 
 
 
 
core/modules/away-mode/active.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- require_once( 'class-itsec-away-mode.php' );
4
- $itsec_away_mode = new ITSEC_Away_Mode();
5
- $itsec_away_mode->run();
 
 
 
 
 
core/modules/away-mode/class-itsec-away-mode.php DELETED
@@ -1,139 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Away_Mode {
4
-
5
- public function run() {
6
-
7
- //Execute away mode functions on admin init
8
- add_action( 'admin_init', array( $this, 'run_active_check' ) );
9
- add_action( 'login_init', array( $this, 'run_active_check' ) );
10
-
11
- add_filter( 'itsec_managed_files', array( $this, 'register_managed_file' ) );
12
-
13
- add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
14
- add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
15
- }
16
-
17
- /**
18
- * Check if away mode is active
19
- *
20
- * @since 4.4
21
- * @static
22
- *
23
- * @param bool $get_details Optional, defaults to false. True to receive details rather than a boolean response.
24
- *
25
- * @return mixed If $get_details is true, an array of status details. Otherwise, true if away and false otherwise.
26
- */
27
- public static function is_active( $get_details = false ) {
28
- require_once( dirname( __FILE__ ) . '/utilities.php' );
29
-
30
- $settings = ITSEC_Modules::get_settings( 'away-mode' );
31
-
32
- if ( 'daily' === $settings['type'] ) {
33
- $details = ITSEC_Away_Mode_Utilities::is_current_time_active( $settings['start_time'], $settings['end_time'], true );
34
- } else {
35
- $details = ITSEC_Away_Mode_Utilities::is_current_timestamp_active( $settings['start'], $settings['end'], true );
36
- }
37
-
38
- $details['has_active_file'] = ITSEC_Away_Mode_Utilities::has_active_file();
39
- $details['override_type'] = $settings['override_type'];
40
- $details['override_end'] = $settings['override_end'];
41
-
42
- if ( empty( $settings['override_type'] ) || ( ITSEC_Core::get_current_time() > $settings['override_end'] ) ) {
43
- $details['override_active'] = false;
44
- } else {
45
- $details['override_active'] = true;
46
-
47
- if ( 'activate' === $details['override_type'] ) {
48
- $details['active'] = true;
49
- } else {
50
- $details['active'] = false;
51
- }
52
- }
53
-
54
- // If the active file does not exist, completely disable the away mode feature to allow an administrator
55
- // to regain access to their site.
56
- if ( ! $details['has_active_file'] ) {
57
- $details['active'] = false;
58
- $details['remaining'] = false;
59
- $details['next'] = false;
60
- $details['length'] = false;
61
- }
62
-
63
- if ( ! isset( $details['error'] ) ) {
64
- $details['error'] = false;
65
- }
66
-
67
-
68
- if ( $get_details ) {
69
- return $details;
70
- }
71
-
72
- return $details['active'];
73
- }
74
-
75
- /**
76
- * Execute away mode functionality
77
- *
78
- * @return void
79
- */
80
- public function run_active_check() {
81
-
82
- if ( wp_doing_ajax() ) {
83
- return;
84
- }
85
-
86
- $away_mode_details = self::is_active( true );
87
-
88
- if ( $away_mode_details['active'] ) {
89
- ITSEC_Log::add_notice( 'away_mode', 'away-mode-active', array( 'login_details' => ITSEC_Lib::get_login_details(), 'away_mode_details' => $away_mode_details ) );
90
-
91
- wp_redirect( get_option( 'siteurl' ) );
92
- wp_clear_auth_cookie();
93
- die();
94
- }
95
- }
96
-
97
- /**
98
- * Register the away mode file as a managed file.
99
- *
100
- * @param array $files
101
- *
102
- * @return array
103
- */
104
- public function register_managed_file( $files ) {
105
-
106
- require_once( dirname( __FILE__ ) . '/utilities.php' );
107
-
108
- $files[] = ITSEC_Away_Mode_Utilities::get_active_file_name();
109
-
110
- return $files;
111
- }
112
-
113
- /**
114
- * Register verbs for Sync.
115
- *
116
- * @since 3.6.0
117
- *
118
- * @param Ithemes_Sync_API $api API object.
119
- */
120
- public function register_sync_verbs( $api ) {
121
- $api->register( 'itsec-get-away-mode', 'Ithemes_Sync_Verb_ITSEC_Get_Away_Mode', dirname( __FILE__ ) . '/sync-verbs/itsec-get-away-mode.php' );
122
- $api->register( 'itsec-override-away-mode', 'Ithemes_Sync_Verb_ITSEC_Override_Away_Mode', dirname( __FILE__ ) . '/sync-verbs/itsec-override-away-mode.php' );
123
- }
124
-
125
- /**
126
- * Filter to add verbs to the response for the itsec-get-everything verb.
127
- *
128
- * @since 3.6.0
129
- *
130
- * @param array Array of verbs.
131
- *
132
- * @return array Array of verbs.
133
- */
134
- public function register_sync_get_everything_verbs( $verbs ) {
135
- $verbs['away_mode'][] = 'itsec-get-away-mode';
136
-
137
- return $verbs;
138
- }
139
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/css/images/ui-bg_glass_55_fbf9ee_1x400.png DELETED
Binary file
core/modules/away-mode/css/images/ui-bg_glass_65_ffffff_1x400.png DELETED
Binary file
core/modules/away-mode/css/images/ui-bg_glass_75_dadada_1x400.png DELETED
Binary file
core/modules/away-mode/css/images/ui-bg_glass_75_e6e6e6_1x400.png DELETED
Binary file
core/modules/away-mode/css/images/ui-bg_glass_95_fef1ec_1x400.png DELETED
Binary file
core/modules/away-mode/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png DELETED
Binary file
core/modules/away-mode/css/images/ui-icons_222222_256x240.png DELETED
Binary file
core/modules/away-mode/css/images/ui-icons_2e83ff_256x240.png DELETED
Binary file
core/modules/away-mode/css/images/ui-icons_454545_256x240.png DELETED
Binary file
core/modules/away-mode/css/images/ui-icons_888888_256x240.png DELETED
Binary file
core/modules/away-mode/css/images/ui-icons_cd0a0a_256x240.png DELETED
Binary file
core/modules/away-mode/css/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/away-mode/css/jquery-ui.min.css DELETED
@@ -1,7 +0,0 @@
1
- /*! jQuery UI - v1.11.4 - 2016-04-05
2
- * http://jqueryui.com
3
- * Includes: core.css, datepicker.css, theme.css
4
- * To view and modify this theme, visit http://jqueryui.com/themeroller/
5
- * Copyright jQuery Foundation and other contributors; Licensed MIT */
6
-
7
- .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}
 
 
 
 
 
 
 
core/modules/away-mode/css/jquery.datepicker.css DELETED
@@ -1,210 +0,0 @@
1
- /* https://github.com/xwp/wp-jquery-ui-datepicker-skins */
2
-
3
- /* Date Picker Default Styles */
4
- .ui-datepicker {
5
- padding: 0;
6
- border: 1px solid #ddd;
7
- -webkit-border-radius: 0;
8
- -moz-border-radius: 0;
9
- border-radius: 0;
10
- }
11
- .ui-datepicker * {
12
- padding: 0;
13
- font-family: "Open Sans", sans-serif;
14
- -webkit-border-radius: 0;
15
- -moz-border-radius: 0;
16
- border-radius: 0;
17
- }
18
- .ui-datepicker table {
19
- font-size: 13px;
20
- margin: 0;
21
- }
22
- .ui-datepicker .ui-datepicker-header {
23
- border: none;
24
- background: #23282D;
25
- color: #fff;
26
- font-weight: normal;
27
- }
28
- .ui-datepicker .ui-datepicker-header .ui-state-hover {
29
- background: #23282D;
30
- border-color: transparent;
31
- cursor: pointer;
32
- -webkit-border-radius: 0;
33
- -moz-border-radius: 0;
34
- border-radius: 0;
35
- }
36
- .ui-datepicker thead {
37
- background: #23282D;
38
- color: #fff;
39
- }
40
- .ui-datepicker .ui-datepicker-title {
41
- margin-top: .4em;
42
- margin-bottom: .3em;
43
- color: #fff;
44
- font-size: 14px;
45
- }
46
- .ui-datepicker .ui-datepicker-prev-hover,
47
- .ui-datepicker .ui-datepicker-next-hover,
48
- .ui-datepicker .ui-datepicker-next,
49
- .ui-datepicker .ui-datepicker-prev {
50
- height: 1em;
51
- top: .9em;
52
- border: none;
53
- }
54
- .ui-datepicker .ui-datepicker-prev-hover {
55
- left: 2px;
56
- }
57
- .ui-datepicker .ui-datepicker-next-hover {
58
- right: 2px;
59
- }
60
- .ui-datepicker .ui-datepicker-next span,
61
- .ui-datepicker .ui-datepicker-prev span {
62
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAQAAABFnnJAAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAEgAAABIAEbJaz4AABe4SURBVHja7V1diCXHdf56vbZmVl6nxwKFO2yyq1mM4qAwM7oDsR6C7iYIKesH3V1QHgyBu5YYJwHjrB9NQCuByIthHbAga6TZxeBgHMJKISZ+SDIb1oQgRtoVgtjGyD8PmSGQMIpfJmCLk4f+q6o+daq6+965P1VfM3Pv7VN16ud8Vd1dp6o6IUSEjBPTzkDEdBEJEDgiAXT0QOhNOxPHiUgAFT3sA9gPiQLjJsD0208Pbe9rM/OvwkaBQvP0yzhG6ASQO0AqDwmu9mOPT3nqPWsYV9qFEduVIDP/QU4BSfMC9REqAcbRAa520FDELdphc3SJCyRIcADgAAkSQXOXMs4ckrIxFEUs2oENBNSqR0WmJ2kVv2hltvRdaVPHvPtqdpVxjlD1AHIH6AupDbovH1nqkgllLd3apnQJUjV362dmDEnjOya5FUltsEqqbdtxa5Dbppx3uQ+sNLv6mblCcwLIoKlXTQ/7rQkmX4IKzdMv4xgxbgLMO3rYXyTzuhEJEDjiSGDgiAQIHJEAgSMSIHBEAgSOSIDAEQkQOOJ8ADPutPN/zGgyH8BvRoDLGdMT5wPIKbjN02U+gNsdnuV9oUjSbD6AnwdMrkK7gVYt3311u8zv0r5vfNq1L8xsgPp8gAz20fAilORvs8tdsX3mA0i5k1N3x5dBue7icyGgzwfgvus48OoF+DDu9ukzH0Bqf355s9OHnLMNmqQ0F2jjDJIcrrM+H0Ail6v/KUoe3cECpl85XecDTDv/x4zoDg4ccSAocEQCBI5IgMARCRA4IgECRyRA4IgECBwnp52BmQNNcZS/+1hp4/yf7BZ9IpUwzRyQMwftXUHumFMouX4JIED09fvsD0AtJE3RNg1X/jPTJ6IWNznaxvYrgU+oBnFPaAFcxU88CmCPn3hUkE8RSHD2+OQvEWT6Z7M0Com7BuQSygQiR2zA1Yi1/KuXAN/i22bruCspGUMHT6In0nUV7ZIDKmMnrFRNnUulaF72PJAgl3VXpZObgZrLBGh6E0gerccVyoed7dq4n3ETD+2SgXz0tq0BqQn66HbXQU3e5DGw6uJ8QvEyuQt0M1jW4epi/bpoVwtype5zE9kWboq75VoOTHdw6E8B851+i8fIOB8gcMSRwMARCRA4IgECRyRA4IgECByRAIEjEiBw6ItDi81Spwf3fuCThE8N+HhE28VzrY32TaURKgIUC6N8tntvVwHjQztdrpz71YC01Wzljes1jp35KYvDXoZe6xogI5cA+MWh8hJOH492jzk3PgrYdPltDWHPv18N2NLoGctre41iVyVw9UDdlqbXFujqQ8E+26m7lmDbFoj6zaRx58Cmx72FuzvnfhvS8z63umkTQ+5aWF4Zh1ufSJpm2WFsc1gn9TBNCeBfAUmDuH45kKvQJ3332n57+q7YLgK460A2oJsAMoEsBGj2FFBVQBuPVaLo4LWT9iml3wZVvtveQ0ipr+bS4grO7yAgpV/E5O8BXJqrvRuk9c1mLhvOB5AqQL+BkYovZ04qoqRDLZp72hdnAjcBpRIcGBtIHDTW4AfJvAfla2/8commlwA/XzU5t4iQr4JdYvtVL18Ov2tw9yltkp72L7Vx3wOwiPMBVPScb0xaOEQCBI44FBw4IgECRyRA4IgECByRAIEjEmDcmLPHKnM+gAuSv8qn6L2O/u5Jo3vuEsdg94yVvT4fQILkMfcZJSw8dqseC0zt6Lq4Sl4enjiXv7oNbIsvaZ8SqoEgNVvu3X7rg43q0sh2b//2HWgGJI+dK3ZiDaeetXk7pcWj5CX1L+mxgL8HsE06kP1V1aWhzaSFrOW4p03Z9zAg+Pr6kgZnuXS59N0rm2cS5vsCqips+gZu8xKSNIhbSP2WQLsXb7ffQkIyo6uH8Ncs7RFwzKh6ANVR2CZrrjcJuJ2tXa+OidKD8D2E/smFGE8r5lLQe48ZMb9+CTgoM8V14FkHn0334nEgzgfw8fa7t4eQUc0F4KdUyFVfXYTsu4sUIZOGUjVE2wk1EwHvDSRrB+7jLLXdBI7D2eo3a7HtjabPTSQAB0Fnxrg+iO5gHTPTNR8XIgECRxwKDhyRAIEjEiBwRAIEjkiAwDF7BEhny1u26DAJ4OfLllw25CW1hUhxeCxP4tPeB2Fm0Gy7eJ/tpBOsdMjPIQqSpGK4tlvWV1igN4B3gU4AeZRaHu0uWnWKQ5YCVA6U8mv3sq5/pQxxaIlffGu3qbra+/i9an7BURFA3+69jsofbtsyPUGSm/8DNi25a8+6/g+wIqydLVLn09dnFJha/LaPCA7FULDf6nYJxVwbm/nNuThmL+L6LZ2tS8xw5vYR0lvOg4I6JSxB3Uwm7FMZ/CZE2CeVmQSyTcngc+jaPkEn+IK9Ar4LqhdGqFVm32DE/dYL39dK2OQZBah2H1E4gvld+fX8+Lw2IgIAtz+A38r1ul/evUONJM1uHuUQurbE61wB9w5CgcL/hRH6rNjq+7hhv4voBv/tI4LCrM0HmJT5IyyYNQJEHDNmzxcQcayIBAgckQCBIxIgcCwSAfqlJ6A/Ef0nsZQfzd63OtPICDDMK24Xw9aaXnZ4+l0g3NBmCzQ1Yh975fc9Nna/Ez1O4ld4FEc4wqP4FUuBvjP3a7l0zZKGXe6KCTxnHHIKxtwOoqs0oAENSUUVtjo/JFgOKj+3tbhqCF57doyor8lHmrRPJvqN5HoOudytGfHXDPkSrRPREi0R0TotCTXAp1GHVEP+EjXEtncZyxAFk+8BAO5YJnPcxpdxD0CK2+W5O7hgZeMXLCOKy+W3o5rsJr4htL89ZBtLZNgHYU9LYQ9bSg8AbBlyN95Xcpfl0Ix/H8t4EEAf942wej6BLUbim5suL5+XsYb3y++lpoIA9/NPdRqGmuV7AH6EI3G2z1L+ecMa4qNC7Ct4D9uC/JLmSr6kUDHD2woFtvC2ISWoizP5FYYfxS/K759g8/AgPoEEp4RcXhRkpkeSl/MvoE4YLc0IXpj/D/DP6unqWnboUPAjHNXC6FnICLAiaLO1G8DVAwBvlRQgXMJbTIiCAnXzZ0iNz3rufl3M6Qb+G38I4B+xwcZ/GygpxOdgK++p9N7qOFCY/4+wrFPA936WM3+9AgG5o8razk9ZmasHKCgAi/mRpw7rTd5549PEEn6efzvLSB/Eu9jAfQAbeBcP4f8MuTkLqe0yU3dH7wrxOfxN7Vxm/i/gQQD/AADFBdz/MbBufrOAp3AKp7CMU9ZO8gE8gAcAcC3spsP8QA9v4RIu4S3LdM4+DjDEEAct7/OX8Vv5sVzLXw9nQPgFNrCBEyCcEaeUblmMf075s2EJLrhCfIs5l5H+Bj6G7wAALuBOJmj/RFsv4rIzzg+FkK4eoIcDIG/7B0z19zU5R4HfMD7NKspayQayOyK9n3gY9wH8pPx9HxvCjKIuHfxRxxD8hNmf5OXL7s9K86N8DEwdj1HuR5iXNfnL1scV+2PMjQk+BhYPSdWnLRd95hGQe4hzPeg1fQwGjayl9wmhlpDXXzwEDtSzs+MO7gP4HeX3e5YbqcmCAJxXWvr4tWeYztSUNbyvtX7E+QDBY5F8AREtEAkQOCIBAkckQOCIBFgsvIJXmkXQCZCC4FqYbQeVjoyujxZ9cB71LeXxte5vGxqPvUNDvm3IuUEn1Vu+NgE5APylo34GGLSut9P4Cr6C045QI4wwKn8pdZISUZ/6RJTWBhF2aSkfRrhG6zRkh0Fu5AMR23TDkMuebPPos+G3FE82EdEWOxBSHPX47oGcLMw6rdP6hORFmGxQx5QMqBiQS83hmtw6RNuEvHQpo/suERHdFWu3GEwa0TkCQU+gn4+h1dUT9RqNg3FyENGf5X9XrRXUp2wCSt+iH5YUXAYmAv07ge4R6N+InzJBtJ5L1gU5RPlpWqLTFnlR/SMaNa7BAtusfLsWe5tNf0hEI0qrkUTVHVw4Kfew1WqjFnWuAO85fAL7+DF+gJ/jPYtPsI89XMIbVpduF/wTfhffwwa+hyfwL9ZQ2Vj6hijXxyx1/LLmJ6wwwk0AyP/X6+9Q+8WDn21x1uMMAHwJwEp+eUzxAcoe4IYxmt6nG417ALWDs/UAV4noL4joFbaF9InY1j+uHuBvCfRdAn2Llpj0H3X0AIUcotzeA4y03JmXgIGS6wyDRuX7qib7KlOD5+hcPXUziX5OhHr1j4MAn6HfpM/R+fyvifn1WXE2AqRElFKaXy9N+XeI6Nv539/V5EX5bNfwSv64Q87Hr6692XG+IQFMd10q1JDsatLOmwrUT122RENCyaEBE0ImQN1f18T8ZvFkAvDys/Rdeph26WH6e/q0IV+t5W5trPLK/LAeuolTUW6T3s1vBE35iIpr/25bAsBxUJ6N1DM8b+C+Vb5lVDD3FJCWRecJYidghsccuesih8P8Pi73ggIpI3uKPsxv/LbpQ3qKIcBVGtXj+ibvNqh+H8rfg0qTlmXzmxTYqkld07plORFRz2GcLvKR0/yux8CKAqkl9nPl9+dqsa09kK87eB3v+gWM6IgBoHvsx4QRAOCWeTrOBwgc0RcQOCIBAkckQOCIBAgc4REgcxsPGMmgfDZ61EOPbTfFOburVgmwXlbAemt9NLUqSEHYyb/vwDar4Vq+qHS3RoHfxy4u4zI+hU/hh/h0LWZWO1fzX9mepeq+55v4LAhP4AkQPovNWnx5+4ph7cF+6JC7QgxraRgjQObZdSLapSEN6ZAqpwc34CENZpAzRPtjlOdrnR1UybBDoB1rLorhkHPsUNep8vgYOxa6Q8M8B5meAZO+fRyv8FKklmEw+UwVO7XoycYa+7k3B0z51BjrxXc1QDZKvENEh1YjTpMAxVgWn0Zh9uqzruGaONZ5gj5CCSX0ETrBGmRIIKJB6dPncicTQPWHmCuXMm9Iv6zjOgGgzaKoEyDTul2OyXIEOEOr+cEQYJhX4w4748dFAHJWgdu4cvx1Rcb1UDuKnDN/KuonSspjjUn9eh5rlx9SpWz3kOLgCWBPn8oZV7wGKn2cQwKdYwkAhQB8n7JOm7RZ+isZAuzkVcdP+XIxXA7RTMoTyOVTk1p/NeMpw8CQf5uI1vKD6DqjYVC6bLn0iU53IsBD+dmHyv9m7KL/GhLROUsPYK+/PhGdoTO0Smdok4iuZefV9wV8gBQ38fn8FknecR9OubSbuFvKpbBe7mOSrdE38U38cfn9+/g9TZYqM56AK/UxcazjS8qvl/EzIfWslszyrwD4EADwS2aDGcKKNs/KfMHFI9p5ws8MuT5f6Ca+bMzayrbwzybL7jHb+RMeB/BfAID/VGpX62APaUhD2qXZvAksWh+fxjfL1p/hrhG3n7f71Kp/s2w759jUd+gqUd4Odxh59vmQ5UbPnGhjtt/H6DHjv9x/NL8HGFKfNmmTNolos34JUK+xNvPP/lPAXSrmxnLVM3CksEPbtM1Op0IeO6MQRwESfpln12ohhrUOfGjEPFc7mt0DEA3Lw0IA31Y4LQLIR6q0+rtkes2z1uFTOlv+s57lkFIqbieTWtzP02eoJ9QSEZT7DF22lD8FZP/NbejqBDEpMhSlaum0MkZ3sIqH8BKAF/E/rHSEFdzM32aQ4n/xa9qbDVz3SIC+meUk5j23QCRA4AjPFxChIRIgcEQCBA6TAEPrfuEXcau8obwlbokaMU/QHhRuExHRbeYB5ou1h5AvTuVhLx5jPtQfz5TGfcYIdlF7eixwkVG4R0R71uR+6jkgA48Qx3/sKfTfm3puJkCA21T4nMw+4DWWAK8xCuWBoNowhNX8HEncy6tcIVxyor1yuGiPlfvlf46O6uszmoH1PsCGpgQAgV24WTd/vZ9Q19byBnSFGBHR07RET1s1+JVuQQmQXf8Lr7PeB+gFl6phm2zLwlTz7zKSQfnJXyZcxkFu+DQnAid/Ov/2tKBhT0hjgQkwIBOqAfwJwHfeuqZ7jEeuiGczvy8BUrL3MVSOsNf99RXsBOgT0R7t0R75+BXm5DArwPzelACuozBS3fyZ2W3mn4UeYIFvAqv2nyp+58oMX2cJ8HVGoasHsB1qD8THn/49gJuCc3hwRasX8ilW+hSj0FU5dqnU+nUDj1qH6PoUkIWZutHGT4A91sBqFTxfkz5vNeNASFCiR9ve43iPBSOAvzv4SQzx5/n3r+EN/Ou0xzCnhLbvAppRxPkAgSN6AwNHJEDgiAQIHJEAgSMSIHBEApgYgFpL5xAqAYjZNkEHYVyvhJgWbgjvNgeAAXZbS+cTxigd0a4wGqd6A0xJES+1ahjQbu4IGtCuoAGEfHmTLt/WNLVx1w4cg80Dj9hFDdi1zNXBV5/NhBIB1Hj1ah6UcQb5CntZg50Atvg+BJCksqOnLl2QIWFbIflpHS4CZCasfutSEErjuTTYCGCP7yZAUxPbpfwGDHN58DeBd3AB32hxPbmDC7iAbJ3cBWXMPFHOXhB3wi002OCKfzxYJG9AjeWLfA/gOgK8B1CdQYQ7eElsX5Szv/icL6hPLrbcZ/f57aRziegNNDHArmBiWTqHiAQIHHEkMHBEAgSOSIDAEQkQOCIBAodJAFJeLB4RACoCpPlWqWdx1rLXfjZ0dM0ijZhLFARIcVhuDvMIDlkjb+ACvoYXcejxQglzeGHbGIHcPmZ5hA15je0Q0V8RiOhFyvbFtY0ep+JewtWovOt3/1jl8bAchVGJ7hEI9CINCHSPbG+n3qFzlDllU6tSfqfapXw79dP5n7nT7WmSNlwv5CTKl8q/pdYOocCOkwCA8wDeBAC8BAB4Exs4X9vKdAObuIkreASXcIgruN6oq/mk0ukkqI9Afzz/S8Avvzqd/30S/ObyHwewkstXFmu8fpLIfAEpDnFfedHRPWxgRdsJt0CKQ9zBBezgClvJlO9UX9+t/re13/9h7JU7aXmEDXlXsEvVoukR8Zu4ZL7w7H0VtpfK2ObL6O8Wr883mrQ8Hpaj8AZm78O4jzfxLDbAv5Gjmg1wAWDcomrrX7A1tIuLyh28jmv5g+AbuBZfFh8K4nyAwBF9AYEjEiBwRAIEjkiAwFERwPU+gK7yJ3G9lF/Hk8cun3T5pi1vi1yn630AXeWubeYmLZ90+aYtb31kHxeJQ/U+gK5y10aTk5ZPunzTlnc4skvA5bJDSJQRvMvMNxWm/IU8boIXDPmzin4wZ5/l1FvkSYv4l9n4XPlc5Vfz3zz+20iUELb4klzNBW+VhsgGgqrRoKT2S5dzGSnkCYDnAbyunRmffp/4bv1qiKbxE/ALzKjBb1K01OV/AgD4a2v9FWc38Q7GskhNJYD+Lup6Blxy4AW8DuB5vCZWsK2C3PoTVpuvAdz6ZQJ0r5+kPN9O/jjeUcw/FgKc7K5CwYfK/+YgZ48gQ12O3kZDony2iU8ecV1usj8VZI/jnmb+sWDcl4ARgFtofwno3sXb81fX0K4Ll/T79SCuHsqW/uO4p5l/DD1AdhP4Kit7lfkmyZ8HcBM382+q/HUlFjFnX1ekLjlEOVi5nn+qnX1Vk7jkbeunKB+1lr+jmZ9PtSmCeAyb9mPopOWdxwEWfyBm2gNRk5Z3JgDoSbpeKr9OT9aCdpVfpFul/BYziDFp+aTLN215yyNOCAkc0RsYOCIBAkckQOCIBAgckQCBIxIgcKjOoLrTUcesyyNaQPcGLpffjtjQXeURM4f6JaCb6Y6cGrq13KSzhggNJgFcBjzCkShfxpHSD9ThMqA6JYoDtfT1R1hgEmAZEA24jGVRfoRlkSAEecJE7qGwIq47HjPql4DlFlr02LKGbu3XRaCIhtDfF6BJamFnXR7RAtEbGDjiQFDgiAQIHJEAgSMSIHBEAgSOSIDAMb8E6MUBoXFAJ0D3cTZCH4T+xPPdwz5WJ55KANAJsJr/TRuu1p2Z/2Da2VwE6ATYz/+mC1frjuYfI3x7AEKv9tcMzKokFpl57TQszB/vAcYCfUbQPhLss26WhPlrhi2vUIX5V0X5QbwHGBd0Akg9wGppmuKvWSe8VztTJ1FP0c9RTDd/vAiMAbPUA0TzTwG+PUB3uN/f0cz8PUu4iEbw7QGOA1Lvwpk/3gOMAfqEkB4O0JvJzpXyTt/8jOiIOCMocMyvLyBiLPh/gj9Qphd3t8gAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTMtMDItMDFUMDU6MzM6MTAtMDg6MDApYMCSAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDEzLTAyLTAxVDA1OjMzOjEwLTA4OjAwWD14LgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAASUVORK5CYII=');
63
- background-position: -32px 0;
64
- margin-top: 0;
65
- top: 0;
66
- font-weight: normal;
67
- }
68
- .ui-datepicker .ui-datepicker-prev span {
69
- background-position: -96px 0;
70
- }
71
- .ui-datepicker th {
72
- padding: 0.75em 0;
73
- color: #fff;
74
- font-weight: normal;
75
- border: none;
76
- border-top: 1px solid #333;
77
- }
78
- .ui-datepicker td {
79
- background: #f1f1f1;
80
- border: none;
81
- padding: 0;
82
- }
83
- .ui-datepicker td .ui-state-default {
84
- background: transparent;
85
- border: none;
86
- text-align: center;
87
- padding: .5em;
88
- margin: 0;
89
- font-weight: normal;
90
- color: #333;
91
- }
92
- .ui-datepicker td .ui-state-active,
93
- .ui-datepicker td .ui-state-hover {
94
- background: #0073AA;
95
- color: #fff;
96
- }
97
- .ui-datepicker td.ui-state-disabled,
98
- .ui-datepicker td.ui-state-disabled .ui-state-default {
99
- opacity: 1;
100
- color: #999;
101
- }
102
- /* Other Datepicker Color Schemes */
103
- /* Blue */
104
- .admin-color-blue .ui-datepicker .ui-datepicker-header,
105
- .admin-color-blue .ui-datepicker .ui-datepicker-header .ui-state-hover,
106
- .admin-color-blue .ui-datepicker thead {
107
- background: #4796b3;
108
- }
109
- .admin-color-blue .ui-datepicker th {
110
- border-color: #52accc;
111
- }
112
- .admin-color-blue .ui-datepicker td .ui-state-active,
113
- .admin-color-blue .ui-datepicker td .ui-state-hover {
114
- background: #096484;
115
- }
116
- /* Coffee */
117
- .admin-color-coffee .ui-datepicker .ui-datepicker-header,
118
- .admin-color-coffee .ui-datepicker .ui-datepicker-header .ui-state-hover,
119
- .admin-color-coffee .ui-datepicker thead {
120
- background: #46403c;
121
- }
122
- .admin-color-coffee .ui-datepicker th {
123
- border-color: #59524c;
124
- }
125
- .admin-color-coffee .ui-datepicker td .ui-state-active,
126
- .admin-color-coffee .ui-datepicker td .ui-state-hover {
127
- background: #c7a589;
128
- }
129
- /* Ectoplasm */
130
- .admin-color-ectoplasm .ui-datepicker .ui-datepicker-header,
131
- .admin-color-ectoplasm .ui-datepicker .ui-datepicker-header .ui-state-hover,
132
- .admin-color-ectoplasm .ui-datepicker thead {
133
- background: #413256;
134
- }
135
- .admin-color-ectoplasm .ui-datepicker th {
136
- border-color: #523f6d;
137
- }
138
- .admin-color-ectoplasm .ui-datepicker td .ui-state-active,
139
- .admin-color-ectoplasm .ui-datepicker td .ui-state-hover {
140
- background: #a3b745;
141
- }
142
- /* Midnight */
143
- .admin-color-midnight .ui-datepicker .ui-datepicker-header,
144
- .admin-color-midnight .ui-datepicker .ui-datepicker-header .ui-state-hover,
145
- .admin-color-midnight .ui-datepicker thead {
146
- background: #26292c;
147
- }
148
- .admin-color-midnight .ui-datepicker th {
149
- border-color: #363b3f;
150
- }
151
- .admin-color-midnight .ui-datepicker td .ui-state-active,
152
- .admin-color-midnight .ui-datepicker td .ui-state-hover {
153
- background: #e14d43;
154
- }
155
- /* Ocean */
156
- .admin-color-ocean .ui-datepicker .ui-datepicker-header,
157
- .admin-color-ocean .ui-datepicker .ui-datepicker-header .ui-state-hover,
158
- .admin-color-ocean .ui-datepicker thead {
159
- background: #627c83;
160
- }
161
- .admin-color-ocean .ui-datepicker th {
162
- border-color: #738e96;
163
- }
164
- .admin-color-ocean .ui-datepicker td .ui-state-active,
165
- .admin-color-ocean .ui-datepicker td .ui-state-hover {
166
- background: #9ebaa0;
167
- }
168
- /* Sunrise */
169
- .admin-color-sunrise .ui-datepicker .ui-datepicker-header,
170
- .admin-color-sunrise .ui-datepicker .ui-datepicker-header .ui-state-hover,
171
- .admin-color-sunrise .ui-datepicker thead {
172
- background: #be3631;
173
- }
174
- .admin-color-sunrise .ui-datepicker th {
175
- border-color: #cf4944;
176
- }
177
- .admin-color-sunrise .ui-datepicker td .ui-state-active,
178
- .admin-color-sunrise .ui-datepicker td .ui-state-hover {
179
- background: #dd823b;
180
- }
181
- /* Light */
182
- .admin-color-light .ui-datepicker .ui-datepicker-header,
183
- .admin-color-light .ui-datepicker .ui-datepicker-header .ui-state-hover,
184
- .admin-color-light .ui-datepicker thead {
185
- background: #e5e5e5;
186
- }
187
- .admin-color-light .ui-datepicker td {
188
- background: #fff;
189
- }
190
- .admin-color-light .ui-datepicker .ui-datepicker-next span,
191
- .admin-color-light .ui-datepicker .ui-datepicker-prev span {
192
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAADwCAYAAADvl7rLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMUIxRjI2RjhCODYxMUUzQTEyNERCMDU1QzdBQ0EyMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMUIxRjI3MDhCODYxMUUzQTEyNERCMDU1QzdBQ0EyMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjExQjFGMjZEOEI4NjExRTNBMTI0REIwNTVDN0FDQTIwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjExQjFGMjZFOEI4NjExRTNBMTI0REIwNTVDN0FDQTIwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+kKfR4AAAHcRJREFUeNrsXWuMXsdZnuMu4CUN2S0t0a6o4sQqAaooTncrKGrUNbe2qSC2uQqpqtexbCqI0xaQEBclKUL8qts6stqNkuwWiYqbajtqS9oAuyEISrubrLmUFnBSfmRXFa12KT+cKk2GM9mZ7ng858w7l3PmnO88jzQ633e+8565vs+8M9+8MwXnnAEAMEzsQREAAAgAAIABYkx9OHXqFEoDMDFVho0yTJdhE8UxOjhz5gwsAICk/Exep1AkGAK0DY6G93L+eUblnw4gATPNqMceEsBURMVxSwhFbO8TEj838j4V8B6eQHF1RWyzDnTl3zRIIDTNsCJ6RABdMf+mM6XBjNfsDXP1wm2hkEGN+ze1eyFpngYJ9IcAYs0/vQEVkenz7X1SwRZv2xNhqeJNVRcxac5Vj0AAAcSYf13pBVMMPfS8xyhhaFpy9pqhQ6CqNOe0ZgBPAgg1/7rcC8aQQJEx7lzkGzoEqkpz7noEPOcAmhhPxjTGzcA42zR/m0jDZkSPGZPvmCFQVZqh/B3EWMfTV6CKvm2F5LJAQhYCmWlGPYIAAJAPMMQhAAAAIAAAAEAAAACAAAAAAAEAAAACAAAABAAAAAhAA/zI8+wHoMcL772BEkBsA0i1J0CsM0yIAvGa0CZ55doPIIU7uF7uIJKeEUCKBpDS6yumEW4kTPt0S+mOVf7YtG84voekHXsB9IQAUlXeZgMk0JYC2dIe6swS2vBTK79P2mMdmTZayBPQEAGkrLyUJDDdkvLb0h7ryRZCAtOZlD/F8G264TwBDRJAURNYJhLwbcSpepqU+wFs9KDcqhR1OkHa4Q7cQbTlDRjjVRa6H0BuFInKbTqQ0IpE8YYqrl7n8CgcOAHkVKK+I+d+ACj/AQ4BAAAAAQAAAAIAAAAEAAAACAAAABAAAAAgAAAAeg1sCw64IJYBFwPNt4li1Mp/DJUPBSAoQUwZ8AwKlCLOQdT5HmLlh1ZCikM6eUsyOXqUJvMRW/660hcRaUlBHDyjEqdsuzxTW+O+BJCi8lOdyRcSf5GoAaWqhJAelCcsvyKB8sQocmzeU7SBWAuorbabmjxqy3+spcrnERkPbUhFR8z3UAVIMQ7NVQbcEneRIO/UvBSW/PuWAw+U54naQJFgCFZVlgV1DiDH2LlI/K5UzNlmL55ScYtEaQ9VoFTpbbsNhHaCqdKdoh045fe0pPxFBiKJnXtIwcAx6SgSpD+VCR3bA8XmPdUkZB+sv9TzGLV135QFUHTgXTl7z5Tx5yzH0PynznvRkTJscx4lZvhHjnesw4XXFRTIP/I/qvFiJSAADBggAAAAAQAAAAIAAAAEAAAACAAAABAAAABDIwB1suvQT3WNXU3YZ6RqA6m8QtuML/Zk6N60P8rhoBs9bABdJZO2G05omadsA0Vg/DZvuKkW4lZyKY7GM9Ocwx14inm6A28Q7zVdCSGVzztKAr7pmkqYh5DyT9kGeIDy1x1QO9Vg3E1YQDmPRXceklt3OGjV91w9H6Ugiw5aAr6706Q42Ti2/FO1gZCdeTYqLBkfIoptBzEWUGia60z/UEvOVZaNTwKGNIC6sVdo5fNMSp9SAXwbQS4yrMt7G+vbiwTtILUV7Itp4r1G5gCaVP62HCtiG38KBs6xFx5FCbpo9VQ1dNv4e7qFdhBjAcWmWcA8Xj3FMe/WuYw9HWwAdZMvoZWfk4FTKUCKrb24R/2lMEFD0rtZU9Z9PCo+9nj1IlDeVZaNEkATPb5vQcb0eKkYOCQdmyy9uedbH6kIkEeUv82nfbPFNKQq981Ew+DQ4V/tMe9jDTJnFzazYJEkkGszkk2WvucqPOOf1sa9PgTYtf0D+riZSGvx4mAQoE0SAjoGLAUGABAAAAAgAAAAQAAAAIAAAAAAAQBAVnAUQfsEkMobLdSjKlWl6/GjIYUpX+6yCz0hCfUeSACpvNFCPapSnYmnxy/eN83SHFLat14s9njwgsUf8JpCgXlLaR88AVR5o/n6NKf2KQ+NX5GJz8KWlCfqhnok8kRxhypwEdgTV3kDxigwi6w7kECCOQDfjQ1iPKpsCt/WxgrcQn4pThj2aYS2k3lTmNJNPEvNrw+BxJ7KDEQQQJ0LYRs+0dNaXLEbK8QoSpHAjCwSKVauHoxnym/Kd6QYjgyKAKq80dpi4VhvuBSurLlNxqLCAuEtNvqu9cQ8sO5SHfM+qCGAbbzssyXSlFFpIRsihO4HkMqVNbbx8wQkEDoOjm30tmGQ7xCCB86rxMjWkQCD8l8NH2/AjcAKjPGlZwHviXFlTWmG6uWQwowtAuOPSXusK23oxjAxsk0ORwZNAG1MIqV6X1dcWYvM70mhwLn86aGwGYcAAAAlBAEAAAACAAAABAAAAAgAAAAQAAAAIIDRxwTD8lFgoASQ0g871LGGJ5T1fYdQ/i02On+HxZ50CwyIAGIP1kzpCDOZqXy2LEQyEamAOWR1bIAEAAoBxKyfjlnPbfbUqhee9FAUcxmpz9l6usk/aXnHlmf85r0YQk1lPW2g6QNVBMAjeyCbPzj3JBSlbLryb0dYH769vpLflnGHnCxr5t0n/3V7ErjSkmo7N2AAGCOY/TygF09hRTCL8ucYi2+zdA41VOWvKjdX+ZvbsBUV9TuNpg/YCMBstFWffUzQWK+0WMSOu32tj7q8u8qhqpenluFGTb4LTfk30fQBGwHUKV0R0fCLSDLIRSCFQQKcMBfBa4ZCrrzUlU8XdtcBBjAH4OqNQhXWd0+8mP34YuQnLbJbWh62CUpW1NwriKQTgqrt3KD4gDcBxI7fQ7elKioCi5APGfPrsqn+hiwSP2cidjs1AEOA6IZYJG7UOREzB5ALmww9PpCIAIYOKBIw2CEAAAAgAAAAQAAAAIAAAAAAAQDNYoZdvZZhpkfpF5PLe42ACeeOE8Aho8Ety3tt4/0szoc/FiKuBVa9l8BMC8q/arm/6hH3TEbyEIr+rTLcXIbLMtws740Fpj8kHzcZsjd55iNEPjZOgV9whJg8XKVLeoWcK8P7yvC0/D4h79lg+3vskOX5w2U475no32dXH8t1kvktB64jDNc75svwL2U4UfH7LWVY81ReHbMR8qsOef250KPNRKO5VPP7/jI84yCA15dhvQzj8p4ggQNl+LIkAuaZfp98VNX9JY82wCxlcIkgFyJj4s/l9aTsiFLWY1FHAExTfoEV5rcCjkog4r0HA5jRlwTGLfcuE+QWy/BgYO+3qhW0uSJPnXa8WpMHpeCrNeSxyppdn3Cpouz0MnTFr5T/Go3Y1h3vrStPlXcKmjiVqm+Hy1aROLextVlxOrY8C1kp/5dlQwldQrvX+L4Q8I7vCIzbZQG4cLiCqLj87ZxDfq2CBCg9P69ogL7nFIqy+4bl/vd4lMM18nkR33cHluUdEcO4GIIIdYcviGkpWlT+nyjD39SZa4yo9FQo5Xe9pyASwGRg2sYD0x9jAQh83kICSvk/T3yHSQIU5dcx4fhOKbvrI8pUmPtfK8Pb5fe/kveYR/6ZhYR8ymDWsKhmCcOzvsNU/l+UdVZJAqlnZqnK76O8oeaU3us826IFYJIA81R+G0n6TuLtd3ynWGD/bdy7waPnvygVfl0jBHHve8vwPLH3rduZqs0l2inM+dh3/EoZPu45b3FSG4J9Urt/xfC7ib8Btzwadp3yqjCuffbBd2nBpwdbjFR+gSmNBJTy+2zEKRReOPUckmGTtTuTL8rpB40wTig/kcfvl439G1LxD8h2xuVvoRuSznoq/r6Ka+xwNMc7/iSA+MWw+TvZ7qSiUv6VJi2AVJM04wni+VLgO2MtgCm2u+OO3utvEhv/TI08lQRe6/juakSXDHNenx+qsya+T3vO9k/BunxfyI5Eucz3y5nf4bMvxzNG/S3UKX8oAcxGmDkU5f8DqYBVv6UimSbmACjjzNkG5XVFrPvuakT63MWYTJPr7z+dJFzPuCbSeIK6/cuKK7UTWKy43+Y7QvCMhcStyv9yYXK+U9anTp1iwBXj7VsqfhfktDaQsuBar/9MT9OeqmPoG9SEoFX5z5w5k20I0HWsGdcho0Dae4tnKGUAXwAAGDBAAAAAAgAAAAQAAAAIAACATuAPZchCAOqU3NgjsUNg+i23vSeADcpHnbIYZ5bZ/bAp/+EfYtV7EXDm3qPhhEOeusCpypf8pp7IK/xRZPuZk6FtXFuG35Hh2sh3HdXCFRirUf4trcFST+gVm4gIBxB95dP9bMcD7kZ5pfhzi4U4a1qlndQUsKiRUygaUH61QMfljivK7AtGWlTaxP03svrFPue0PJtYIJShy3NygdEWOqmFJLoTzzqj+7jnllf4baNNCCVYIij9smzzy/LepNSDykU1hu6oOnxQku6C9h7KOROfNj7fHqH85oIkkf6vvFwoloVAuvLr3mirhMSLlwk/+Lrz50M3dPCRV0r3a2U4q13FfgWnPRvQjCQjteGJyzOv6mBPah4oPVVByP/nyvCjbMdF+7Yy/EMZfozRnWk423XiUTK3MvdKPpu8Qoi8+D/7BbbjonyTh7zZ+NUKvMWG26Ape9JCykWN9eYi8JOMvlJVtVmR9/NSr8XnpbqFQKbyq15vVv7WxgKLqn0EfLwM3ySJ6D/L8O9sx7tNrOJ71lP5Rd7Vzka+brm58Ndl+JEyPCaV6DFZHn8bOBzTFTJUXuCWAHmh/M8HyJk936Jn+9vybJuhFpmOGxI9o3CPluZDhpWybZsDWGDV69EVCSy00IC3ZdiSYZv5H8/1j2X4uTJ8VjaGT0r2PBeg/Oc0S6AP+Em2s/b9bdJ8FFfhTkrdYONmS2/lQ/ymvAqvCJAXPf9e5rfBy9EahXetxZ8zlN88X3KrwTmB3y3DB2p+/4B8xoV9Mqh0npblMS+HP9tVcwAnLeNZphHCWsXYtItQFsBXy/BP0pQU138LUP6Ynp+zMH/wSW3uxdcCEi6gP1+GP5VKL66/xGhuqcJj8UvamFu59CrzPUReKP6LEfL6HICv8psK/2QL1utWgHWr8Jtl+I2a30LIr5K8Xb4AIY1+SzOZ9xkm98EWFH+WoGwfb0n5Y0nAtIqo+C22sxmEmNi5Xl7Ffgq/V4YjNXJi/uY5456pdPs7LG9T/iXPcl4xlJhbFHjbUU9VJECZANT/cft7eX2zabo78q+P+VfqImvCGeh5trsT8Fci5wxC9hRci4xTWTuhyi9m+b9QMxZ+Y8PKr8qdSetHv/6MQ+45Y7z+r57x5pZf1Ig2RPkplhZlHsxGAtTZf9E+XirDu9nuZJ+YHPyI/O1xot6oMf9hXwKo2hBR78maxMmantJn+GGm1TftoT3/ag0JuP4CVD3cJa3xbHn0gLp83e8uiJ54M6IOc8nPJ1B+ZakuG0qs/w1IJW19GEclcTHp+ctl+Avt3oPyPS8Q33FaK4/aeFPvByDGiRcZAIwO5rShQR9wVPtcSYJN7QcA5QdGDSs9S6+X5QNfAAAYMEAAAAACAAAABAAAAAgAGCno7sVzRJk5drUb7s2J0uPzV2wX3MAHSQC3WhrArRnSxwfYCNQ+DI8Y9x9h/nsz3Meu9H1YJpDAj8vnjsjwOhnE8twfIsSpt533Vih/YQQdwnPxHVL+TTJwee82QvwzlrZLPVDFtReDaz8GinzsOw556g2v0yPbOgC15nulDB+W9xZlwzPdOykJ8WV9m3zMO3LgqCwzVV6qTCkLVPQ8i3cck8o/71kWKg0KYj+GZwnyIv5rLPe/VYZvMporrYj3UUk+qgz09NT51KdwBzdXkFK9WDmju5tT47Zhy/EOtZpR30BmVbtfEPJvi+Pb7thqHYDNAlDKLypJLOn9Wba7BnmdARQsamV5VCu3RQ9ZJpXeVH6qW+s+47uPG/TzUtkvy/BNSQBUPMp2l4NPsqv98ldaqIP9bNeblHlaA+buT75W6LbMp+7J6uPRyo20zBDJ0YQ4i3FaC/9DHQJ8WDM71RFH8y0pT5W50sZwgBODCwcqFJbiT3+MVXuzKYuAMoy4N7IcXtK+v8Toh2t+SPb8qpzuNZR/ifCO8Yrgg1XP+wp72ZW7P+3V4qce8inK/7Rm0jPmdzhpETF/UkjiEW3tNWzHGew1WrgCdSsBdeU/Rhx7VLFUm6Z8ivMJY3GRXX023LzH8OlYpPLbNnVhmvldhz+TCm/6DFySyu2CGPdfsMw5+KzPF77/L7A8uMby/Wvy86uJ73iP9lmQofIsvZFoBfAKsqJ0Pspa+LpG3nuk8j/Fdrboq50D4DKRE0ajU5XZxpZeLOIdvEFZah6qfOepcyh/XIZ3Wu4L99DbA5WfqoAi7fdU/PZ+tutp6Jt3KoHp4+gXtfsvyOEIdQ6gapztGsPfWPM8Z/UernrcVcO797UwB/AG+fmr2v3n9LZbNwdwQBvzPyp7fqX8B4jKYc7uVs34UuV93lE4QowsNQ/rFSb8eoDy61aE8At/0jG5pCv/QdkgC4/eVxDUAzLdeqAov8qjauhM63HUfAZ1DC3C/7GdM+5f4WF+zzK/reNMvFKGZ+X1WuNzDKjD6Ng5gBukbl+vBSaJgbuGABeloq+zK/9C8v0HYMhQ5r8qs6fZ7r8ALrzT6PFFr/k6trspxJuJY1/X7rV1eFqm/3Py+w8QlV8nsRU5Dl6RQ4ctLf/HPN71dc+0rzH75itiQ1HXCcfCVNePprd9PkwYgt3o+J3SCXKP+za8lmTKNnw8+FD/BoyBMuNNc/9Jqfx1vuVc6wXXEtWdb/nr/1psa+NefbvsPTUNWd2/qwxfZDubuW4GtCVlKt9kmcsoHJOAr2e7e2Ay7bPYTq5ug9JDjLbnpNoxK+QddbKUoewVQ4CmjwcvMsv3EdsV+b695fJ6tWa++/yjIHr3v5NEtaiRldog43/LcB1hMuzhyPRXbQ3nOpzleXb1rlLUcj2foA7Ot6k3YwwAqk3vXw+UXaohN+ZQ/lQktsaG2YF4Ab4AAAACAAAABAAAAAhAwyFGXwGocIccA5rLZ5cY/WQaAAAyE8A5I1Bwdxk+VYZ3WX57l/ztbhQ7AHSbAN5m9PyH5D1Xz3/GuGdbOXfGwxJYldbDakDenjUskLkE5TWHJuNVb2ZYRdH0gwDUARyTbHddtutQjiMe8VKfnTGuPthnfF9OoPzLRBI4ajT8owHxxb4jVl4p7IxFuV2Y9bwPdIgA9N5f92F2WQF3VTQiTnw2NZT1MZngXXMagbhIQCia6bO/6KmAse9Q8qK+xuV1MYAEZiy9+QzUZrQJQO/pJ9iVa5dPBiphKE4GxqunXy0/XQk08+cM68G1xn7R837ds/OSwOY936GU/zNsZ2XbZzQS8MUa1GQ4BDBnjP31HVWUFdDmOPg/jKsvVNqFI85hovLrPbyv8qfGeWmBnQ+QfcLxndVYbLGbrsxo5LGmkQish44TwDKhB19uMX0rMv5YpTvI3GvPbWZ+TuVXhDvB/P+KFXiL43udxRa7hHZBU3jdnXUBKtddApirMKEnCGaywFmPeKnPzrG4GXzVmCm7sKywK3fLCVX+ec/7dc8uSitm0fMd4rnHyvBWtuPd9lb5PWRbt5mEMrAAOkwAyxUmtG1zBduzF2pMSsqzrCaeUKvD15w1SSCk51+yKJrvcdWx71jSSOCypvxLnuUnTPdZwzKgzAmYlkQqywJokADWPBuGicfLcJwge1w+SzXdGaOfyZ5q2HEw0uxfMhr9UoZ3xMoXzL6vAP7KGyGMJa5Y4cP9X3LM+h7jN7ErjJjMesJTGWN6jSKCBNBbAYMigFR4Qob3ongBEGl/hgAAAIAAAAAAAQAAAAIAAAAEAAAACAAYIOZYuG9AjCyQkQBEpVF931nNO3jNd6B5LLC49fdzLHwVZows0AELQFVgLBH4koYZ30RAGlTaJyz3eEAaFE4Q5U/UpIl7lIXvseRmfCdkmOuI8nOGXZV6NwRomwjM+IQjz0FNeeYIJucy2/UADEl/yjznIFJmKKCPInOtDEOtvirZAlZBf+cAVEM+0eLYc1lrMIXWmHlNz20+G6N4ZhpClLBtxWc15ZMTGP71nABWZK/6YEtpU/EdNBrQQWZfXlpUPBvjx2+mwRex8Y8SsCS4pwSw0nJDNuOb0Ex61+YgKxoRqDmAkPSnzPNKBiIoKoKP7MGIeA/WWAEHoXYdYuWK48G5bKz3RzRa8zjn2KPCgXhz27fs5yzDqjZkgYbhOh48RYUVMAF7b26vaNZUm7JAB+cAgGFihWFfBRAAAAAgAAAAQAAAAIAAAAAAAQAAMJoEEHqyLQAAPSUAsXruHu37DTJMeLxX92C7z1MWAIBMBKBO0zXPortR3qcq8gG2swhEnAVwr5S9NUF6XQ4lJ5jdjVaFEyMuDwBeMFcCnpbXfzbur2i/HyO896ImJ5YTn2M7J/Qe0H5rAmLzi8JBIPpptaMmDwDBFoDo3eeloqohgDDf1Uk+6/J3ihWgeqxH5PPKEWY9cDjg40oqDsO8Vl71z+I6XoZVh/y1Mj79HSr4yvMI+b2WK0UeAIIsgP3yqh/ceb/2+YLswfcTeiDx3G1s51TbeTmEOCyHAuL7BxvM06sqCKQgEskrjWthvIMRFFi/vsqTxFS8k4b8JMOyWqBBC+CSvN5Z8eydxnOuIcCS1ogFEWzL6+mGen6F6yxhQl43mPsMRPHsD7Pdo9H1dzxHkL9OyutXJe8bv/4eavwAEGQBbEszfY7t/PW3pP12VPbqK/I5quIWsucXcwD7yvAoCzujnoqTZfii4/e1EZYHAC+Y+wHcKsfpasx/Qfb8BzTT/qInAajvaiOIZYIpq5vstisAABGo2g/golTy+9jOX4FK8c/Le9QZ/Lq9AAoP+aorAACJhwA6CRxG0QDA6AO+AAAAAgAAAAQAAAAIAACA4RLAHWxnDYDpiLIkf3Mht/xb2M5KQ1P+g/K3UZfve/0NXb5VmOsA7i7DGYeMePCBit9yy99Vhocc8sfL8PCIyve9/oYu3xrUOgCdAAQ7fYoo/44yfNrCfDnlf6oMnyXK/3QZHh8x+b7X39DlsxCAPgQ4YrMQmH3xzRHivSq45I+zqxcPHXfI31mRfkZ89k6P9PvIFy3Ff8Qjfmr9FQnkWab411j1sWi+8RcJ2n8RqCutDQF4TaI5oXJ9HXfq5AvNJGaaycs948+V/lTxp0h/1Tuajr8gposFps8n/iKg/Yjff1X7/tGA9mc+Kzxkn3KQY6sWAPVosMJTSaoyR5XnxliXMjZ2pYdHKHcIScRWcJE4/b7vSJl2lqD9hKSfW0jABx8xCMAHb5DKblP+zmCMdRcvVnxuAzyBhZBKgXIrcEgHkKL8U8Qb60D27kA5ofxPd1356wggtgJ4ogZ4VF6X2O5fKzENKpQEeCISaav8eKTyxhIgz0AaLgskxIL6aGD+n9JIoLPKL6BPAp71kDtLvBcqr8b+izLo96rkH/ZQpoeJ93gCeRYhzzzkz3oQCqX+eEL5ttuPWX88g/xTNcp/tosEcMFDAS4Q71XBJf+QpQE+5JD/hIcCfoJ4jyWQ5y3Ff8Ejfmr98QTyLEP83EFATcvrJMAidaU1AhD/Kx8nyBxnV/8H3QV58b/qKYL8KWb/D7bv8n2vv6HLZycAZVrOsZ39/E18SP72sMM0zSkvVliJRRYfs/z2MfnbAyMs3/f6G7p8+5MlxlJgAAAGANtKQAAABgYQAACAAAAAAAEAAAACAAAABAAAwIjD5QtgA2V9O+Tj5AEgKwEIjFvuXfZ4d255AAASDAFyKt3lBGnI2ePiSDOg1wQQq4CXtRCCcSk7nkkBq7aEokI/1BQAekcA4zXmOFWBxyPkL2skEKqAjMXtI8BZvg0pACD7EGA8Y/rGE6QhZ+/LO5AGAKjEWINj/9zyAAAEEkDKDS0hDwA9HgIAAAACAAAABAAAAAgAAAAQAAAAIAAAAEAAAAVTDAuDgB4QQBdWsYm4Z7TrKCj/Rhmm0fSArhPAtHFFzx1Hhrryb6I4ga4TwIZxHbryx/TcUH5gEBYA13rKumvTwwZXCFXejQTKjzkAoDMYc1gAhXaloCBem8RsQz1/iAVgU37MAQC9IIBQC2DaojTmtUkzeNWDqHyV34cM65QfwwAAFkCHLQAoPwALIMAC6ALWEryjSeWf8nwfAPTGAhgVhFouVOXHHAAACyBAEbsO08x3fQeAvD0d5/hHCgCGCvgCAMCA8f8CDABatG6NN+gY2wAAAABJRU5ErkJggg==');
193
- }
194
- .admin-color-light .ui-datepicker th {
195
- border-color: #fff;
196
- }
197
- .admin-color-light .ui-datepicker .ui-datepicker-title,
198
- .admin-color-light .ui-datepicker td .ui-state-default,
199
- .admin-color-light .ui-datepicker th {
200
- color: #555;
201
- }
202
- .admin-color-light .ui-datepicker td .ui-state-active,
203
- .admin-color-light .ui-datepicker td .ui-state-hover {
204
- color: #fff;
205
- background: #888;
206
- }
207
- .admin-color-light .ui-datepicker td.ui-state-disabled,
208
- .admin-color-light .ui-datepicker td.ui-state-disabled .ui-state-default {
209
- color: #ccc;
210
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/deactivate.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- require_once( dirname( __FILE__ ) . '/utilities.php' );
4
-
5
- ITSEC_Away_Mode_Utilities::delete_active_file();
 
 
 
 
 
core/modules/away-mode/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/away-mode/js/admin-away-mode.js DELETED
@@ -1,33 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( "#itsec_away_mode_end_date, #itsec_away_mode_start_date" ).datepicker();
4
-
5
- jQuery( "#itsec_away_mode_enabled" ).change(function () {
6
-
7
- if ( jQuery( "#itsec_away_mode_enabled" ).is( ':checked' ) ) {
8
-
9
- jQuery( "#away_mode-settings" ).show();
10
-
11
- } else {
12
-
13
- jQuery( "#away_mode-settings" ).hide();
14
-
15
- }
16
-
17
- } ).change();
18
-
19
- jQuery( "#itsec_away_mode_type" ).change(function () {
20
-
21
- if ( jQuery( "#itsec_away_mode_type" ).val() == "2" ) {
22
-
23
- jQuery( ".end_date_field, .start_date_field" ).closest( "tr" ).show();
24
-
25
- } else {
26
-
27
- jQuery( ".end_date_field, .start_date_field" ).closest( "tr" ).hide();
28
-
29
- }
30
-
31
- } ).change();
32
-
33
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/away-mode/js/settings-page.js DELETED
@@ -1,25 +0,0 @@
1
- (function( $ ) {
2
- var ithemesSecurityAwayModeSettingsPage = {
3
- init: function() {
4
- $( '#itsec-away-mode-start_date, #itsec-away-mode-end_date' ).datepicker({
5
- dateFormat: 'yy-mm-dd'
6
- });
7
-
8
- $( '#itsec-away-mode-type' ).change( ithemesSecurityAwayModeSettingsPage.typeChanged );
9
- },
10
- typeChanged: function() {
11
- if ( 'daily' === $( '#itsec-away-mode-type' ).val() ) {
12
- $( '#itsec-away-mode-start_date, #itsec-away-mode-end_date' ).closest( 'tr' ).hide();
13
- } else {
14
- $( '#itsec-away-mode-start_date, #itsec-away-mode-end_date' ).closest( 'tr' ).show();
15
- }
16
- }
17
- };
18
-
19
- $(document).ready(function() {
20
- ithemesSecurityAwayModeSettingsPage.init();
21
- ithemesSecurityAwayModeSettingsPage.typeChanged();
22
-
23
- itsecSettingsPage.events.on( 'modulesReloaded', ithemesSecurityAwayModeSettingsPage.init );
24
- });
25
- })( jQuery );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/labels.php DELETED
@@ -1,5 +0,0 @@
1
- <?php
2
-
3
- return [
4
- 'title' => __( 'Away Mode', 'better-wp-security' ),
5
- ];
 
 
 
 
 
core/modules/away-mode/logs.php DELETED
@@ -1,18 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Away_Mode_Logs {
4
- public function __construct() {
5
- add_filter( 'itsec_logs_prepare_away_mode_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
- }
7
-
8
- public function filter_entry_for_list_display( $entry ) {
9
- $entry['module_display'] = esc_html__( 'Away Mode', 'better-wp-security' );
10
-
11
- if ( 'away-mode-active' === $entry['code'] ) {
12
- $entry['description'] = esc_html__( 'Access Blocked', 'better-wp-security' );
13
- }
14
-
15
- return $entry;
16
- }
17
- }
18
- new ITSEC_Away_Mode_Logs();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/settings-page.php DELETED
@@ -1,176 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Away_Mode_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $version = 2;
5
-
6
-
7
- public function __construct() {
8
- $this->id = 'away-mode';
9
- $this->title = __( 'Away Mode', 'better-wp-security' );
10
- $this->description = __( 'Disable access to the WordPress Dashboard on a schedule.', 'better-wp-security' );
11
- $this->type = 'recommended';
12
-
13
- parent::__construct();
14
- }
15
-
16
- public function enqueue_scripts_and_styles() {
17
- wp_enqueue_script( 'itsec-away-mode-settings-page-script', plugins_url( 'js/settings-page.js', __FILE__ ), array( 'jquery-ui-datepicker' ), $this->version, true );
18
-
19
- wp_enqueue_style( 'itsec-jquery-ui', plugins_url( 'css/jquery-ui.min.css', __FILE__ ), array(), '1.11.4' );
20
- wp_enqueue_style( 'itsec-jquery-ui-datepicker', plugins_url( 'css/jquery.datepicker.css', __FILE__ ), array( 'itsec-jquery-ui' ), '2014.03.27' );
21
- }
22
-
23
- protected function render_description( $form ) {
24
-
25
- ?>
26
- <p><?php _e( 'As most sites are only updated at certain times of the day it is not always necessary to provide access to the WordPress dashboard 24 hours a day, 7 days a week. The options below will allow you to disable access to the WordPress Dashboard for the specified period. In addition to limiting exposure to attackers this could also be useful to disable site access based on a schedule for classroom or other reasons.', 'better-wp-security' ); ?></p>
27
- <?php
28
-
29
- }
30
-
31
- private function set_datetime_options( $form, $prefix, $has_meridiems ) {
32
- $timestamp = $form->get_option( $prefix );
33
- $timestamp += ITSEC_Core::get_time_offset();
34
-
35
- $form->set_option( "{$prefix}_date", date( 'Y-m-d', $timestamp ) );
36
-
37
- if ( $has_meridiems ) {
38
- $form->set_option( "{$prefix}_hour", intval( date( 'g', $timestamp ) ) );
39
- $form->set_option( "{$prefix}_meridiem", date( 'a', $timestamp ) );
40
- } else {
41
- $form->set_option( "{$prefix}_hour", intval( date( 'G', $timestamp ) ) );
42
- }
43
-
44
- $form->set_option( "{$prefix}_minute", intval( date( 'i', $timestamp ) ) );
45
- }
46
-
47
- protected function render_settings( $form ) {
48
- global $wp_locale;
49
-
50
-
51
- $settings = $form->get_options();
52
- $validator = ITSEC_Modules::get_validator( $this->id );
53
-
54
-
55
- $types = $validator->get_valid_types();
56
-
57
-
58
- if ( 1 === $settings['start'] ) {
59
- $tomorrow = date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS );
60
- $new_start = strtotime( "$tomorrow 1:00 am" ) - ITSEC_Core::get_time_offset();
61
-
62
- $form->set_option( 'start', $new_start );
63
- }
64
-
65
- if ( 1 === $settings['end'] ) {
66
- $tomorrow = date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS );
67
- $new_end = strtotime( "$tomorrow 6:00 am" ) - ITSEC_Core::get_time_offset();
68
-
69
- $form->set_option( 'end', $new_end );
70
- }
71
-
72
-
73
- $date_format = get_option( 'date_format' );
74
- $time_format = get_option( 'time_format' );
75
-
76
- if ( false !== strpos( $time_format, 'G' ) ) {
77
- for ( $hour = 0; $hour < 24; $hour++ ) {
78
- $hours[$hour] = $hour;
79
- }
80
- } else if ( false !== strpos( $time_format, 'H' ) ) {
81
- for ( $hour = 0; $hour < 24; $hour++ ) {
82
- $hours[$hour] = sprintf( '%02d', $hour );
83
- }
84
- } else {
85
- for ( $hour = 1; $hour <= 12; $hour++ ) {
86
- $hours[$hour] = $hour;
87
- }
88
-
89
- if ( false !== strpos( $time_format, 'A' ) ) {
90
- $am = $wp_locale->get_meridiem( 'AM' );
91
- $pm = $wp_locale->get_meridiem( 'PM' );
92
- } else {
93
- $am = $wp_locale->get_meridiem( 'am' );
94
- $pm = $wp_locale->get_meridiem( 'pm' );
95
- }
96
-
97
- $meridiems = array(
98
- 'am' => $am,
99
- 'pm' => $pm,
100
- );
101
- }
102
-
103
- for ( $minute = 0; $minute <= 59; $minute++ ) {
104
- $minutes[$minute] = sprintf( '%02d', $minute );
105
- }
106
-
107
-
108
- $this->set_datetime_options( $form, 'start', isset( $meridiems ) );
109
- $this->set_datetime_options( $form, 'end', isset( $meridiems ) );
110
-
111
-
112
- /* translators: 1: date, 2: time */
113
- $datetime_format = _x( '%1$s \a\t %2$s', 'Date and time format', 'better-wp-security' );
114
- $datetime_format = sprintf( $datetime_format, $date_format, $time_format );
115
-
116
- $current_datetime = date_i18n( $datetime_format );
117
-
118
- ?>
119
- <p><?php printf( __( 'Please note that according to your <a href="%s">WordPress Timezone settings</a> your current time is:', 'better-wp-security' ), admin_url( 'options-general.php#timezone_string' ) ); ?></p>
120
- <p class="current-date-time"><?php echo $current_datetime; ?></p>
121
- <p><?php printf( __( 'If this is incorrect, please update it on the <a href="%s">WordPress General Settings page</a> by selecting the appropriate time zone. Failure to set the correct timezone may result in unintended lockouts.', 'better-wp-security' ), admin_url( 'options-general.php#timezone_string' ) ); ?></p>
122
- <table class="form-table itsec-settings-section">
123
- <tr>
124
- <th scope="row"><label for="itsec-away-mode-type"><?php _e( 'Type of Restriction', 'better-wp-security' ); ?></label></th>
125
- <td>
126
- <?php $form->add_select( 'type', $types ); ?>
127
- <br />
128
- <p class="description"><?php _e( 'Select the type of restriction you would like to enable.', 'better-wp-security' ); ?></p>
129
- </td>
130
- </tr>
131
- <tr>
132
- <th scope="row"><label for="itsec-away-mode-start_date"><?php _e( 'Start Date', 'better-wp-security' ); ?></label></th>
133
- <td>
134
- <?php $form->add_text( 'start_date' ); ?>
135
- <br />
136
- <p class="description"><?php _e( 'Date when the admin dashboard should become unavailable.', 'better-wp-security' ); ?></p>
137
- </td>
138
- </tr>
139
- <tr>
140
- <th scope="row"><label for="itsec-away-mode-start_hour"><?php _e( 'Start Time', 'better-wp-security' ); ?></label></th>
141
- <td>
142
- <?php $form->add_select( 'start_hour', $hours ); ?>
143
- <?php $form->add_select( 'start_minute', $minutes ); ?>
144
- <?php if ( isset( $meridiems ) ) : ?>
145
- <?php $form->add_select( 'start_meridiem', $meridiems ); ?>
146
- <?php endif; ?>
147
- <br />
148
- <p class="description"><?php _e( 'Time when the admin dashboard should become unavailable.', 'better-wp-security' ); ?></p>
149
- </td>
150
- </tr>
151
- <tr>
152
- <th scope="row"><label for="itsec-away-mode-end_date"><?php _e( 'End Date', 'better-wp-security' ); ?></label></th>
153
- <td>
154
- <?php $form->add_text( 'end_date' ); ?>
155
- <br />
156
- <p class="description"><?php _e( 'Date when the admin dashboard should become available again.', 'better-wp-security' ); ?></p>
157
- </td>
158
- </tr>
159
- <tr>
160
- <th scope="row"><label for="itsec-away-mode-end_hour"><?php _e( 'End Time', 'better-wp-security' ); ?></label></th>
161
- <td>
162
- <?php $form->add_select( 'end_hour', $hours ); ?>
163
- <?php $form->add_select( 'end_minute', $minutes ); ?>
164
- <?php if ( isset( $meridiems ) ) : ?>
165
- <?php $form->add_select( 'end_meridiem', $meridiems ); ?>
166
- <?php endif; ?>
167
- <p class="description"><?php _e( 'Time when the admin dashboard should become available again.', 'better-wp-security' ); ?></p>
168
- </td>
169
- </tr>
170
- </table>
171
- <?php
172
-
173
- }
174
- }
175
-
176
- new ITSEC_Away_Mode_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/settings.php DELETED
@@ -1,27 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Away_Mode_Settings extends ITSEC_Settings {
4
- public function get_id() {
5
- return 'away-mode';
6
- }
7
-
8
- public function get_defaults() {
9
- return array(
10
- 'type' => 'daily',
11
- 'start' => 1,
12
- 'start_time' => 100000,
13
- 'end' => 1,
14
- 'end_time' => 100000,
15
- 'override_type' => '',
16
- 'override_end' => 0,
17
- );
18
- }
19
-
20
- protected function after_save() {
21
- require_once( dirname( __FILE__ ) . '/utilities.php' );
22
-
23
- ITSEC_Away_Mode_Utilities::create_active_file();
24
- }
25
- }
26
-
27
- ITSEC_Modules::register_settings( new ITSEC_Away_Mode_Settings() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/setup.php DELETED
@@ -1,154 +0,0 @@
1
- <?php
2
-
3
- if ( ! class_exists( 'ITSEC_Away_Mode_Setup' ) ) {
4
-
5
- class ITSEC_Away_Mode_Setup {
6
-
7
- public function __construct() {
8
-
9
- add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
10
- add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
11
- add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
12
- add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
13
-
14
- }
15
-
16
- /**
17
- * Execute module activation.
18
- *
19
- * @since 4.0
20
- *
21
- * @return void
22
- */
23
- public function execute_activate() {
24
- }
25
-
26
- /**
27
- * Execute module deactivation
28
- *
29
- * @return void
30
- */
31
- public function execute_deactivate() {
32
-
33
- delete_site_option( 'itsec_away_mode_sync_override' );
34
- delete_site_transient( 'itsec_away' );
35
- delete_site_transient( 'itsec_away_mode' );
36
-
37
- }
38
-
39
- /**
40
- * Execute module uninstall
41
- *
42
- * @return void
43
- */
44
- public function execute_uninstall() {
45
-
46
- $this->execute_deactivate();
47
-
48
- delete_site_option( 'itsec_away_mode' );
49
-
50
- }
51
-
52
- /**
53
- * Execute module upgrade
54
- *
55
- * @return void
56
- */
57
- public function execute_upgrade( $itsec_old_version ) {
58
-
59
- if ( $itsec_old_version < 4000 ) {
60
-
61
- global $itsec_bwps_options;
62
-
63
- $current_options = get_site_option( 'itsec_away_mode' );
64
- $current_time = ITSEC_Core::get_current_time();
65
-
66
- // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
67
- if ( false !== $current_options ) {
68
- $current_options['enabled'] = isset( $itsec_bwps_options['am_enabled'] ) && $itsec_bwps_options['am_enabled'] == 1 ? true : false;
69
- $current_options['type'] = isset( $itsec_bwps_options['am_type'] ) && $itsec_bwps_options['am_type'] == 1 ? 1 : 2;
70
-
71
- if ( isset( $itsec_bwps_options['am_startdate'] ) && isset( $itsec_bwps_options['am_starttime'] ) ) {
72
-
73
- $current_options['start'] = strtotime( date( 'Y-m-d', $itsec_bwps_options['am_startdate'] ) ) + intval( $itsec_bwps_options['am_starttime'] );
74
-
75
- } elseif ( isset( $current_options['am_starttime'] ) && $current_options['type'] == 1 ) {
76
-
77
- $current_options['start'] = strtotime( date( 'Y-m-d', $current_time ) ) + intval( $itsec_bwps_options['am_starttime'] );
78
-
79
- } else {
80
-
81
- $current_options['enabled'] = false; //didn't have the whole start picture so disable
82
-
83
- }
84
-
85
- if ( isset( $itsec_bwps_options['am_enddate'] ) && isset( $itsec_bwps_options['am_endtime'] ) ) {
86
-
87
- $current_options['end'] = strtotime( date( 'Y-m-d', $itsec_bwps_options['am_enddate'] ) ) + intval( $itsec_bwps_options['am_endtime'] );
88
-
89
- } elseif ( isset( $itsec_bwps_options['am_endtime'] ) && $itsec_bwps_options['type'] == 1 ) {
90
-
91
- $current_options['end'] = strtotime( date( 'Y-m-d', $current_time ) ) + intval( $itsec_bwps_options['am_endtime'] );
92
-
93
- } else {
94
-
95
- $current_options['enabled'] = false; //didn't have the whole start picture so disable
96
-
97
- }
98
-
99
- update_site_option( 'itsec_away_mode', $current_options );
100
-
101
- $away_file = ITSEC_Core::get_storage_dir() . '/itsec_away.confg'; //override file
102
-
103
- if ( $current_options['enabled'] === true && ! file_exists( $away_file ) ) {
104
-
105
- @file_put_contents( $away_file, 'true' );
106
-
107
- } else {
108
-
109
- @unlink( $away_file );
110
-
111
- }
112
-
113
- }
114
- }
115
-
116
- if ( $itsec_old_version < 4041 ) {
117
- $current_options = get_site_option( 'itsec_away_mode' );
118
- $current_override_options = get_site_option( 'itsec_away_mode_sync_override' );
119
-
120
- // If there are no current options, go with the new defaults by not saving anything
121
- if ( is_array( $current_options ) || is_array( $current_override_options ) ) {
122
- $settings = ITSEC_Modules::get_defaults( 'away-mode' );
123
- $original_settings = $settings;
124
-
125
- if ( is_array( $current_options ) ) {
126
- $settings['type'] = ( 1 == $current_options['type'] )? 'daily' : 'one-time';
127
- $settings['start'] = intval( $current_options['start'] - ITSEC_Core::get_time_offset() );
128
- $settings['start_time'] = $current_options['start'] - strtotime( date( 'Y-m-d', $current_options['start'] ) );
129
- $settings['end'] = intval( $current_options['end'] - ITSEC_Core::get_time_offset() );
130
- $settings['end_time'] = $current_options['end'] - strtotime( date( 'Y-m-d', $current_options['end'] ) );
131
- }
132
-
133
- if ( is_array( $current_override_options ) ) {
134
- $settings['override_type'] = $current_override_options['intention'];
135
- $settings['override_end'] = $current_override_options['expires'];
136
- }
137
-
138
- ITSEC_Modules::set_settings( 'away-mode', $settings );
139
-
140
- if ( isset( $current_options['enabled'] ) && $current_options['enabled'] ) {
141
- ITSEC_Modules::activate( 'away-mode' );
142
- } else {
143
- ITSEC_Modules::deactivate( 'away-mode' );
144
- }
145
- }
146
- }
147
-
148
- }
149
-
150
- }
151
-
152
- }
153
-
154
- new ITSEC_Away_Mode_Setup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/sync-verbs/itsec-get-away-mode.php DELETED
@@ -1,28 +0,0 @@
1
- <?php
2
-
3
- class Ithemes_Sync_Verb_ITSEC_Get_Away_Mode extends Ithemes_Sync_Verb {
4
- public static $name = 'itsec-get-away-mode';
5
- public static $description = 'Retrieve current away mode status.';
6
-
7
- public $default_arguments = array();
8
-
9
- public function run( $arguments ) {
10
- $details = ITSEC_Away_Mode::is_active( true );
11
-
12
- $response = array(
13
- 'api' => '1',
14
- 'enabled' => $details['active'],
15
- 'next' => $details['next'],
16
- 'details' => $details,
17
- );
18
-
19
- // For backwards compatibility with the api version 0 functionality, the next value represents either next or
20
- // remaining depending on the context. The context when this occurs is when away mode is either active or has
21
- // an active override, but not both.
22
- if ( $details['active'] xor $details['override_active'] ) {
23
- $response['next'] = $details['remaining'];
24
- }
25
-
26
- return $response;
27
- }
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/sync-verbs/itsec-override-away-mode.php DELETED
@@ -1,96 +0,0 @@
1
- <?php
2
-
3
- class Ithemes_Sync_Verb_ITSEC_Override_Away_Mode extends Ithemes_Sync_Verb {
4
- public static $name = 'itsec-override-away-mode';
5
- public static $description = 'Override current away mode status.';
6
-
7
- public $default_arguments = array(
8
- 'intention' => '',
9
- );
10
-
11
- public function run( $arguments ) {
12
- $arguments = Ithemes_Sync_Functions::merge_defaults( $arguments, $this->default_arguments );
13
-
14
-
15
- $details = ITSEC_Away_Mode::is_active( true );
16
- $settings = ITSEC_Modules::get_settings( 'away-mode' );
17
- $defaults = ITSEC_Modules::get_defaults( 'away-mode' );
18
-
19
- $errors = array();
20
-
21
-
22
- if ( 'activate' === $arguments['intention'] ) {
23
- if ( $details['active'] ) {
24
- $action = 'stayed-active';
25
- $success = true;
26
- } else if ( $details['override_active'] && 'deactivate' === $details['override_type'] ) {
27
- $action = 'removed-deactivate-override';
28
-
29
- $settings['override_type'] = $defaults['override_type'];
30
- $settings['override_end'] = $defaults['override_end'];
31
- } else if ( false === $details['next'] ) {
32
- $action = 'denied-activate';
33
- $errors[] = new WP_Error( 'itsec-sync-verb-itsec-override-away-mode-cannot-override-activate-expired-one-time', __( 'iThemes Security received a request to modify the override behavior of the Away Mode module. However, the request is invalid as the module is configured for a one-time lockout that occurred in the past. Allowing an activate override would result in an unending Away Mode lockout.', 'better-wp-security' ) );
34
- $success = false;
35
- } else {
36
- $action = 'added-activate-override';
37
-
38
- $settings['override_type'] = 'activate';
39
- $settings['override_end'] = ITSEC_Core::get_current_time() + $details['next'];
40
- }
41
- } else if ( 'deactivate' === $arguments['intention'] ) {
42
- if ( ! $details['active'] ) {
43
- $action = 'stayed-inactive';
44
- $success = true;
45
- } else if ( $details['override_active'] && 'activate' === $details['override_type'] ) {
46
- $action = 'removed-activate-override';
47
-
48
- $settings['override_type'] = $defaults['override_type'];
49
- $settings['override_end'] = $defaults['override_end'];
50
- } else {
51
- $action = 'added-deactivate-override';
52
-
53
- $settings['override_type'] = 'deactivate';
54
- $settings['override_end'] = ITSEC_Core::get_current_time() + $details['remaining'];
55
- }
56
- } else if ( empty( $arguments['intention'] ) ) {
57
- $action = 'missing-intention';
58
- $errors[] = new WP_Error( 'itsec-sync-verb-itsec-override-away-mode-missing-intention', __( 'iThemes Security received a request to modify the override behavior of the Away Mode module. However, the request is invalid as the required "intention" argument is missing.', 'better-wp-security' ) );
59
- $success = false;
60
- } else {
61
- $action = 'unknown-intention';
62
- $errors[] = new WP_Error( 'itsec-sync-verb-itsec-override-away-mode-unknown-intention', sprintf( __( 'iThemes Security received a request to modify the override behavior of the Away Mode module. However, the request is invalid as the required "intention" argument is set to an unrecognized value: "".', 'better-wp-security' ), $arguments['intention'] ) );
63
- $success = false;
64
- }
65
-
66
- if ( ! isset( $success ) ) {
67
- ITSEC_Core::set_interactive( false );
68
- $results = ITSEC_Modules::set_settings( 'away-mode', $settings );
69
-
70
- if ( $results['saved'] ) {
71
- $success = true;
72
- } else {
73
- $errors = $results['errors'];
74
- $success = false;
75
- }
76
- }
77
-
78
-
79
- if ( $success ) {
80
- $status = "{$arguments['intention']}d";
81
- } else {
82
- $status = 'error';
83
- }
84
-
85
- $response = array(
86
- 'api' => '1',
87
- 'status' => $status,
88
- 'action' => $action,
89
- 'errors' => $errors,
90
- 'details' => ITSEC_Away_Mode::is_active( true ),
91
- );
92
-
93
-
94
- return $response;
95
- }
96
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/utilities.php DELETED
@@ -1,164 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Away_Mode_Utilities {
4
-
5
- /**
6
- * Check if the config file signaling away mode is active exists.
7
- *
8
- * @return bool
9
- */
10
- public static function has_active_file() {
11
- if ( @file_exists( self::get_active_file_name() ) ) {
12
- return true;
13
- } else {
14
- return false;
15
- }
16
- }
17
-
18
- /**
19
- * Create a config file specifying that away mode is active.
20
- *
21
- * @return bool
22
- */
23
- public static function create_active_file() {
24
- if ( self::has_active_file() ) {
25
- return true;
26
- }
27
-
28
- $file = self::get_active_file_name();
29
-
30
- $result = @file_put_contents( $file, 'true' );
31
-
32
- if ( false === $result ) {
33
- return false;
34
- } else {
35
- return true;
36
- }
37
- }
38
-
39
- /**
40
- * Delete the config file specifying that away mode is active.
41
- *
42
- * @return bool
43
- */
44
- public static function delete_active_file() {
45
- if ( ! self::has_active_file() ) {
46
- return true;
47
- }
48
-
49
- $file = self::get_active_file_name();
50
-
51
- return @unlink( $file );
52
- }
53
-
54
- /**
55
- * Get the file name for the config file specifying that away mode is active,
56
- *
57
- * @return string
58
- */
59
- public static function get_active_file_name() {
60
- $file_name = apply_filters( 'itsec_filer_away_mode_active_file', ITSEC_Core::get_storage_dir() . '/itsec_away.confg' );
61
-
62
- return $file_name;
63
- }
64
-
65
- /**
66
- * Check if the current UTC time falls between the two specified times, inclusive.
67
- *
68
- * @param int $start The UTC timestamp signalling the beginning of the active window.
69
- * @param int $end The UTC timestamp signalling the end of the active window.
70
- * @param bool $include_details Whether to include additional details about the active window.
71
- *
72
- * @return array|bool
73
- */
74
- public static function is_current_timestamp_active( $start, $end, $include_details = false ) {
75
- $now = ITSEC_Core::get_current_time_gmt();
76
-
77
- $active = false;
78
-
79
- if ( $start <= $now && $now <= $end ) {
80
- $active = true;
81
- }
82
-
83
- if ( ! $include_details ) {
84
- return $active;
85
- }
86
-
87
-
88
- if ( $start > $end ) {
89
- $remaining = false;
90
- $next = false;
91
- $length = false;
92
-
93
- /* translators: 1: start timestamp, 2: end timestamp */
94
- $error = new WP_Error( 'itsec-away-mode-is-current-timestamp-in-range-start-after-end', sprintf( __( 'The supplied data is invalid. The supplied start (%1$s) is after the supplied end (%2$s).', 'better-wp-security' ), $start, $end ) );
95
- } else {
96
- $remaining = $end - $now;
97
- $next = $start - $now;
98
- $length = $end - $start;
99
- $error = false;
100
-
101
- if ( $now < $start ) {
102
- $remaining = false;
103
- }
104
-
105
- if ( $next < 0 ) {
106
- $next = false;
107
- }
108
- }
109
-
110
- return compact( 'active', 'remaining', 'next', 'length', 'error' );
111
- }
112
-
113
- /**
114
- * Check if the current local time falls between the two specified times, inclusive.
115
- *
116
- * @param int $start The local timestamp signalling the beginning of the active window.
117
- * @param int $end The local timestamp signalling the end of the active window.
118
- * @param bool $include_details Whether to include additional details about the active window.
119
- *
120
- * @return array|bool
121
- */
122
- public static function is_current_time_active( $start, $end, $include_details = false ) {
123
- $current_time = ITSEC_Core::get_current_time();
124
- $now = $current_time - strtotime( date( 'Y-m-d', $current_time ) );
125
-
126
- $active = false;
127
-
128
- if ( $start <= $end ) {
129
- if ( $start <= $now && $now <= $end ) {
130
- $active = true;
131
- }
132
- } else {
133
- if ( $start <= $now || $now <= $end ) {
134
- $active = true;
135
- }
136
- }
137
-
138
- if ( ! $include_details ) {
139
- return $active;
140
- }
141
-
142
-
143
- $remaining = $end - $now;
144
- $next = $start - $now;
145
- $length = $end - $start;
146
-
147
- if ( $active && $remaining < 0 ) {
148
- $remaining += DAY_IN_SECONDS;
149
- } else if ( ! $active && $remaining >= 0 ) {
150
- $remaining -= DAY_IN_SECONDS;
151
- }
152
-
153
- if ( $next < 0 ) {
154
- $next += DAY_IN_SECONDS;
155
- }
156
-
157
- if ( $length < 0 ) {
158
- $length += DAY_IN_SECONDS;
159
- }
160
-
161
-
162
- return compact( 'active', 'remaining', 'next', 'length' );
163
- }
164
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/away-mode/validator.php DELETED
@@ -1,135 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Away_Mode_Validator extends ITSEC_Validator {
4
- public function get_id() {
5
- return 'away-mode';
6
- }
7
-
8
- public function get_valid_types() {
9
- return array(
10
- 'daily' => __( 'Daily', 'better-wp-security' ),
11
- 'one-time' => __( 'One Time', 'better-wp-security' ),
12
- );
13
- }
14
-
15
- protected function sanitize_settings() {
16
- if ( ! isset( $this->settings['override_type'] ) ) {
17
- $this->settings['override_type'] = $this->previous_settings['override_type'];
18
- }
19
- if ( ! isset( $this->settings['override_end'] ) ) {
20
- $this->settings['override_end'] = $this->previous_settings['override_end'];
21
- }
22
-
23
-
24
- $types = array_keys( $this->get_valid_types() );
25
- $this->sanitize_setting( $types, 'type', __( 'Type of Restriction', 'better-wp-security' ) );
26
-
27
- $this->sanitize_setting( array( '', 'activate', 'deactivate' ), 'override_type', __( 'Override Type', 'better-wp-security' ) );
28
- $this->sanitize_setting( 'int', 'override_end', __( 'Override End', 'better-wp-security' ) );
29
-
30
- $this->sanitize_datetime( 'start', __( 'Start Timestamp', 'better-wp-security' ), __( 'Start Date', 'better-wp-security' ), __( 'Start Time', 'better-wp-security' ) );
31
- $this->sanitize_datetime( 'end', __( 'End Timestamp', 'better-wp-security' ), __( 'End Date', 'better-wp-security' ), __( 'End Time', 'better-wp-security' ) );
32
- }
33
-
34
- private function sanitize_datetime( $prefix, $msg_timestamp, $msg_date, $msg_time ) {
35
- $this->vars_to_skip_validate_matching_fields[] = "{$prefix}_date";
36
- $this->vars_to_skip_validate_matching_fields[] = "{$prefix}_hour";
37
- $this->vars_to_skip_validate_matching_fields[] = "{$prefix}_minute";
38
- $this->vars_to_skip_validate_matching_fields[] = "{$prefix}_meridiem";
39
- $this->vars_to_skip_validate_matching_fields[] = "{$prefix}_time";
40
-
41
-
42
- if ( isset( $this->settings[$prefix] ) ) {
43
- $this->sanitize_setting( 'positive-int', $prefix, $msg_timestamp );
44
- return;
45
- }
46
-
47
-
48
- $valid_date = $this->sanitize_setting( 'date', "{$prefix}_date", $msg_date );
49
-
50
- if ( isset( $this->settings["{$prefix}_meridiem"] ) ) {
51
- if ( $this->sanitize_setting( array( 'am', 'pm' ), "{$prefix}_meridiem", $msg_time ) ) {
52
- $meridiem = $this->settings["{$prefix}_meridiem"];
53
- } else {
54
- $meridiem = false;
55
- }
56
-
57
- $valid_hours = range( 1, 12 );
58
- } else {
59
- $meridiem = '';
60
- $valid_hours = range( 0, 23 );
61
- }
62
-
63
- $valid_hour = $this->sanitize_setting( 'positive-int', "{$prefix}_hour", $msg_time ) && $this->sanitize_setting( $valid_hours, "{$prefix}_hour", $msg_time );
64
- $valid_minute = $this->sanitize_setting( 'positive-int', "{$prefix}_minute", $msg_time ) && $this->sanitize_setting( range( 0, 59 ), "{$prefix}_minute", $msg_time );
65
-
66
-
67
- if ( $valid_date && $valid_hour && $valid_minute && false !== $meridiem ) {
68
- $datetime = $this->settings["{$prefix}_date"] . ' ';
69
- $datetime .= sprintf( '%d:%02d %s', $this->settings["{$prefix}_hour"], $this->settings["{$prefix}_minute"], $meridiem );
70
- $datetime = trim( $datetime );
71
-
72
- $timestamp = strtotime( $datetime );
73
-
74
- if ( false === $timestamp ) {
75
- $id = $this->get_id();
76
-
77
- /* translators: 1: date input name, 2: time input name, 3: submitted date time */
78
- $this->add_error( new WP_Error( "itsec-validator-$id-invalid-datetime", sprintf( __( 'The %1$s and %2$s values resulted in a date and time of <code>%3$s</code>, which was unable to be processed properly. This could be an issue with PHP or a server configuration issue.', 'better-wp-security' ), $msg_date, $msg_time, $datetime ) ) );
79
-
80
- $this->vars_to_skip_validate_matching_fields[] = $prefix;
81
- } else {
82
- $this->settings[$prefix] = intval( $timestamp - ITSEC_Core::get_time_offset() );
83
- $this->settings["{$prefix}_time"] = $timestamp - strtotime( date( 'Y-m-d', $timestamp ) );
84
-
85
- unset( $this->settings["{$prefix}_date"] );
86
- unset( $this->settings["{$prefix}_hour"] );
87
- unset( $this->settings["{$prefix}_minute"] );
88
- unset( $this->settings["{$prefix}_meridiem"] );
89
- }
90
- } else {
91
- $this->vars_to_skip_validate_matching_fields[] = $prefix;
92
- }
93
- }
94
-
95
- protected function validate_settings() {
96
- // Only validate settings if the data was successfully sanitized.
97
- if ( ! $this->can_save() ) {
98
- return;
99
- }
100
-
101
-
102
- require_once( dirname( __FILE__) . '/utilities.php' );
103
-
104
-
105
- $id = $this->get_id();
106
-
107
- if ( 'one-time' === $this->settings['type'] ) {
108
- if ( $this->settings['start'] >= $this->settings['end'] ) {
109
- /* translators: 1: "Start Date", 2: "Start Time", 3: "End Date", 4: "End Time" */
110
- $this->add_error( new WP_Error( "itsec-validator-$id-start-after-end", sprintf( __( 'The %1$s and %2$s must be before the %3$s and %4$s.', 'better-wp-security' ), __( 'Start Date', 'better-wp-security' ), __( 'Start Time', 'better-wp-security' ), __( 'End Date', 'better-wp-security' ), __( 'End Time', 'better-wp-security' ) ) ) );
111
- $this->set_can_save( false );
112
- } else if ( false === ITSEC_Away_Mode_Utilities::is_current_timestamp_active( $this->settings['start'], $this->settings['end'], true ) ) {
113
- /* translators: 1: "End Date", 2: "End Time" */
114
- $this->add_error( new WP_Error( "itsec-validator-$id-end-already-ended", sprintf( __( 'The selected restriction date and time has already ended. Please select an %1$s and %2$s that has not already ended.', 'better-wp-security' ), __( 'End Date', 'better-wp-security' ), __( 'End Time', 'better-wp-security' ) ) ) );
115
- $this->set_can_save( false );
116
- } else if ( ITSEC_Core::is_interactive() && ITSEC_Away_Mode_Utilities::is_current_timestamp_active( $this->settings['start'], $this->settings['end'] ) ) {
117
- /* translators: 1: "Start Date", 2: "Start Time" */
118
- $this->add_error( new WP_Error( "itsec-validator-$id-start-already-started", sprintf( __( 'The selected restriction date and time has already started and would result in locking you out immediately. Please select a %1$s and %2$s that has not already started.', 'better-wp-security' ), __( 'Start Date', 'better-wp-security' ), __( 'Start Time', 'better-wp-security' ) ) ) );
119
- $this->set_can_save( false );
120
- }
121
- } else {
122
- if ( $this->settings['start_time'] === $this->settings['end_time'] ) {
123
- /* translators: 1: "Start Time", 2: "End Time" */
124
- $this->add_error( new WP_Error( "itsec-validator-$id-start-equals-end", sprintf( __( 'The %1$s and %2$s cannot be the same.', 'better-wp-security' ), __( 'Start Time', 'better-wp-security' ), __( 'End Time', 'better-wp-security' ) ) ) );
125
- $this->set_can_save( false );
126
- } else if ( ITSEC_Core::is_interactive() && ITSEC_Away_Mode_Utilities::is_current_time_active( $this->settings['start_time'], $this->settings['end_time'] ) ) {
127
- /* translators: 1: "Start Time", 2: "End Time" */
128
- $this->add_error( new WP_Error( "itsec-validator-$id-settings-result-in-current-lockout", sprintf( __( 'The %1$s and %2$s settings restrict the current time and would result in locking you out immediately. Please select a %1$s and %2$s that does not restrict the current time.', 'better-wp-security' ), __( 'Start Time', 'better-wp-security' ), __( 'End Time', 'better-wp-security' ) ) ) );
129
- $this->set_can_save( false );
130
- }
131
- }
132
- }
133
- }
134
-
135
- ITSEC_Modules::register_validator( new ITSEC_Away_Mode_Validator() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/activate.php DELETED
@@ -1,8 +0,0 @@
1
- <?php
2
-
3
- $settings = ITSEC_Modules::get_settings( 'backup' );
4
-
5
- if ( $settings['enabled'] && $settings['interval'] > 0 ) {
6
- ITSEC_Core::get_scheduler()->register_custom_schedule( 'backup', DAY_IN_SECONDS * $settings['interval'] );
7
- ITSEC_Core::get_scheduler()->schedule( 'backup', 'backup' );
8
- }
 
 
 
 
 
 
 
 
core/modules/backup/cards/class-itsec-dashboard-card-database-backup.php ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Dashboard_Card_Database_Backup extends ITSEC_Dashboard_Card {
4
+
5
+ /**
6
+ * @inheritDoc
7
+ */
8
+ public function get_slug() {
9
+ return 'database-backup';
10
+ }
11
+
12
+ /**
13
+ * @inheritDoc
14
+ */
15
+ public function get_label() {
16
+ return __( 'Database Backups', 'better-wp-security' );
17
+ }
18
+
19
+ /**
20
+ * @inheritDoc
21
+ */
22
+ public function get_size() {
23
+ $no_data = 1 === ITSEC_Modules::get_setting( 'backup', 'method' ) && 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' );
24
+
25
+ return array(
26
+ 'minW' => 1,
27
+ 'minH' => 1,
28
+ 'maxW' => $no_data ? 1 : 2,
29
+ 'maxH' => $no_data ? 1 : 3,
30
+ 'defaultW' => $no_data ? 1 : 2,
31
+ 'defaultH' => $no_data ? 1 : 2,
32
+ );
33
+ }
34
+
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ public function query_for_data( array $query_args, array $settings ) {
39
+
40
+ $dir = trailingslashit( ITSEC_Modules::get_setting( 'backup', 'location' ) );
41
+ $method = ITSEC_Modules::get_setting( 'backup', 'method' );
42
+
43
+ if ( 'email' === $method ) {
44
+ if ( 'file' === ITSEC_Modules::get_setting( 'global', 'log_type' ) ) {
45
+ return array();
46
+ }
47
+
48
+ $logs = ITSEC_Log::get_entries( array( 'module' => 'backup' ), 100, 1, 'timestamp', 'DESC', array(
49
+ 'code',
50
+ 'data',
51
+ 'init_timestamp',
52
+ ) );
53
+
54
+ $backups = array();
55
+
56
+ foreach ( $logs as $log ) {
57
+ $size = empty( $log['data']['size'] ) ? false : $log['data']['size'];
58
+
59
+ $backups[] = array(
60
+ 'time' => ITSEC_Lib::to_rest_date( $log['init_timestamp'] ),
61
+ 'size' => $size,
62
+ 'size_format' => $size ? size_format( $size, 2 ) : __( 'unknown', 'better-wp-security' ),
63
+ 'url' => false,
64
+ );
65
+ }
66
+
67
+ return array(
68
+ 'total' => count( $backups ),
69
+ 'backups' => $backups,
70
+ 'source' => 'logs',
71
+ );
72
+ }
73
+
74
+ if ( ! $dir || ! @file_exists( $dir ) ) {
75
+ return new WP_Error( 'itsec-dashboard-card-database-backup-invalid-dir', esc_html__( 'Invalid Backups Directory', 'better-wp-security' ) );
76
+ }
77
+
78
+ $backups = array();
79
+
80
+ $files = scandir( $dir, SCANDIR_SORT_DESCENDING );
81
+ $files = array_unique( $files );
82
+
83
+ foreach ( $files as $file ) {
84
+ if ( 0 === strpos( $file, 'backup-' ) ) {
85
+ $backups[] = $this->format_backup( $file, $dir );
86
+ }
87
+ }
88
+
89
+ return array(
90
+ 'total' => count( $backups ),
91
+ 'backups' => wp_list_sort( array_slice( $backups, 0, 100 ), 'time', 'DESC' ),
92
+ 'source' => 'files',
93
+ );
94
+ }
95
+
96
+ /**
97
+ * @inheritdoc
98
+ */
99
+ public function get_links() {
100
+ return array(
101
+ array(
102
+ 'rel' => ITSEC_Lib_REST::LINK_REL . 'logs',
103
+ 'href' => ITSEC_Core::get_logs_page_url( array( 'module' => 'backup' ) ),
104
+ 'title' => __( 'View Logs', 'better-wp-security' ),
105
+ 'media' => 'text/html',
106
+ 'cap' => ITSEC_Core::get_required_cap(),
107
+ ),
108
+ array(
109
+ 'rel' => ITSEC_Lib_REST::LINK_REL . 'rpc',
110
+ 'title' => __( 'Backup Now', 'better-wp-security' ),
111
+ 'endpoint' => 'backup',
112
+ 'cap' => ITSEC_Core::get_required_cap(),
113
+ 'callback' => array( $this, 'do_backup' ),
114
+ )
115
+ );
116
+ }
117
+
118
+ public function do_backup() {
119
+ global $itsec_backup;
120
+
121
+ if ( null === $itsec_backup ) {
122
+ ITSEC_Modules::load_module_file( 'class-itsec-backup.php' );
123
+ $itsec_backup = new ITSEC_Backup();
124
+ $itsec_backup->run();
125
+ }
126
+
127
+ $result = $itsec_backup->do_backup( true );
128
+
129
+ if ( is_wp_error( $result ) ) {
130
+ return $result;
131
+ }
132
+
133
+ if ( is_array( $result ) ) {
134
+ return array(
135
+ 'message' => $result['message'],
136
+ 'backup' => array(
137
+ 'time' => ITSEC_Lib::to_rest_date(),
138
+ 'size' => $result['size'],
139
+ 'size_format' => $result['size'] ? size_format( $result['size'], 2 ) : __( 'unknown', 'better-wp-security' ),
140
+ 'url' => ITSEC_Lib::get_url_from_file( $result['output_file'] ),
141
+ ),
142
+ );
143
+ }
144
+
145
+ return new WP_Error( 'itsec-dashboard-card-backup-unexpected-response', __( 'The backup request returned an unexpected response.', 'better-wp-security' ) );
146
+ }
147
+
148
+ /**
149
+ * Format a backup file to an array.
150
+ *
151
+ * @param string $file
152
+ * @param string $dir
153
+ *
154
+ * @return array
155
+ */
156
+ private function format_backup( $file, $dir ) {
157
+
158
+ $path = trailingslashit( $dir ) . $file;
159
+ list( , $time, $day ) = array_reverse( explode( '-', $file ) );
160
+
161
+ $epoch = strtotime( $day . ' ' . $time );
162
+ $size = @filesize( $path );
163
+
164
+ return array(
165
+ 'time' => ITSEC_Lib::to_rest_date( $epoch ),
166
+ 'size' => $size,
167
+ 'size_format' => $size ? size_format( $size, 2 ) : __( 'unknown', 'better-wp-security' ),
168
+ 'url' => ITSEC_Lib::get_url_from_file( $path ),
169
+ );
170
+ }
171
+ }
core/modules/backup/cards/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/backup/class-itsec-backup.php CHANGED
@@ -45,7 +45,6 @@ class ITSEC_Backup {
45
  }
46
 
47
  ITSEC_Core::get_scheduler()->register_custom_schedule( 'backup', DAY_IN_SECONDS * $this->settings['interval'] );
48
- add_action( 'itsec_scheduler_register_events', array( $this, 'register_events' ) );
49
 
50
  if ( ! $this->settings['enabled'] || $this->settings['interval'] <= 0 ) {
51
  // Don't run when scheduled backups aren't enabled or the interval is zero or less.
@@ -69,7 +68,7 @@ class ITSEC_Backup {
69
  *
70
  * @since 4.0.0
71
  *
72
- * @param boolean $one_time whether this is a one time backup
73
  *
74
  * @return array|WP_Error false on error or nothing
75
  */
@@ -88,10 +87,10 @@ class ITSEC_Backup {
88
  }
89
 
90
  switch ( $this->settings['method'] ) {
91
- case 0:
92
  $message = __( 'Backup complete. The backup was sent to the selected email recipients and was saved locally.', 'better-wp-security' );
93
  break;
94
- case 1:
95
  $message = __( 'Backup complete. The backup was sent to the selected email recipients.', 'better-wp-security' );
96
  break;
97
  default:
@@ -120,12 +119,12 @@ class ITSEC_Backup {
120
 
121
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-directory.php' );
122
 
123
- $dir = $this->settings['location'];
124
  $result = ITSEC_Lib_Directory::create( $dir );
125
 
126
  if ( is_wp_error( $result ) ) {
127
  return $result;
128
- } else if ( ! $result ) {
129
  return new WP_Error( 'itsec-backup-failed-to-create-backup-dir', esc_html__( 'Unable to create the backup directory due to an unknown error.', 'better-wp-security' ) );
130
  }
131
 
@@ -135,17 +134,11 @@ class ITSEC_Backup {
135
  return new WP_Error( 'itsec-backup-failed-to-write-backup-file', esc_html__( 'Unable to write the backup file. This may be due to a permissions or disk space issue.', 'better-wp-security' ) );
136
  }
137
 
138
-
139
  if ( false === $one_time ) {
140
  ITSEC_Modules::set_setting( 'backup', 'last_run', ITSEC_Core::get_current_time_gmt() );
141
  }
142
 
143
-
144
- if ( $this->settings['all_sites'] ) {
145
- $tables = $wpdb->get_col( 'SHOW TABLES' );
146
- } else {
147
- $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->base_prefix . '%' ) );
148
- }
149
 
150
  $max_rows_per_query = 1000;
151
 
@@ -163,7 +156,7 @@ class ITSEC_Backup {
163
 
164
  $num_fields = count( $wpdb->get_results( "DESCRIBE `$table`;" ) );
165
 
166
- $offset = 0;
167
  $has_more_rows = true;
168
 
169
  while ( $has_more_rows ) {
@@ -173,14 +166,14 @@ class ITSEC_Backup {
173
  $sql = "INSERT INTO `$table` VALUES (";
174
 
175
  for ( $j = 0; $j < $num_fields; $j ++ ) {
176
- if ( isset( $row[$j] ) ) {
177
- $row[$j] = addslashes( $row[$j] );
178
 
179
  if ( PHP_EOL !== "\n" ) {
180
- $row[$j] = preg_replace( '#' . PHP_EOL . '#', "\n", $row[$j] );
181
  }
182
 
183
- $sql .= '"' . $row[$j] . '"';
184
  } else {
185
  $sql .= '""';
186
  }
@@ -217,8 +210,8 @@ class ITSEC_Backup {
217
  require( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
218
  }
219
 
220
- $zip_file = substr( $file, 0, -4 ) . '.zip';
221
- $pclzip = new PclZip( $zip_file );
222
 
223
  if ( 0 != $pclzip->create( $file, PCLZIP_OPT_REMOVE_PATH, $dir ) ) {
224
  @unlink( $file );
@@ -226,7 +219,7 @@ class ITSEC_Backup {
226
  }
227
  }
228
 
229
- if ( 2 !== $this->settings['method'] || true === $one_time ) {
230
  $mail_success = $this->send_mail( $file );
231
  } else {
232
  $mail_success = null;
@@ -240,9 +233,9 @@ class ITSEC_Backup {
240
  'size' => @filesize( $file ),
241
  );
242
 
243
- if ( 1 === $this->settings['method'] ) {
244
  @unlink( $file );
245
- } else if ( $this->settings['retain'] > 0 ) {
246
  $files = scandir( $dir, 1 );
247
 
248
  if ( is_array( $files ) && count( $files ) > 0 ) {
@@ -257,18 +250,18 @@ class ITSEC_Backup {
257
  @unlink( trailingslashit( $dir ) . $file );
258
  }
259
 
260
- $count++;
261
  }
262
  }
263
  }
264
 
265
- if ( 0 === $this->settings['method'] ) {
266
  if ( false === $mail_success ) {
267
  ITSEC_Log::add_warning( 'backup', 'email-failed-file-stored', $log_data );
268
  } else {
269
  ITSEC_Log::add_notice( 'backup', 'email-succeeded-file-stored', $log_data );
270
  }
271
- } else if ( 1 === $this->settings['method'] ) {
272
  if ( false === $mail_success ) {
273
  ITSEC_Log::add_error( 'backup', 'email-failed', $log_data );
274
  } else {
@@ -283,7 +276,7 @@ class ITSEC_Backup {
283
 
284
  private function send_mail( $file ) {
285
 
286
- $nc = ITSEC_Core::get_notification_center();
287
  $mail = $nc->mail();
288
 
289
  $mail->add_header( esc_html__( 'Database Backup', 'better-wp-security' ), sprintf( esc_html__( 'Site Database Backup for %s', 'better-wp-security' ), '<b>' . date_i18n( get_option( 'date_format' ) ) . '</b>' ) );
@@ -310,20 +303,6 @@ class ITSEC_Backup {
310
  return $nc->send( 'backup', $mail );
311
  }
312
 
313
- /**
314
- * Register the events.
315
- *
316
- * @param ITSEC_Scheduler $scheduler
317
- */
318
- public function register_events( $scheduler ) {
319
-
320
- $settings = ITSEC_Modules::get_settings( 'backup' );
321
-
322
- if ( $settings['enabled'] && $settings['interval'] > 0 ) {
323
- $scheduler->schedule( 'backup', 'backup' );
324
- }
325
- }
326
-
327
  /**
328
  * Register the Backup notification email.
329
  *
@@ -335,7 +314,7 @@ class ITSEC_Backup {
335
 
336
  $method = ITSEC_Modules::get_setting( 'backup', 'method' );
337
 
338
- if ( 0 === $method || 1 === $method ) {
339
  $notifications['backup'] = array(
340
  'subject_editable' => true,
341
  'recipient' => ITSEC_Notification_Center::R_EMAIL_LIST,
@@ -354,9 +333,13 @@ class ITSEC_Backup {
354
  */
355
  public function notification_strings() {
356
  return array(
357
- 'label' => esc_html__( 'Database Backup', 'better-wp-security' ),
358
- 'description' => sprintf( esc_html__( 'The %1$sDatabase Backup%2$s module will send a copy of any backups to the email addresses listed below.', 'better-wp-security' ), '<a href="#" data-module-link="backup">', '</a>' ),
359
- 'subject' => esc_html__( 'Database Backup', 'better-wp-security' ),
 
 
 
 
360
  );
361
  }
362
 
45
  }
46
 
47
  ITSEC_Core::get_scheduler()->register_custom_schedule( 'backup', DAY_IN_SECONDS * $this->settings['interval'] );
 
48
 
49
  if ( ! $this->settings['enabled'] || $this->settings['interval'] <= 0 ) {
50
  // Don't run when scheduled backups aren't enabled or the interval is zero or less.
68
  *
69
  * @since 4.0.0
70
  *
71
+ * @param boolean $one_time whether this is a one time backup
72
  *
73
  * @return array|WP_Error false on error or nothing
74
  */
87
  }
88
 
89
  switch ( $this->settings['method'] ) {
90
+ case 'both':
91
  $message = __( 'Backup complete. The backup was sent to the selected email recipients and was saved locally.', 'better-wp-security' );
92
  break;
93
+ case 'email':
94
  $message = __( 'Backup complete. The backup was sent to the selected email recipients.', 'better-wp-security' );
95
  break;
96
  default:
119
 
120
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-directory.php' );
121
 
122
+ $dir = $this->settings['location'];
123
  $result = ITSEC_Lib_Directory::create( $dir );
124
 
125
  if ( is_wp_error( $result ) ) {
126
  return $result;
127
+ } elseif ( ! $result ) {
128
  return new WP_Error( 'itsec-backup-failed-to-create-backup-dir', esc_html__( 'Unable to create the backup directory due to an unknown error.', 'better-wp-security' ) );
129
  }
130
 
134
  return new WP_Error( 'itsec-backup-failed-to-write-backup-file', esc_html__( 'Unable to write the backup file. This may be due to a permissions or disk space issue.', 'better-wp-security' ) );
135
  }
136
 
 
137
  if ( false === $one_time ) {
138
  ITSEC_Modules::set_setting( 'backup', 'last_run', ITSEC_Core::get_current_time_gmt() );
139
  }
140
 
141
+ $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->base_prefix . '%' ) );
 
 
 
 
 
142
 
143
  $max_rows_per_query = 1000;
144
 
156
 
157
  $num_fields = count( $wpdb->get_results( "DESCRIBE `$table`;" ) );
158
 
159
+ $offset = 0;
160
  $has_more_rows = true;
161
 
162
  while ( $has_more_rows ) {
166
  $sql = "INSERT INTO `$table` VALUES (";
167
 
168
  for ( $j = 0; $j < $num_fields; $j ++ ) {
169
+ if ( isset( $row[ $j ] ) ) {
170
+ $row[ $j ] = addslashes( $row[ $j ] );
171
 
172
  if ( PHP_EOL !== "\n" ) {
173
+ $row[ $j ] = preg_replace( '#' . PHP_EOL . '#', "\n", $row[ $j ] );
174
  }
175
 
176
+ $sql .= '"' . $row[ $j ] . '"';
177
  } else {
178
  $sql .= '""';
179
  }
210
  require( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
211
  }
212
 
213
+ $zip_file = substr( $file, 0, - 4 ) . '.zip';
214
+ $pclzip = new PclZip( $zip_file );
215
 
216
  if ( 0 != $pclzip->create( $file, PCLZIP_OPT_REMOVE_PATH, $dir ) ) {
217
  @unlink( $file );
219
  }
220
  }
221
 
222
+ if ( 'local' !== $this->settings['method'] || true === $one_time ) {
223
  $mail_success = $this->send_mail( $file );
224
  } else {
225
  $mail_success = null;
233
  'size' => @filesize( $file ),
234
  );
235
 
236
+ if ( 'email' === $this->settings['method'] ) {
237
  @unlink( $file );
238
+ } elseif ( $this->settings['retain'] > 0 ) {
239
  $files = scandir( $dir, 1 );
240
 
241
  if ( is_array( $files ) && count( $files ) > 0 ) {
250
  @unlink( trailingslashit( $dir ) . $file );
251
  }
252
 
253
+ $count ++;
254
  }
255
  }
256
  }
257
 
258
+ if ( 'both' === $this->settings['method'] ) {
259
  if ( false === $mail_success ) {
260
  ITSEC_Log::add_warning( 'backup', 'email-failed-file-stored', $log_data );
261
  } else {
262
  ITSEC_Log::add_notice( 'backup', 'email-succeeded-file-stored', $log_data );
263
  }
264
+ } elseif ( 'email' === $this->settings['method'] ) {
265
  if ( false === $mail_success ) {
266
  ITSEC_Log::add_error( 'backup', 'email-failed', $log_data );
267
  } else {
276
 
277
  private function send_mail( $file ) {
278
 
279
+ $nc = ITSEC_Core::get_notification_center();
280
  $mail = $nc->mail();
281
 
282
  $mail->add_header( esc_html__( 'Database Backup', 'better-wp-security' ), sprintf( esc_html__( 'Site Database Backup for %s', 'better-wp-security' ), '<b>' . date_i18n( get_option( 'date_format' ) ) . '</b>' ) );
303
  return $nc->send( 'backup', $mail );
304
  }
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  /**
307
  * Register the Backup notification email.
308
  *
314
 
315
  $method = ITSEC_Modules::get_setting( 'backup', 'method' );
316
 
317
+ if ( 'local' !== $method ) {
318
  $notifications['backup'] = array(
319
  'subject_editable' => true,
320
  'recipient' => ITSEC_Notification_Center::R_EMAIL_LIST,
333
  */
334
  public function notification_strings() {
335
  return array(
336
+ 'label' => __( 'Database Backup', 'better-wp-security' ),
337
+ 'description' => sprintf(
338
+ __( 'The %1$sDatabase Backup%2$s module will send a copy of any backups to the email addresses listed below.', 'better-wp-security' ),
339
+ ITSEC_Core::get_link_for_settings_route( ITSEC_Core::get_settings_module_route( 'backup' ) ),
340
+ '</a>'
341
+ ),
342
+ 'subject' => __( 'Database Backup', 'better-wp-security' ),
343
  );
344
  }
345
 
core/modules/backup/container.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use Pimple\Container;
4
+
5
+ return static function ( Container $c ) {
6
+ ITSEC_Lib::extend_if_able( $c, 'dashboard.cards', function ( $cards ) {
7
+ require_once __DIR__ . '/cards/class-itsec-dashboard-card-database-backup.php';
8
+
9
+ $cards[] = new ITSEC_Dashboard_Card_Database_Backup();
10
+
11
+ return $cards;
12
+ } );
13
+ };
core/modules/backup/css/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/backup/css/multi-select.css DELETED
@@ -1,94 +0,0 @@
1
- .ms-container {
2
- background : transparent url('../img/switch.png') no-repeat 50% 50%;
3
- width : 370px;
4
- }
5
-
6
- .ms-container:after {
7
- content : ".";
8
- display : block;
9
- height : 0;
10
- line-height : 0;
11
- font-size : 0;
12
- clear : both;
13
- min-height : 0;
14
- visibility : hidden;
15
- }
16
-
17
- .ms-container .ms-selectable, .ms-container .ms-selection {
18
- background : #fff;
19
- color : #555;
20
- float : left;
21
- width : 45%;
22
- }
23
-
24
- .ms-container .ms-selection {
25
- float : right;
26
- }
27
-
28
- .ms-container .ms-list {
29
- -webkit-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
30
- -moz-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
31
- box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
32
- -webkit-transition : border linear 0.2s, box-shadow linear 0.2s;
33
- -moz-transition : border linear 0.2s, box-shadow linear 0.2s;
34
- -ms-transition : border linear 0.2s, box-shadow linear 0.2s;
35
- -o-transition : border linear 0.2s, box-shadow linear 0.2s;
36
- transition : border linear 0.2s, box-shadow linear 0.2s;
37
- border : 1px solid #ccc;
38
- -webkit-border-radius : 3px;
39
- -moz-border-radius : 3px;
40
- border-radius : 3px;
41
- position : relative;
42
- height : 200px;
43
- padding : 0;
44
- overflow-y : auto;
45
- }
46
-
47
- .ms-container .ms-list.ms-focus {
48
- border-color : rgba(82, 168, 236, 0.8);
49
- -webkit-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
50
- -moz-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
51
- box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
52
- outline : 0;
53
- outline : thin dotted \9;
54
- }
55
-
56
- .ms-container ul {
57
- margin : 0;
58
- list-style-type : none;
59
- padding : 0;
60
- }
61
-
62
- .ms-container .ms-optgroup-container {
63
- width : 100%;
64
- }
65
-
66
- .ms-container .ms-optgroup-label {
67
- margin : 0;
68
- padding : 5px 0px 0px 5px;
69
- cursor : pointer;
70
- color : #999;
71
- }
72
-
73
- .ms-container .ms-selectable li.ms-elem-selectable,
74
- .ms-container .ms-selection li.ms-elem-selection {
75
- border-bottom : 1px #eee solid;
76
- padding : 2px 10px;
77
- color : #555;
78
- font-size : 14px;
79
- }
80
-
81
- .ms-container .ms-selectable li.ms-hover,
82
- .ms-container .ms-selection li.ms-hover {
83
- cursor : pointer;
84
- color : #fff;
85
- text-decoration : none;
86
- background-color : #08c;
87
- }
88
-
89
- .ms-container .ms-selectable li.disabled,
90
- .ms-container .ms-selection li.disabled {
91
- background-color : #eee;
92
- color : #aaa;
93
- cursor : text;
94
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/css/settings-page.css DELETED
@@ -1,110 +0,0 @@
1
- div.inside ul.ms-list {
2
- margin-left : 0;
3
- }
4
-
5
- .ms-container {
6
- margin-top : 10px;
7
- }
8
-
9
- .ms-container .custom-header {
10
- font-weight : bold;
11
- text-align : center;
12
- }
13
-
14
- #backupbuddy_info .bub-cta {
15
- margin : 2em .5em;
16
- }
17
-
18
- .ms-container {
19
- background : transparent url('../img/switch.png') no-repeat 50% 50%;
20
- }
21
-
22
- .ms-container:after {
23
- content : ".";
24
- display : block;
25
- height : 0;
26
- line-height : 0;
27
- font-size : 0;
28
- clear : both;
29
- min-height : 0;
30
- visibility : hidden;
31
- }
32
-
33
- .ms-container .ms-selectable, .ms-container .ms-selection {
34
- background : #fff;
35
- color : #555;
36
- float : left;
37
- width : 45%;
38
- }
39
-
40
- .ms-container .ms-selection {
41
- float : right;
42
- }
43
-
44
- .ms-container .ms-list {
45
- -webkit-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
46
- -moz-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
47
- box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075);
48
- -webkit-transition : border linear 0.2s, box-shadow linear 0.2s;
49
- -moz-transition : border linear 0.2s, box-shadow linear 0.2s;
50
- -ms-transition : border linear 0.2s, box-shadow linear 0.2s;
51
- -o-transition : border linear 0.2s, box-shadow linear 0.2s;
52
- transition : border linear 0.2s, box-shadow linear 0.2s;
53
- border : 1px solid #ccc;
54
- -webkit-border-radius : 3px;
55
- -moz-border-radius : 3px;
56
- border-radius : 3px;
57
- position : relative;
58
- height : 200px;
59
- padding : 0;
60
- overflow-y : auto;
61
- }
62
-
63
- .ms-container .ms-list.ms-focus {
64
- border-color : rgba(82, 168, 236, 0.8);
65
- -webkit-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
66
- -moz-box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
67
- box-shadow : inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
68
- outline : 0;
69
- outline : thin dotted \9;
70
- }
71
-
72
- .ms-container ul {
73
- margin : 0;
74
- list-style-type : none;
75
- padding : 0;
76
- }
77
-
78
- .ms-container .ms-optgroup-container {
79
- width : 100%;
80
- }
81
-
82
- .ms-container .ms-optgroup-label {
83
- margin : 0;
84
- padding : 5px 0px 0px 5px;
85
- cursor : pointer;
86
- color : #999;
87
- }
88
-
89
- .ms-container .ms-selectable li.ms-elem-selectable,
90
- .ms-container .ms-selection li.ms-elem-selection {
91
- border-bottom : 1px #eee solid;
92
- padding : 2px 10px;
93
- color : #555;
94
- font-size : 14px;
95
- }
96
-
97
- .ms-container .ms-selectable li.ms-hover,
98
- .ms-container .ms-selection li.ms-hover {
99
- cursor : pointer;
100
- color : #fff;
101
- text-decoration : none;
102
- background-color : #08c;
103
- }
104
-
105
- .ms-container .ms-selectable li.disabled,
106
- .ms-container .ms-selection li.disabled {
107
- background-color : #eee;
108
- color : #aaa;
109
- cursor : text;
110
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/entries/dashboard.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { setLocaleData } from '@wordpress/i18n';
5
+ import { registerPlugin } from '@wordpress/plugins';
6
+ import { useDispatch } from '@wordpress/data';
7
+
8
+ // Silence warnings until JS i18n is stable.
9
+ setLocaleData( { '': {} }, 'ithemes-security-pro' );
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { useSingletonEffect } from '@ithemes/security-hocs';
15
+ import { slug, settings } from './dashboard/index';
16
+ import './dashboard/style.scss';
17
+
18
+ registerPlugin( 'itsec-backup-dashboard', {
19
+ render() {
20
+ return <App />;
21
+ },
22
+ } );
23
+
24
+ function App() {
25
+ const { registerCard } = useDispatch( 'ithemes-security/dashboard' );
26
+ useSingletonEffect( App, () => registerCard( slug, settings ) );
27
+
28
+ return null;
29
+ }
core/modules/backup/entries/dashboard/index.js ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { take, isEmpty } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies.
8
+ */
9
+ import { __ } from '@wordpress/i18n';
10
+ import { dateI18n } from '@wordpress/date';
11
+ import { withDispatch } from '@wordpress/data';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import {
17
+ CardHeader,
18
+ CardHeaderTitle,
19
+ CardFooterSchemaActions,
20
+ } from '@ithemes/security.dashboard.dashboard';
21
+ import { shortenNumber } from '@ithemes/security-utils';
22
+ import './style.scss';
23
+
24
+ function DatabaseBackup( { card, config, addNotice } ) {
25
+ const onComplete = ( href, response ) => {
26
+ if ( href.endsWith( '/backup' ) ) {
27
+ addNotice( response.message, 'backup-complete' );
28
+ }
29
+ };
30
+
31
+ if ( isEmpty( card.data ) ) {
32
+ return (
33
+ <div className="itsec-card--type-database-backup itsec-card-database-backup--no-data">
34
+ <CardHeader>
35
+ <CardHeaderTitle card={ card } config={ config } />
36
+ </CardHeader>
37
+ <section className="itsec-card-database-backup__no-data-message">
38
+ <p>
39
+ { __(
40
+ 'Enable database logging or file backups to see a history of completed backups.',
41
+ 'better-wp-security'
42
+ ) }
43
+ </p>
44
+ </section>
45
+ <CardFooterSchemaActions
46
+ card={ card }
47
+ onComplete={ onComplete }
48
+ />
49
+ </div>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div
55
+ className={ `itsec-card--type-database-backup itsec-card-database-backup--source-${ card.data.source }` }
56
+ >
57
+ <CardHeader>
58
+ <CardHeaderTitle card={ card } config={ config } />
59
+ </CardHeader>
60
+ <section className="itsec-card-database-backup__total">
61
+ <span className="itsec-card-database-backup__total-count">
62
+ { shortenNumber( card.data.total ) }
63
+ { card.data.total === 100 && <sup>+</sup> }
64
+ </span>
65
+ <span className="itsec-card-database-backup__total-label">
66
+ { __( 'Backups', 'better-wp-security' ) }
67
+ </span>
68
+ </section>
69
+ { card.data.backups.length > 0 && (
70
+ <section
71
+ className="itsec-card-database-backup__recent-backups-section"
72
+ aria-label={ __( 'Recent Backups', 'better-wp-security' ) }
73
+ >
74
+ <table className="itsec-card-database-backup__recent-backups">
75
+ <thead>
76
+ <tr>
77
+ <th
78
+ scope="column"
79
+ className="itsec-card-database-backup__col-date"
80
+ >
81
+ { __( 'Date', 'better-wp-security' ) }
82
+ </th>
83
+ <th
84
+ scope="column"
85
+ className="itsec-card-database-backup__col-size"
86
+ >
87
+ { __( 'Size', 'better-wp-security' ) }
88
+ </th>
89
+ { card.data.source === 'files' && (
90
+ <th
91
+ scope="column"
92
+ className="itsec-card-database-backup__col-actions"
93
+ >
94
+ <span className="screen-reader-text">
95
+ { __( 'Download', 'better-wp-security' ) }
96
+ </span>
97
+ </th>
98
+ ) }
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ { take( card.data.backups, 50 ).map( ( backup ) => (
103
+ <tr key={ backup.url || backup.time }>
104
+ <th
105
+ scope="row"
106
+ className="itsec-card-database-backup__col-date"
107
+ >
108
+ <span className="itsec-card-database-backup__backup-date">
109
+ { dateI18n(
110
+ 'M d, Y',
111
+ backup.time
112
+ ) }
113
+ </span>{ ' ' }
114
+ <span className="itsec-card-database-backup__backup-time">
115
+ { dateI18n( 'g:i A', backup.time ) }
116
+ </span>
117
+ </th>
118
+ <td className="itsec-card-database-backup__col-size">
119
+ { backup.size_format }
120
+ </td>
121
+ { card.data.source === 'files' && (
122
+ <td className="itsec-card-database-backup__col-actions">
123
+ { backup.url && (
124
+ <a href={ backup.url } download>
125
+ { __( 'Download', 'better-wp-security' ) }
126
+ </a>
127
+ ) }
128
+ </td>
129
+ ) }
130
+ </tr>
131
+ ) ) }
132
+ </tbody>
133
+ </table>
134
+ </section>
135
+ ) }
136
+ <CardFooterSchemaActions card={ card } onComplete={ onComplete } />
137
+ </div>
138
+ );
139
+ }
140
+
141
+ export const slug = 'database-backup';
142
+ export const settings = {
143
+ render: withDispatch( ( dispatch ) => ( {
144
+ addNotice( message, id ) {
145
+ dispatch( 'core/notices' ).createSuccessNotice( message, {
146
+ id,
147
+ context: 'ithemes-security',
148
+ } );
149
+ setTimeout(
150
+ () =>
151
+ dispatch( 'core/notices' ).removeNotice(
152
+ id,
153
+ 'ithemes-security'
154
+ ),
155
+ 10000
156
+ );
157
+ },
158
+ } ) )( DatabaseBackup ),
159
+ elementQueries: [
160
+ {
161
+ type: 'width',
162
+ dir: 'max',
163
+ px: 300,
164
+ },
165
+ {
166
+ type: 'width',
167
+ dir: 'max',
168
+ px: 250,
169
+ },
170
+ {
171
+ type: 'height',
172
+ dir: 'max',
173
+ px: 300,
174
+ },
175
+ ],
176
+ };
core/modules/backup/entries/dashboard/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/backup/entries/dashboard/style.scss ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-card--type-database-backup {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: space-between;
5
+ height: 100%;
6
+ overflow: hidden;
7
+
8
+ & .itsec-card-header {
9
+ flex-shrink: 0;
10
+ }
11
+
12
+ & .itsec-card-database-backup__total {
13
+ border: 5px solid #0081E3;
14
+ border-radius: 50%;
15
+ height: 150px;
16
+ width: 150px;
17
+ margin: 0 auto 1em auto;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ position: relative;
22
+ flex-shrink: 0;
23
+ flex-grow: 0;
24
+ }
25
+
26
+ & .itsec-card-database-backup__total-count {
27
+ position: absolute;
28
+ top: 25%;
29
+ color: $dark-gray-900;
30
+ font-size: 4.5em;
31
+ font-weight: bold;
32
+ letter-spacing: -3.8px;
33
+ line-height: 1em;
34
+
35
+ & sup {
36
+ position: absolute;
37
+ font-size: .5em;
38
+ line-height: 1;
39
+ }
40
+ }
41
+
42
+ & .itsec-card-database-backup__total-label {
43
+ position: absolute;
44
+ bottom: 25%;
45
+ margin-top: auto;
46
+ color: $dark-gray-500;
47
+ text-transform: uppercase;
48
+ }
49
+
50
+ & h3 {
51
+ margin: .5em 0 0;
52
+ padding: 0 1em;
53
+ color: grey;
54
+ font-size: 1.1em;
55
+ font-weight: normal;
56
+ text-transform: uppercase;
57
+ }
58
+
59
+ & .itsec-card-database-backup__recent-backups-section {
60
+ flex-shrink: 1;
61
+ overflow-y: scroll;
62
+ position: relative;
63
+
64
+ & .itsec-card-database-backup__recent-backups {
65
+ width: 100%;
66
+ border-spacing: 0;
67
+
68
+ @include sticky-table();
69
+ }
70
+ }
71
+
72
+ &.itsec-card-database-backup--no-data {
73
+ justify-content: flex-start;
74
+
75
+ & .itsec-card-footer__actions {
76
+ border: none;
77
+ flex-grow: 1;
78
+ align-self: center;
79
+ flex-direction: column;
80
+ justify-content: center;
81
+ }
82
+
83
+ & .itsec-card-footer__action {
84
+ margin: .25em 0;
85
+ }
86
+ }
87
+
88
+ .itsec-card-database-backup__no-data-message {
89
+ padding: 0 1em;
90
+
91
+ p {
92
+ margin: 0;
93
+ }
94
+ }
95
+
96
+ .itsec-card[max-width~="300px"] &.itsec-card-database-backup--source-files .itsec-card-database-backup__col-size {
97
+ display: none;
98
+ }
99
+
100
+ .itsec-card[max-width~="250px"] &.itsec-card-database-backup--source-files .itsec-card-database-backup__backup-time {
101
+ display: none;
102
+ }
103
+
104
+ .itsec-card[max-height~="300px"] &:not(.itsec-card-database-backup--no-data) {
105
+ display: grid;
106
+ grid-template-areas: "header header" "left right";
107
+ grid-template-columns: 1fr 1fr;
108
+ grid-template-rows: minmax(max-content, auto) 1fr;
109
+ padding: 0 1em;
110
+ align-items: start;
111
+
112
+ & .itsec-card-header {
113
+ grid-area: header;
114
+ padding: 1em 0 0;
115
+ }
116
+
117
+ & .itsec-card-database-backup__total {
118
+ grid-area: left;
119
+ margin: 0 auto;
120
+ width: 100px;
121
+ height: 100px;
122
+ align-self: center;
123
+ }
124
+
125
+ & .itsec-card-database-backup__total-count {
126
+ font-size: 3em;
127
+ letter-spacing: -2px;
128
+ }
129
+
130
+ & .itsec-card-database-backup__total-label {
131
+ font-size: .75em;
132
+ }
133
+
134
+ & .itsec-card-footer__actions {
135
+ grid-area: right;
136
+ margin: 0;
137
+ border: none;
138
+ align-items: center;
139
+ flex-direction: column;
140
+ flex-shrink: 1;
141
+ justify-content: center;
142
+ height: 100%;
143
+ }
144
+
145
+ & .itsec-card-footer__actions .itsec-card-footer__action {
146
+ margin: .25em 0;
147
+ }
148
+
149
+ & .itsec-card-database-backup__recent-backups-section {
150
+ display: none;
151
+ }
152
+ }
153
+
154
+ .itsec-card[max-height~="300px"][max-width~="250px"] &:not(.itsec-card-database-backup--no-data) {
155
+ grid-template-areas: "header header" "left left";
156
+
157
+ & .itsec-card-database-backup__total {
158
+ height: 125px;
159
+ width: 125px;
160
+ }
161
+
162
+ & .itsec-card-database-backup__total-count {
163
+ font-size: 4em;
164
+ letter-spacing: -3.5px;
165
+ }
166
+
167
+ & .itsec-card-database-backup__total-label {
168
+ font-size: .75em;
169
+ bottom: 20%;
170
+ }
171
+
172
+ & .itsec-card-footer__actions {
173
+ display: none;
174
+ }
175
+ }
176
+ }
core/modules/backup/entries/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/backup/img/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/backup/img/switch.png DELETED
Binary file
core/modules/backup/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/modules/backup/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/backup/js/jquery.multi-select.js DELETED
@@ -1,552 +0,0 @@
1
- /*
2
- * MultiSelect v0.9.10
3
- * Copyright (c) 2012 Louis Cuny
4
- *
5
- * This program is free software. It comes without any warranty, to
6
- * the extent permitted by applicable law. You can redistribute it
7
- * and/or modify it under the terms of the Do What The Fuck You Want
8
- * To Public License, Version 2, as published by Sam Hocevar. See
9
- * http://sam.zoy.org/wtfpl/COPYING for more details.
10
- */
11
-
12
- ! function ( $ ) {
13
-
14
- "use strict";
15
-
16
- /* MULTISELECT CLASS DEFINITION
17
- * ====================== */
18
-
19
- var MultiSelect = function ( element, options ) {
20
- this.options = options;
21
- this.$element = $( element );
22
- this.$container = $( '<div/>', { 'class' : "ms-container" } );
23
- this.$selectableContainer = $( '<div/>', { 'class' : 'ms-selectable' } );
24
- this.$selectionContainer = $( '<div/>', { 'class' : 'ms-selection' } );
25
- this.$selectableUl = $( '<ul/>', { 'class' : "ms-list", 'tabindex' : '-1' } );
26
- this.$selectionUl = $( '<ul/>', { 'class' : "ms-list", 'tabindex' : '-1' } );
27
- this.scrollTo = 0;
28
- this.elemsSelector = 'li:visible:not(.ms-optgroup-label,.ms-optgroup-container,.' + options.disabledClass + ')';
29
- };
30
-
31
- MultiSelect.prototype = {
32
- constructor : MultiSelect,
33
-
34
- init : function () {
35
- var that = this,
36
- ms = this.$element;
37
-
38
- if ( ms.next( '.ms-container' ).length === 0 ) {
39
- ms.css( { position : 'absolute', left : '-9999px' } );
40
- ms.attr( 'id', ms.attr( 'id' ) ? ms.attr( 'id' ) : Math.ceil( Math.random() * 1000 ) + 'multiselect' );
41
- this.$container.attr( 'id', 'ms-' + ms.attr( 'id' ) );
42
- this.$container.addClass( that.options.cssClass );
43
- ms.find( 'option' ).each( function () {
44
- that.generateLisFromOption( this );
45
- } );
46
-
47
- this.$selectionUl.find( '.ms-optgroup-label' ).hide();
48
-
49
- if ( that.options.selectableHeader ) {
50
- that.$selectableContainer.append( that.options.selectableHeader );
51
- }
52
- that.$selectableContainer.append( that.$selectableUl );
53
- if ( that.options.selectableFooter ) {
54
- that.$selectableContainer.append( that.options.selectableFooter );
55
- }
56
-
57
- if ( that.options.selectionHeader ) {
58
- that.$selectionContainer.append( that.options.selectionHeader );
59
- }
60
- that.$selectionContainer.append( that.$selectionUl );
61
- if ( that.options.selectionFooter ) {
62
- that.$selectionContainer.append( that.options.selectionFooter );
63
- }
64
-
65
- that.$container.append( that.$selectableContainer );
66
- that.$container.append( that.$selectionContainer );
67
- ms.after( that.$container );
68
-
69
- that.activeMouse( that.$selectableUl );
70
- that.activeKeyboard( that.$selectableUl );
71
-
72
- var action = that.options.dblClick ? 'dblclick' : 'click';
73
-
74
- that.$selectableUl.on( action, '.ms-elem-selectable', function () {
75
- that.select( $( this ).data( 'ms-value' ) );
76
- } );
77
- that.$selectionUl.on( action, '.ms-elem-selection', function () {
78
- that.deselect( $( this ).data( 'ms-value' ) );
79
- } );
80
-
81
- that.activeMouse( that.$selectionUl );
82
- that.activeKeyboard( that.$selectionUl );
83
-
84
- ms.on( 'focus', function () {
85
- that.$selectableUl.focus();
86
- } )
87
- }
88
-
89
- var selectedValues = ms.find( 'option:selected' ).map(function () {
90
- return $( this ).val();
91
- } ).get();
92
- that.select( selectedValues, 'init' );
93
-
94
- if ( typeof that.options.afterInit === 'function' ) {
95
- that.options.afterInit.call( this, this.$container );
96
- }
97
- },
98
-
99
- 'generateLisFromOption' : function ( option, index, $container ) {
100
- var that = this,
101
- ms = that.$element,
102
- attributes = "",
103
- $option = $( option );
104
-
105
- for ( var cpt = 0; cpt < option.attributes.length; cpt ++ ) {
106
- var attr = option.attributes[cpt];
107
-
108
- if ( attr.name !== 'value' && attr.name !== 'disabled' ) {
109
- attributes += attr.name + '="' + attr.value + '" ';
110
- }
111
- }
112
- var selectableLi = $( '<li ' + attributes + '><span>' + that.escapeHTML( $option.text() ) + '</span></li>' ),
113
- selectedLi = selectableLi.clone(),
114
- value = $option.val(),
115
- elementId = that.sanitize( value );
116
-
117
- selectableLi
118
- .data( 'ms-value', value )
119
- .addClass( 'ms-elem-selectable' )
120
- .attr( 'id', elementId + '-selectable' );
121
-
122
- selectedLi
123
- .data( 'ms-value', value )
124
- .addClass( 'ms-elem-selection' )
125
- .attr( 'id', elementId + '-selection' )
126
- .hide();
127
-
128
- if ( $option.prop( 'disabled' ) || ms.prop( 'disabled' ) ) {
129
- selectedLi.addClass( that.options.disabledClass );
130
- selectableLi.addClass( that.options.disabledClass );
131
- }
132
-
133
- var $optgroup = $option.parent( 'optgroup' );
134
-
135
- if ( $optgroup.length > 0 ) {
136
- var optgroupLabel = $optgroup.attr( 'label' ),
137
- optgroupId = that.sanitize( optgroupLabel ),
138
- $selectableOptgroup = that.$selectableUl.find( '#optgroup-selectable-' + optgroupId ),
139
- $selectionOptgroup = that.$selectionUl.find( '#optgroup-selection-' + optgroupId );
140
-
141
- if ( $selectableOptgroup.length === 0 ) {
142
- var optgroupContainerTpl = '<li class="ms-optgroup-container"></li>',
143
- optgroupTpl = '<ul class="ms-optgroup"><li class="ms-optgroup-label"><span>' + optgroupLabel + '</span></li></ul>';
144
-
145
- $selectableOptgroup = $( optgroupContainerTpl );
146
- $selectionOptgroup = $( optgroupContainerTpl );
147
- $selectableOptgroup.attr( 'id', 'optgroup-selectable-' + optgroupId );
148
- $selectionOptgroup.attr( 'id', 'optgroup-selection-' + optgroupId );
149
- $selectableOptgroup.append( $( optgroupTpl ) );
150
- $selectionOptgroup.append( $( optgroupTpl ) );
151
- if ( that.options.selectableOptgroup ) {
152
- $selectableOptgroup.find( '.ms-optgroup-label' ).on( 'click', function () {
153
- var values = $optgroup.children( ':not(:selected)' ).map(function () {
154
- return $( this ).val()
155
- } ).get();
156
- that.select( values );
157
- } );
158
- $selectionOptgroup.find( '.ms-optgroup-label' ).on( 'click', function () {
159
- var values = $optgroup.children( ':selected' ).map(function () {
160
- return $( this ).val()
161
- } ).get();
162
- that.deselect( values );
163
- } );
164
- }
165
- that.$selectableUl.append( $selectableOptgroup );
166
- that.$selectionUl.append( $selectionOptgroup );
167
- }
168
- index = index == undefined ? $selectableOptgroup.children().length : index + 1;
169
- selectableLi.insertAt( index, $selectableOptgroup.children() );
170
- selectedLi.insertAt( index, $selectionOptgroup.children() );
171
- } else {
172
- index = index == undefined ? that.$selectableUl.children().length : index;
173
-
174
- selectableLi.insertAt( index, that.$selectableUl );
175
- selectedLi.insertAt( index, that.$selectionUl );
176
- }
177
- },
178
-
179
- 'addOption' : function ( options ) {
180
- var that = this;
181
-
182
- if ( options.value ) {
183
- options = [options];
184
- }
185
- $.each( options, function ( index, option ) {
186
- if ( option.value && that.$element.find( "option[value='" + option.value + "']" ).length === 0 ) {
187
- var $option = $( '<option value="' + option.value + '">' + option.text + '</option>' ),
188
- index = parseInt( (typeof option.index === 'undefined' ? that.$element.children().length : option.index) ),
189
- $container = option.nested == undefined ? that.$element : $( "optgroup[label='" + option.nested + "']" )
190
-
191
- $option.insertAt( index, $container );
192
- that.generateLisFromOption( $option.get( 0 ), index, option.nested );
193
- }
194
- } )
195
- },
196
-
197
- 'escapeHTML' : function ( text ) {
198
- return $( "<div>" ).text( text ).html();
199
- },
200
-
201
- 'activeKeyboard' : function ( $list ) {
202
- var that = this;
203
-
204
- $list.on( 'focus', function () {
205
- $( this ).addClass( 'ms-focus' );
206
- } )
207
- .on( 'blur', function () {
208
- $( this ).removeClass( 'ms-focus' );
209
- } )
210
- .on( 'keydown', function ( e ) {
211
- switch ( e.which ) {
212
- case 40:
213
- case 38:
214
- e.preventDefault();
215
- e.stopPropagation();
216
- that.moveHighlight( $( this ), (e.which === 38) ? - 1 : 1 );
217
- return;
218
- case 37:
219
- case 39:
220
- e.preventDefault();
221
- e.stopPropagation();
222
- that.switchList( $list );
223
- return;
224
- case 9:
225
- if ( that.$element.is( '[tabindex]' ) ) {
226
- e.preventDefault();
227
- var tabindex = parseInt( that.$element.attr( 'tabindex' ), 10 );
228
- tabindex = (e.shiftKey) ? tabindex - 1 : tabindex + 1;
229
- $( '[tabindex="' + (tabindex) + '"]' ).focus();
230
- return;
231
- } else {
232
- if ( e.shiftKey ) {
233
- that.$element.trigger( 'focus' );
234
- }
235
- }
236
- }
237
- if ( $.inArray( e.which, that.options.keySelect ) > - 1 ) {
238
- e.preventDefault();
239
- e.stopPropagation();
240
- that.selectHighlighted( $list );
241
- return;
242
- }
243
- } );
244
- },
245
-
246
- 'moveHighlight' : function ( $list, direction ) {
247
- var $elems = $list.find( this.elemsSelector ),
248
- $currElem = $elems.filter( '.ms-hover' ),
249
- $nextElem = null,
250
- elemHeight = $elems.first().outerHeight(),
251
- containerHeight = $list.height(),
252
- containerSelector = '#' + this.$container.prop( 'id' );
253
-
254
- // Deactive mouseenter event when move is active
255
- // It fixes a bug when mouse is over the list
256
- $elems.off( 'mouseenter' );
257
-
258
- $elems.removeClass( 'ms-hover' );
259
- if ( direction === 1 ) { // DOWN
260
-
261
- $nextElem = $currElem.nextAll( this.elemsSelector ).first();
262
- if ( $nextElem.length === 0 ) {
263
- var $optgroupUl = $currElem.parent();
264
-
265
- if ( $optgroupUl.hasClass( 'ms-optgroup' ) ) {
266
- var $optgroupLi = $optgroupUl.parent(),
267
- $nextOptgroupLi = $optgroupLi.next( ':visible' );
268
-
269
- if ( $nextOptgroupLi.length > 0 ) {
270
- $nextElem = $nextOptgroupLi.find( this.elemsSelector ).first();
271
- } else {
272
- $nextElem = $elems.first();
273
- }
274
- } else {
275
- $nextElem = $elems.first();
276
- }
277
- }
278
- } else if ( direction === - 1 ) { // UP
279
-
280
- $nextElem = $currElem.prevAll( this.elemsSelector ).first();
281
- if ( $nextElem.length === 0 ) {
282
- var $optgroupUl = $currElem.parent();
283
-
284
- if ( $optgroupUl.hasClass( 'ms-optgroup' ) ) {
285
- var $optgroupLi = $optgroupUl.parent(),
286
- $prevOptgroupLi = $optgroupLi.prev( ':visible' );
287
-
288
- if ( $prevOptgroupLi.length > 0 ) {
289
- $nextElem = $prevOptgroupLi.find( this.elemsSelector ).last();
290
- } else {
291
- $nextElem = $elems.last();
292
- }
293
- } else {
294
- $nextElem = $elems.last();
295
- }
296
- }
297
- }
298
- if ( $nextElem.length > 0 ) {
299
- $nextElem.addClass( 'ms-hover' );
300
- var scrollTo = $list.scrollTop() + $nextElem.position().top -
301
- containerHeight / 2 + elemHeight / 2;
302
-
303
- $list.scrollTop( scrollTo );
304
- }
305
- },
306
-
307
- 'selectHighlighted' : function ( $list ) {
308
- var $elems = $list.find( this.elemsSelector ),
309
- $highlightedElem = $elems.filter( '.ms-hover' ).first();
310
-
311
- if ( $highlightedElem.length > 0 ) {
312
- if ( $list.parent().hasClass( 'ms-selectable' ) ) {
313
- this.select( $highlightedElem.data( 'ms-value' ) );
314
- } else {
315
- this.deselect( $highlightedElem.data( 'ms-value' ) );
316
- }
317
- $elems.removeClass( 'ms-hover' );
318
- }
319
- },
320
-
321
- 'switchList' : function ( $list ) {
322
- $list.blur();
323
- if ( $list.parent().hasClass( 'ms-selectable' ) ) {
324
- this.$selectionUl.focus();
325
- } else {
326
- this.$selectableUl.focus();
327
- }
328
- },
329
-
330
- 'activeMouse' : function ( $list ) {
331
- var that = this;
332
-
333
- $list.on( 'mousemove', function () {
334
- var elems = $list.find( that.elemsSelector );
335
-
336
- elems.on( 'mouseenter', function () {
337
- elems.removeClass( 'ms-hover' );
338
- $( this ).addClass( 'ms-hover' );
339
- } );
340
- } );
341
- },
342
-
343
- 'refresh' : function () {
344
- this.destroy();
345
- this.$element.multiSelect( this.options );
346
- },
347
-
348
- 'destroy' : function () {
349
- $( "#ms-" + this.$element.attr( "id" ) ).remove();
350
- this.$element.removeData( 'multiselect' );
351
- },
352
-
353
- 'select' : function ( value, method ) {
354
- if ( typeof value === 'string' ) {
355
- value = [value];
356
- }
357
-
358
- var that = this,
359
- ms = this.$element,
360
- msIds = $.map( value, function ( val ) {
361
- return(that.sanitize( val ));
362
- } ),
363
- selectables = this.$selectableUl.find( '#' + msIds.join( '-selectable, #' ) + '-selectable' ).filter( ':not(.' + that.options.disabledClass + ')' ),
364
- selections = this.$selectionUl.find( '#' + msIds.join( '-selection, #' ) + '-selection' ).filter( ':not(.' + that.options.disabledClass + ')' ),
365
- options = ms.find( 'option:not(:disabled)' ).filter( function () {
366
- return($.inArray( this.value, value ) > - 1);
367
- } );
368
-
369
- if ( method === 'init' ) {
370
- selectables = this.$selectableUl.find( '#' + msIds.join( '-selectable, #' ) + '-selectable' ),
371
- selections = this.$selectionUl.find( '#' + msIds.join( '-selection, #' ) + '-selection' );
372
- }
373
-
374
- if ( selectables.length > 0 ) {
375
- selectables.addClass( 'ms-selected' ).hide();
376
- selections.addClass( 'ms-selected' ).show();
377
- options.prop( 'selected', true );
378
-
379
- var selectableOptgroups = that.$selectableUl.children( '.ms-optgroup-container' );
380
- if ( selectableOptgroups.length > 0 ) {
381
- selectableOptgroups.each( function () {
382
- var selectablesLi = $( this ).find( '.ms-elem-selectable' );
383
- if ( selectablesLi.length === selectablesLi.filter( '.ms-selected' ).length ) {
384
- $( this ).find( '.ms-optgroup-label' ).hide();
385
- }
386
- } );
387
-
388
- var selectionOptgroups = that.$selectionUl.children( '.ms-optgroup-container' );
389
- selectionOptgroups.each( function () {
390
- var selectionsLi = $( this ).find( '.ms-elem-selection' );
391
- if ( selectionsLi.filter( '.ms-selected' ).length > 0 ) {
392
- $( this ).find( '.ms-optgroup-label' ).show();
393
- }
394
- } );
395
- } else {
396
- if ( that.options.keepOrder ) {
397
- var selectionLiLast = that.$selectionUl.find( '.ms-selected' );
398
- if ( (selectionLiLast.length > 1) && (selectionLiLast.last().get( 0 ) != selections.get( 0 )) ) {
399
- selections.insertAfter( selectionLiLast.last() );
400
- }
401
- }
402
- }
403
- if ( method !== 'init' ) {
404
- ms.trigger( 'change' );
405
- if ( typeof that.options.afterSelect === 'function' ) {
406
- that.options.afterSelect.call( this, value );
407
- }
408
- }
409
- }
410
- },
411
-
412
- 'deselect' : function ( value ) {
413
- if ( typeof value === 'string' ) {
414
- value = [value];
415
- }
416
-
417
- var that = this,
418
- ms = this.$element,
419
- msIds = $.map( value, function ( val ) {
420
- return(that.sanitize( val ));
421
- } ),
422
- selectables = this.$selectableUl.find( '#' + msIds.join( '-selectable, #' ) + '-selectable' ),
423
- selections = this.$selectionUl.find( '#' + msIds.join( '-selection, #' ) + '-selection' ).filter( '.ms-selected' ).filter( ':not(.' + that.options.disabledClass + ')' ),
424
- options = ms.find( 'option' ).filter( function () {
425
- return($.inArray( this.value, value ) > - 1);
426
- } );
427
-
428
- if ( selections.length > 0 ) {
429
- selectables.removeClass( 'ms-selected' ).show();
430
- selections.removeClass( 'ms-selected' ).hide();
431
- options.prop( 'selected', false );
432
-
433
- var selectableOptgroups = that.$selectableUl.children( '.ms-optgroup-container' );
434
- if ( selectableOptgroups.length > 0 ) {
435
- selectableOptgroups.each( function () {
436
- var selectablesLi = $( this ).find( '.ms-elem-selectable' );
437
- if ( selectablesLi.filter( ':not(.ms-selected)' ).length > 0 ) {
438
- $( this ).find( '.ms-optgroup-label' ).show();
439
- }
440
- } );
441
-
442
- var selectionOptgroups = that.$selectionUl.children( '.ms-optgroup-container' );
443
- selectionOptgroups.each( function () {
444
- var selectionsLi = $( this ).find( '.ms-elem-selection' );
445
- if ( selectionsLi.filter( '.ms-selected' ).length === 0 ) {
446
- $( this ).find( '.ms-optgroup-label' ).hide();
447
- }
448
- } );
449
- }
450
- ms.trigger( 'change' );
451
- if ( typeof that.options.afterDeselect === 'function' ) {
452
- that.options.afterDeselect.call( this, value );
453
- }
454
- }
455
- },
456
-
457
- 'select_all' : function () {
458
- var ms = this.$element,
459
- values = ms.val();
460
-
461
- ms.find( 'option:not(":disabled")' ).prop( 'selected', true );
462
- this.$selectableUl.find( '.ms-elem-selectable' ).filter( ':not(.' + this.options.disabledClass + ')' ).addClass( 'ms-selected' ).hide();
463
- this.$selectionUl.find( '.ms-optgroup-label' ).show();
464
- this.$selectableUl.find( '.ms-optgroup-label' ).hide();
465
- this.$selectionUl.find( '.ms-elem-selection' ).filter( ':not(.' + this.options.disabledClass + ')' ).addClass( 'ms-selected' ).show();
466
- this.$selectionUl.focus();
467
- ms.trigger( 'change' );
468
- if ( typeof this.options.afterSelect === 'function' ) {
469
- var selectedValues = $.grep( ms.val(), function ( item ) {
470
- return $.inArray( item, values ) < 0;
471
- } );
472
- this.options.afterSelect.call( this, selectedValues );
473
- }
474
- },
475
-
476
- 'deselect_all' : function () {
477
- var ms = this.$element,
478
- values = ms.val();
479
-
480
- ms.find( 'option' ).prop( 'selected', false );
481
- this.$selectableUl.find( '.ms-elem-selectable' ).removeClass( 'ms-selected' ).show();
482
- this.$selectionUl.find( '.ms-optgroup-label' ).hide();
483
- this.$selectableUl.find( '.ms-optgroup-label' ).show();
484
- this.$selectionUl.find( '.ms-elem-selection' ).removeClass( 'ms-selected' ).hide();
485
- this.$selectableUl.focus();
486
- ms.trigger( 'change' );
487
- if ( typeof this.options.afterDeselect === 'function' ) {
488
- this.options.afterDeselect.call( this, values );
489
- }
490
- },
491
-
492
- sanitize : function ( value ) {
493
- var hash = 0, i, char;
494
- if ( value.length == 0 ) {
495
- return hash;
496
- }
497
- var ls = 0;
498
- for ( i = 0, ls = value.length; i < ls; i ++ ) {
499
- char = value.charCodeAt( i );
500
- hash = ((hash << 5) - hash) + char;
501
- hash |= 0; // Convert to 32bit integer
502
- }
503
- return hash;
504
- }
505
- };
506
-
507
- /* MULTISELECT PLUGIN DEFINITION
508
- * ======================= */
509
-
510
- $.fn.multiSelect = function () {
511
- var option = arguments[0],
512
- args = arguments;
513
-
514
- return this.each( function () {
515
- var $this = $( this ),
516
- data = $this.data( 'multiselect' ),
517
- options = $.extend( {}, $.fn.multiSelect.defaults, $this.data(), typeof option === 'object' && option );
518
-
519
- if ( ! data ) {
520
- $this.data( 'multiselect', (data = new MultiSelect( this, options )) );
521
- }
522
-
523
- if ( typeof option === 'string' ) {
524
- data[option]( args[1] );
525
- } else {
526
- data.init();
527
- }
528
- } );
529
- };
530
-
531
- $.fn.multiSelect.defaults = {
532
- keySelect : [32],
533
- selectableOptgroup : false,
534
- disabledClass : 'disabled',
535
- dblClick : false,
536
- keepOrder : false,
537
- cssClass : ''
538
- };
539
-
540
- $.fn.multiSelect.Constructor = MultiSelect;
541
-
542
- $.fn.insertAt = function ( index, $parent ) {
543
- return this.each( function () {
544
- if ( index === 0 ) {
545
- $parent.prepend( this );
546
- } else {
547
- $parent.children().eq( index - 1 ).after( this );
548
- }
549
- } );
550
- }
551
-
552
- }( window.jQuery );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/js/settings-page.js DELETED
@@ -1,58 +0,0 @@
1
- jQuery( document ).ready( function () {
2
- var $container = jQuery( '#wpcontent' );
3
-
4
-
5
- $container.on( 'click', '#itsec-backup-reset_backup_location', function( e ) {
6
- e.preventDefault();
7
-
8
- jQuery( '#itsec-backup-location' ).val( itsec_backup.default_backup_location );
9
- } );
10
-
11
- $container.on( 'change', '#itsec-backup-method', function( e ) {
12
- var method = jQuery(this).val();
13
-
14
- if ( 1 == method ) {
15
- jQuery( '.itsec-backup-method-file-content' ).hide();
16
- } else {
17
- jQuery( '.itsec-backup-method-file-content' ).show();
18
- }
19
- } );
20
-
21
- jQuery( '#itsec-backup-method' ).trigger( 'change' );
22
-
23
-
24
- jQuery( '#itsec-backup-exclude' ).multiSelect( {
25
- selectableHeader: '<div class="custom-header">' + itsec_backup.available_tables_label + '</div>',
26
- selectionHeader: '<div class="custom-header">' + itsec_backup.excluded_tables_label + '</div>',
27
- keepOrder: true
28
- } );
29
-
30
-
31
- jQuery( '#itsec-backup-create_backup' ).click(function( e ) {
32
- e.preventDefault();
33
-
34
- var originalButtonLabel = jQuery( '#itsec-backup-create_backup' ).attr( 'value' );
35
-
36
- jQuery( '#itsec-backup-create_backup' )
37
- .removeClass( 'button-primary' )
38
- .addClass( 'button-secondary' )
39
- .attr( 'value', itsec_backup.creating_backup_text )
40
- .prop( 'disabled', true );
41
-
42
- jQuery( '#itsec_backup_status' ).html( '' );
43
-
44
- var data = {
45
- 'method': 'create-backup'
46
- };
47
-
48
- itsecUtil.sendModuleAJAXRequest( 'backup', data, function( results ) {
49
- jQuery( '#itsec_backup_status' ).html( results.response );
50
-
51
- jQuery( '#itsec-backup-create_backup' )
52
- .removeClass( 'button-secondary' )
53
- .addClass( 'button-primary' )
54
- .attr( 'value', originalButtonLabel )
55
- .prop( 'disabled', false );
56
- } );
57
- });
58
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/module.json ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "backup",
3
+ "status": "default-active",
4
+ "title": "Database Backups",
5
+ "description": "Manually create a database backup or schedule automatic database backups.",
6
+ "help": "Database backups can help you restore your database in the case of data corruption. However, it is not a complete backup and will not include your site files.",
7
+ "type": "utility",
8
+ "settings": {
9
+ "type": "object",
10
+ "properties": {
11
+ "enabled": {
12
+ "type": "boolean",
13
+ "default": false,
14
+ "title": "Schedule Database Backups"
15
+ },
16
+ "interval": {
17
+ "type": "integer",
18
+ "minimum": 0,
19
+ "default": 3,
20
+ "title": "Backup Interval",
21
+ "description": "The number of days between database backups."
22
+ },
23
+ "method": {
24
+ "type": "string",
25
+ "enum": [
26
+ "both",
27
+ "email",
28
+ "local"
29
+ ],
30
+ "enumNames": [
31
+ "Save Locally and Email",
32
+ "Email Only",
33
+ "Save Locally Only"
34
+ ],
35
+ "default": "email",
36
+ "title": "Backup Method",
37
+ "description": "Select what we should do with your backup file. You can have it emailed to you, saved locally or both."
38
+ },
39
+ "location": {
40
+ "type": "string",
41
+ "format": "directory",
42
+ "default": "",
43
+ "title": "Backup Location",
44
+ "description": "The path on your machine where backup files should be stored. For added security, it is recommended you do not include it in your website root folder."
45
+ },
46
+ "retain": {
47
+ "type": "integer",
48
+ "minimum": 0,
49
+ "default": 0,
50
+ "title": "Backups to Retain",
51
+ "description": "Limit the number of backups stored locally (on this server). Any older backups beyond this number will be removed. Enter “0” to retain all backups."
52
+ },
53
+ "zip": {
54
+ "type": "boolean",
55
+ "default": true,
56
+ "title": "Compress Backup Files",
57
+ "description": "By default, iThemes Security will zip backup files to reduce file size. You may need to turn this off if you are having problems with backups."
58
+ },
59
+ "exclude": {
60
+ "type": "array",
61
+ "items": {
62
+ "type": "string",
63
+ "enum": []
64
+ },
65
+ "uniqueItems": true,
66
+ "default": [
67
+ "itsec_logs",
68
+ "itsec_temp",
69
+ "itsec_lockouts",
70
+ "itsec_distributed_storage",
71
+ "itsec_opaque_tokens",
72
+ "itsec_geolocation_cache"
73
+ ],
74
+ "title": "Backup Tables",
75
+ "description": "Specify which tables should be included or excluded from backups. WordPress Core tables are always included."
76
+ },
77
+ "last_run": {
78
+ "type": "integer",
79
+ "minimum": 0,
80
+ "default": 0,
81
+ "title": "Last Run",
82
+ "readonly": true
83
+ }
84
+ },
85
+ "uiSchema": {
86
+ "ui:sections": [
87
+ {
88
+ "title": "Scheduling",
89
+ "fields": [
90
+ "enabled",
91
+ "interval"
92
+ ]
93
+ },
94
+ {
95
+ "title": "Configuration",
96
+ "fields": [
97
+ "method",
98
+ "location",
99
+ "retain",
100
+ "zip"
101
+ ]
102
+ },
103
+ {
104
+ "title": "Backup Tables",
105
+ "fields": [
106
+ "exclude"
107
+ ]
108
+ }
109
+ ],
110
+ "location": {
111
+ "ui:resettable": true
112
+ },
113
+ "exclude": {
114
+ "ui:title": "",
115
+ "ui:widget": "IncludeExcludeWidget",
116
+ "ui:options": {
117
+ "includeList": {
118
+ "title": "Excluded Tables",
119
+ "description": "List of tables to exclude from each backup.",
120
+ "button": "Include Table"
121
+ },
122
+ "excludeList": {
123
+ "title": "Included Tables",
124
+ "description": "List of tables to include in each backup.",
125
+ "button": "Exclude Table"
126
+ }
127
+ }
128
+ }
129
+ }
130
+ },
131
+ "conditional-settings": {
132
+ "interval": {
133
+ "settings": {
134
+ "type": "object",
135
+ "properties": {
136
+ "enabled": {
137
+ "type": "boolean",
138
+ "enum": [
139
+ true
140
+ ]
141
+ }
142
+ }
143
+ }
144
+ },
145
+ "location": {
146
+ "settings": {
147
+ "type": "object",
148
+ "properties": {
149
+ "method": {
150
+ "type": "string",
151
+ "enum": [
152
+ "both",
153
+ "local"
154
+ ]
155
+ }
156
+ }
157
+ }
158
+ },
159
+ "retain": {
160
+ "settings": {
161
+ "type": "object",
162
+ "properties": {
163
+ "method": {
164
+ "type": "string",
165
+ "enum": [
166
+ "both",
167
+ "local"
168
+ ]
169
+ }
170
+ }
171
+ }
172
+ }
173
+ },
174
+ "removed-settings": [
175
+ "all_sites"
176
+ ],
177
+ "scheduling": {
178
+ "backup": {
179
+ "schedule": "backup",
180
+ "type": "recurring",
181
+ "conditional": {
182
+ "type": "object",
183
+ "properties": {
184
+ "enabled": {
185
+ "type": "boolean",
186
+ "enum": [
187
+ true
188
+ ]
189
+ },
190
+ "interval": {
191
+ "type": "integer",
192
+ "minimum": 1
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
core/modules/backup/settings-page.php DELETED
@@ -1,198 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Backup_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $script_version = 1;
5
-
6
-
7
- public function __construct() {
8
- $this->id = 'backup';
9
- $this->title = __( 'Database Backups', 'better-wp-security' );
10
- $this->description = __( 'Create backups of your site\'s database. The backups can be created manually and on a schedule.', 'better-wp-security' );
11
- $this->type = 'recommended';
12
-
13
- parent::__construct();
14
- }
15
-
16
- public function enqueue_scripts_and_styles() {
17
- wp_enqueue_script( 'jquery-multi-select', plugins_url( 'js/jquery.multi-select.js', __FILE__ ), array( 'jquery' ), $this->script_version, true );
18
-
19
- $vars = array(
20
- 'default_backup_location' => ITSEC_Modules::get_default( $this->id, 'location' ),
21
- 'available_tables_label' => __( 'Tables for Backup', 'better-wp-security' ),
22
- 'excluded_tables_label' => __( 'Excluded Tables', 'better-wp-security' ),
23
- 'creating_backup_text' => __( 'Creating Backup...', 'better-wp-security' ),
24
- );
25
-
26
- wp_enqueue_script( 'itsec-backup-settings-page-script', plugins_url( 'js/settings-page.js', __FILE__ ), array( 'jquery', 'jquery-multi-select' ), $this->script_version, true );
27
- wp_localize_script( 'itsec-backup-settings-page-script', 'itsec_backup', $vars );
28
-
29
- wp_enqueue_style( 'itsec-backup-settings-page-style', plugins_url( 'css/settings-page.css', __FILE__ ), array(), $this->script_version );
30
- }
31
-
32
- public function handle_ajax_request( $data ) {
33
- global $itsec_backup;
34
-
35
- if ( ! isset( $itsec_backup ) ) {
36
- require_once( 'class-itsec-backup.php' );
37
- $itsec_backup = new ITSEC_Backup();
38
- $itsec_backup->run();
39
- }
40
-
41
- $result = $itsec_backup->do_backup( true );
42
- $message = '';
43
-
44
- if ( is_wp_error( $result ) ) {
45
- $errors = ITSEC_Response::get_error_strings( $result );
46
-
47
- foreach ( $errors as $error ) {
48
- $message .= '<div class="error inline"><p><strong>' . $error . '</strong></p></div>';
49
- }
50
- } else if ( is_array( $result ) ) {
51
- $message = '<div class="updated fade inline"><p><strong>' . $result['message'] . '</strong></p></div>';
52
- } else {
53
- $message = '<div class="error inline"><p><strong>' . sprintf( __( 'The backup request returned an unexpected response. It returned a response of type <code>%1$s</code>.', 'better-wp-security' ), gettype( $result ) ) . '</strong></p></div>';
54
- }
55
-
56
- ITSEC_Response::set_response( $message );
57
- }
58
-
59
- protected function render_description( $form ) {
60
-
61
- ?>
62
- <p><?php _e( 'One of the best ways to protect yourself from an attack is to have access to a database backup of your site. If something goes wrong, you can get your site back by restoring the database from a backup and replacing the files with fresh ones. Use the button below to create a backup of your database for this purpose. You can also schedule automated backups and download or delete previous backups.', 'better-wp-security' ); ?></p>
63
- <?php
64
-
65
- }
66
-
67
- protected function render_settings( $form ) {
68
- $settings = $form->get_options();
69
-
70
-
71
- $methods = array(
72
- 0 => __( 'Save Locally and Email', 'better-wp-security' ),
73
- 1 => __( 'Email Only', 'better-wp-security' ),
74
- 2 => __( 'Save Locally Only', 'better-wp-security' ),
75
- );
76
-
77
- $excludes = $this->get_excludable_tables( $settings );
78
-
79
- ?>
80
- <div class="hide-if-no-js">
81
- <p><?php _e( 'Press the button below to create a database backup using the saved settings.', 'better-wp-security' ); ?></p>
82
- <p><?php $form->add_button( 'create_backup', array( 'value' => __( 'Create a Database Backup', 'better-wp-security' ), 'class' => 'button-primary' ) ); ?></p>
83
- <div id="itsec_backup_status"></div>
84
- </div>
85
-
86
- <table class="form-table itsec-settings-section">
87
- <tr>
88
- <th scope="row"><label for="itsec-backup-all_sites"><?php _e( 'Backup Full Database', 'better-wp-security' ); ?></label></th>
89
- <td>
90
- <?php $form->add_checkbox( 'all_sites' ); ?>
91
- <label for="itsec-backup-all_sites"><?php _e( 'Checking this box will have the backup script backup all tables in your database, even if they are not part of this WordPress site.', 'better-wp-security' ); ?></label>
92
- </td>
93
- </tr>
94
- <tr>
95
- <th scope="row"><label for="itsec-backup-method"><?php _e( 'Backup Method', 'better-wp-security' ); ?></label></th>
96
- <td>
97
- <?php $form->add_select( 'method', $methods ); ?>
98
- <br />
99
- <label for="itsec-backup-method"><?php _e( 'Backup Save Method', 'better-wp-security' ); ?></label>
100
- <p class="description"><?php _e( 'Select what we should do with your backup file. You can have it emailed to you, saved locally or both.', 'better-wp-security' ); ?></p>
101
- </td>
102
- </tr>
103
- <tr class="itsec-backup-method-file-content">
104
- <th scope="row"><label for="itsec-backup-location"><?php _e( 'Backup Location', 'better-wp-security' ); ?></label></th>
105
- <td>
106
- <?php $form->add_text( 'location', array( 'class' => 'large-text' ) ); ?>
107
- <label for="itsec-backup-location"><?php _e( 'The path on your machine where backup files should be stored.', 'better-wp-security' ); ?></label>
108
- <p class="description"><?php _e( 'This path must be writable by your website. For added security, it is recommended you do not include it in your website root folder.', 'better-wp-security' ); ?></p>
109
- <div class="hide-if-no-js">
110
- <?php $form->add_button( 'reset_backup_location', array( 'value' => __( 'Restore Default Location', 'better-wp-security' ), 'class' => 'button-secondary' ) ); ?>
111
- </div>
112
- </td>
113
- </tr>
114
- <tr>
115
- <th scope="row"><label for="itsec-backup-retain"><?php _e( 'Backups to Retain', 'better-wp-security' ); ?></label></th>
116
- <td>
117
- <?php $form->add_text( 'retain', array( 'class' => 'small-text' ) ); ?>
118
- <label for="itsec-backup-retain"><?php _e( 'Backups', 'better-wp-security' ); ?></label>
119
- <p class="description"><?php _e( 'Limit the number of backups stored locally (on this server). Any older backups beyond this number will be removed. Setting to "0" will retain all backups.', 'better-wp-security' ); ?></p>
120
- </td>
121
- </tr>
122
- <tr>
123
- <th scope="row"><label for="itsec-backup-zip"><?php _e( 'Compress Backup Files', 'better-wp-security' ); ?></label></th>
124
- <td>
125
- <?php $form->add_checkbox( 'zip' ); ?>
126
- <label for="itsec-backup-zip"><?php _e( 'Zip Database Backups', 'better-wp-security' ); ?></label>
127
- <p class="description"><?php _e( 'You may need to turn this off if you are having problems with backups.', 'better-wp-security' ); ?></p>
128
- </td>
129
- </tr>
130
- <tr>
131
- <th scope="row"><label for="itsec-backup-exclude"><?php _e( 'Exclude Tables', 'better-wp-security' ); ?></label></th>
132
- <td>
133
- <label for="itsec-backup-exclude"><?php _e( 'Tables with data that does not need to be backed up', 'better-wp-security' ); ?></label>
134
- <?php $form->add_multi_select( 'exclude', $excludes ); ?>
135
- <p class="description"><?php _e( 'Some plugins can create log files in your database. While these logs might be handy for some functions, they can also take up a lot of space and, in some cases, even make backing up your database almost impossible. Select log tables above to exclude their data from the backup. Note: The table itself will be backed up, but not the data in the table.', 'better-wp-security' ); ?></p>
136
- </td>
137
- </tr>
138
- <tr>
139
- <th scope="row"><label for="itsec-backup-enabled"><?php _e( 'Schedule Database Backups', 'better-wp-security' ); ?></label></th>
140
- <td>
141
- <?php $form->add_checkbox( 'enabled', array( 'class' => 'itsec-settings-toggle' ) ); ?>
142
- <label for="itsec-backup-enabled"><?php _e( 'Enable Scheduled Database Backups', 'better-wp-security' ); ?></label>
143
- </td>
144
- </tr>
145
- <tr class="itsec-backup-enabled-content">
146
- <th scope="row"><label for="itsec-backup-interval"><?php _e( 'Backup Interval', 'better-wp-security' ); ?></label></th>
147
- <td>
148
- <?php $form->add_text( 'interval', array( 'class' => 'small-text' ) ); ?>
149
- <label for="itsec-backup-interval"><?php _e( 'Days', 'better-wp-security' ); ?></label>
150
- <p class="description"><?php _e( 'The number of days between database backups.', 'better-wp-security' ); ?></p>
151
- </td>
152
- </tr>
153
- </table>
154
- <?php
155
-
156
- }
157
-
158
- private function get_excludable_tables( $settings ) {
159
- global $wpdb;
160
-
161
- $ignored_tables = array(
162
- 'commentmeta',
163
- 'comments',
164
- 'links',
165
- 'options',
166
- 'postmeta',
167
- 'posts',
168
- 'term_relationships',
169
- 'term_taxonomy',
170
- 'terms',
171
- 'usermeta',
172
- 'users',
173
- );
174
-
175
- if ( $settings['all_sites'] ) {
176
- $query = 'SHOW TABLES';
177
- } else {
178
- $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', "{$wpdb->base_prefix}%" );
179
- }
180
-
181
- $tables = $wpdb->get_results( $query, ARRAY_N );
182
- $excludes = array();
183
-
184
- foreach ( $tables as $table ) {
185
- $short_table = substr( $table[0], strlen( $wpdb->prefix ) );
186
-
187
- if ( in_array( $short_table, $ignored_tables ) ) {
188
- continue;
189
- }
190
-
191
- $excludes[$short_table] = $table[0];
192
- }
193
-
194
- return $excludes;
195
- }
196
- }
197
-
198
- new ITSEC_Backup_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/backup/settings.php CHANGED
@@ -1,39 +1,67 @@
1
  <?php
2
 
3
- final class ITSEC_Backup_Settings extends ITSEC_Settings {
4
- public function get_id() {
5
- return 'backup';
 
 
 
 
6
  }
7
 
8
- public function get_defaults() {
9
- return array(
10
- 'all_sites' => false,
11
- 'method' => 1,
12
- 'location' => ITSEC_Core::get_storage_dir( 'backups' ),
13
- 'retain' => 0,
14
- 'zip' => true,
15
- 'exclude' => array(
16
- 'itsec_log',
17
- 'itsec_temp',
18
- 'itsec_lockouts',
19
- ),
20
- 'enabled' => false,
21
- 'interval' => 3,
22
- 'last_run' => 0,
23
- );
24
  }
25
 
26
- protected function handle_settings_changes( $old_settings ) {
27
- parent::handle_settings_changes( $old_settings );
28
 
29
- if ( $old_settings['enabled'] !== $this->settings['enabled'] ) {
30
- if ( $this->settings['enabled'] ) {
31
- ITSEC_Core::get_scheduler()->schedule( 'backup', 'backup' );
32
- } else {
33
- ITSEC_Core::get_scheduler()->unschedule( 'backup' );
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
 
 
36
  }
37
  }
38
 
39
- ITSEC_Modules::register_settings( new ITSEC_Backup_Settings() );
1
  <?php
2
 
3
+ final class ITSEC_Backup_Settings extends \iThemesSecurity\Config_Settings {
4
+ public function get_default( $setting, $default = null ) {
5
+ if ( 'location' === $setting ) {
6
+ return ITSEC_Core::get_storage_dir( 'backups' );
7
+ }
8
+
9
+ return parent::get_default( $setting, $default );
10
  }
11
 
12
+ public function get_settings_schema() {
13
+ $schema = parent::get_settings_schema();
14
+
15
+ $schema['properties']['exclude']['items']['enum'] = array_values( array_unique( array_merge(
16
+ $this->get( 'exclude', [] ),
17
+ $this->get_excludable_tables()
18
+ ) ) );
19
+
20
+ return $schema;
 
 
 
 
 
 
 
21
  }
22
 
23
+ protected function get_excludable_tables(): array {
24
+ global $wpdb;
25
 
26
+ $ignore = [
27
+ 'posts',
28
+ 'comments',
29
+ 'links',
30
+ 'options',
31
+ 'postmeta',
32
+ 'terms',
33
+ 'term_taxonomy',
34
+ 'term_relationships',
35
+ 'termmeta',
36
+ 'commentmeta',
37
+ 'categories',
38
+ 'post2cat',
39
+ 'link2cat',
40
+ 'users',
41
+ 'usermeta',
42
+ 'blogs',
43
+ 'blogmeta',
44
+ 'signups',
45
+ 'site',
46
+ 'sitemeta',
47
+ 'sitecategories',
48
+ 'registration_log',
49
+ ];
50
+
51
+ $tables = $wpdb->get_col( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->base_prefix . '%' ) );
52
+
53
+ if ( ! is_multisite() ) {
54
+ $clean = array_map( static function ( $table ) use ( $wpdb ) {
55
+ return substr( $table, strlen( $wpdb->base_prefix ) );
56
+ }, $tables );
57
+ } else {
58
+ $clean = array_map( static function ( $table ) use ( $wpdb ) {
59
+ return preg_replace( "/^{$wpdb->base_prefix}(\d)*_/", '', $table );
60
+ }, $tables );
61
  }
62
+
63
+ return array_values( array_diff( array_unique( $clean ), $ignore ) );
64
  }
65
  }
66
 
67
+ ITSEC_Modules::register_settings( new ITSEC_Backup_Settings( ITSEC_Modules::get_config( 'backup' ) ) );
core/modules/backup/setup.php CHANGED
@@ -1,120 +1,103 @@
1
  <?php
2
 
3
- if ( ! class_exists( 'ITSEC_Backup_Setup' ) ) {
4
 
5
- class ITSEC_Backup_Setup {
 
 
 
6
 
7
- public function __construct() {
 
 
 
 
 
 
 
8
 
9
- add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
10
- add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
11
- add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
12
- add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
 
 
13
 
14
- }
15
 
16
- /**
17
- * Execute module activation.
18
- *
19
- * @since 4.0
20
- *
21
- * @return void
22
- */
23
- public function execute_activate() {}
24
-
25
- /**
26
- * Execute module deactivation
27
- *
28
- * @return void
29
- */
30
- public function execute_deactivate() {}
31
-
32
- /**
33
- * Execute module uninstall
34
- *
35
- * @return void
36
- */
37
- public function execute_uninstall() {
38
-
39
- $this->execute_deactivate();
40
 
41
- delete_site_option( 'itsec_backup' );
42
 
43
- }
44
-
45
- /**
46
- * Execute module upgrade
47
- *
48
- * @return void
49
- */
50
- public function execute_upgrade( $build ) {
51
 
52
- if ( $build < 4000 ) {
 
53
 
54
- global $itsec_bwps_options;
 
55
 
56
- $current_options = get_site_option( 'itsec_backup' );
57
 
 
58
 
59
- // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
60
- if ( false !== $current_options ) {
61
 
62
- $current_options['enabled'] = isset( $itsec_bwps_options['backup_enabled'] ) && $itsec_bwps_options['backup_enabled'] == 1 ? true : false;
63
- $current_options['interval'] = isset( $itsec_bwps_options['backup_interval'] ) ? intval( $itsec_bwps_options['backup_interval'] ) : 1;
 
 
 
 
 
64
 
65
- update_site_option( 'itsec_backup', $current_options );
 
66
 
 
 
 
 
 
 
 
67
  }
68
 
69
- }
70
-
71
- if ( $build < 4040 ) {
72
- $backup_options = get_site_option( 'itsec_backup' );
73
- // Make sure we have an index files to block directory listing in backups directory
74
- if ( is_dir( $backup_options['location'] ) && ! file_exists( path_join( $backup_options['location'], 'index.php' ) ) ) {
75
- file_put_contents( path_join( $backup_options['location'], 'index.php' ), "<?php\n// Silence is golden." );
76
  }
77
- }
78
-
79
- if ( $build < 4041 ) {
80
- $current_options = get_site_option( 'itsec_backup' );
81
 
82
- // If there are no current options, go with the new defaults by not saving anything
83
- if ( is_array( $current_options ) ) {
84
- // Make sure the new module is properly activated or deactivated
85
- if ( $current_options['enabled'] ) {
86
- ITSEC_Modules::activate( 'backup' );
87
- } else {
88
- ITSEC_Modules::deactivate( 'backup' );
89
- }
90
-
91
- if ( isset( $current_options['location'] ) && ! is_dir( $current_options['location'] ) ) {
92
- unset( $current_options['location'] );
93
- }
94
-
95
- $options = ITSEC_Modules::get_defaults( 'backup' );
96
 
97
- foreach ( $options as $name => $value ) {
98
- if ( isset( $current_options[$name] ) ) {
99
- $options[$name] = $current_options[$name];
100
- }
101
  }
102
-
103
- ITSEC_Modules::set_settings( 'backup', $options );
104
  }
105
- }
106
 
107
- if ( $build < 4069 ) {
108
- delete_site_option( 'itsec_backup' );
109
  }
 
110
 
111
- if ( $build < 4079 ) {
112
- wp_clear_scheduled_hook( 'itsec_execute_backup_cron' );
113
- }
114
  }
115
 
116
- }
 
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
  new ITSEC_Backup_Setup();
1
  <?php
2
 
3
+ class ITSEC_Backup_Setup {
4
 
5
+ public function __construct() {
6
+ add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
7
+ add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ) );
8
+ }
9
 
10
+ /**
11
+ * Execute module uninstall
12
+ *
13
+ * @return void
14
+ */
15
+ public function execute_uninstall() {
16
+ delete_site_option( 'itsec_backup' );
17
+ }
18
 
19
+ /**
20
+ * Execute module upgrade
21
+ *
22
+ * @return void
23
+ */
24
+ public function execute_upgrade( $build ) {
25
 
26
+ if ( $build < 4000 ) {
27
 
28
+ global $itsec_bwps_options;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ $current_options = get_site_option( 'itsec_backup' );
31
 
 
 
 
 
 
 
 
 
32
 
33
+ // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
34
+ if ( false !== $current_options ) {
35
 
36
+ $current_options['enabled'] = isset( $itsec_bwps_options['backup_enabled'] ) && $itsec_bwps_options['backup_enabled'] == 1 ? true : false;
37
+ $current_options['interval'] = isset( $itsec_bwps_options['backup_interval'] ) ? intval( $itsec_bwps_options['backup_interval'] ) : 1;
38
 
39
+ update_site_option( 'itsec_backup', $current_options );
40
 
41
+ }
42
 
43
+ }
 
44
 
45
+ if ( $build < 4040 ) {
46
+ $backup_options = get_site_option( 'itsec_backup' );
47
+ // Make sure we have an index files to block directory listing in backups directory
48
+ if ( is_dir( $backup_options['location'] ) && ! file_exists( path_join( $backup_options['location'], 'index.php' ) ) ) {
49
+ file_put_contents( path_join( $backup_options['location'], 'index.php' ), "<?php\n// Silence is golden." );
50
+ }
51
+ }
52
 
53
+ if ( $build < 4041 ) {
54
+ $current_options = get_site_option( 'itsec_backup' );
55
 
56
+ // If there are no current options, go with the new defaults by not saving anything
57
+ if ( is_array( $current_options ) ) {
58
+ // Make sure the new module is properly activated or deactivated
59
+ if ( $current_options['enabled'] ) {
60
+ ITSEC_Modules::activate( 'backup' );
61
+ } else {
62
+ ITSEC_Modules::deactivate( 'backup' );
63
  }
64
 
65
+ if ( isset( $current_options['location'] ) && ! is_dir( $current_options['location'] ) ) {
66
+ unset( $current_options['location'] );
 
 
 
 
 
67
  }
 
 
 
 
68
 
69
+ $options = ITSEC_Modules::get_defaults( 'backup' );
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ foreach ( $options as $name => $value ) {
72
+ if ( isset( $current_options[ $name ] ) ) {
73
+ $options[ $name ] = $current_options[ $name ];
 
74
  }
 
 
75
  }
 
76
 
77
+ ITSEC_Modules::set_settings( 'backup', $options );
 
78
  }
79
+ }
80
 
81
+ if ( $build < 4069 ) {
82
+ delete_site_option( 'itsec_backup' );
 
83
  }
84
 
85
+ if ( $build < 4079 ) {
86
+ wp_clear_scheduled_hook( 'itsec_execute_backup_cron' );
87
+ }
88
 
89
+ if ( $build < 4123 ) {
90
+ $update = [
91
+ 'both',
92
+ 'email',
93
+ 'local',
94
+ ];
95
+
96
+ $legacy = ITSEC_Modules::get_setting( 'backup', 'method' );
97
+ $new = $update[ $legacy ] ?? ITSEC_Modules::get_default( 'backup', 'method' );
98
+ ITSEC_Modules::set_setting( 'backup', 'method', $new );
99
+ }
100
+ }
101
  }
102
 
103
  new ITSEC_Backup_Setup();
core/modules/backup/validator.php DELETED
@@ -1,32 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Backup_Validator extends ITSEC_Validator {
4
- public function get_id() {
5
- return 'backup';
6
- }
7
-
8
- protected function sanitize_settings() {
9
- $previous_settings = ITSEC_Modules::get_settings( $this->get_id() );
10
-
11
- if ( ! isset( $this->settings['interval'] ) ) {
12
- $this->settings['interval'] = $previous_settings['interval'];
13
- }
14
- if ( ! isset( $this->settings['last_run'] ) ) {
15
- $this->settings['last_run'] = $previous_settings['last_run'];
16
- }
17
-
18
-
19
- $this->sanitize_setting( 'bool', 'all_sites', __( 'Backup Full Database', 'better-wp-security' ) );
20
- $this->sanitize_setting( 'positive-int', 'method', __( 'Backup Method', 'better-wp-security' ) );
21
- $this->sanitize_setting( array( 0, 1, 2 ), 'method', __( 'Backup Method', 'better-wp-security' ) );
22
- $this->sanitize_setting( 'writable-directory', 'location', __( 'Backup Location', 'better-wp-security' ) );
23
- $this->sanitize_setting( 'positive-int', 'retain', __( 'Backups to Retain', 'better-wp-security' ) );
24
- $this->sanitize_setting( 'bool', 'zip', __( 'Compress Backup Files', 'better-wp-security' ) );
25
- $this->sanitize_setting( 'newline-separated-array', 'exclude', __( 'Exclude Tables', 'better-wp-security' ) );
26
- $this->sanitize_setting( 'bool', 'enabled', __( 'Schedule Database Backups', 'better-wp-security' ) );
27
- $this->sanitize_setting( 'positive-int', 'interval', __( 'Backup Interval', 'better-wp-security' ) );
28
- $this->sanitize_setting( 'positive-int', 'last_run', __( 'Last Run', 'better-wp-security' ), false );
29
- }
30
- }
31
-
32
- ITSEC_Modules::register_validator( new ITSEC_Backup_Validator() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/ban-users/Database_Repository.php CHANGED
@@ -191,7 +191,7 @@ final class Database_Repository implements Repository, Creatable, Updatable, Del
191
  public function get_creation_schema() {
192
  return [
193
  'type' => 'object',
194
- 'title' => __( 'Manually Ban', 'better-wp-security' ),
195
  'required' => [ 'host' ],
196
  'properties' => [
197
  'host' => [
191
  public function get_creation_schema() {
192
  return [
193
  'type' => 'object',
194
+ 'title' => __( 'Add Ban', 'better-wp-security' ),
195
  'required' => [ 'host' ],
196
  'properties' => [
197
  'host' => [
core/modules/ban-users/Module/Validator.php DELETED
@@ -1,154 +0,0 @@
1
- <?php
2
-
3
- namespace iThemesSecurity\Ban_Users;
4
-
5
- use iThemesSecurity\Actor\User;
6
- use iThemesSecurity\Ban_Users\Ban;
7
- use iThemesSecurity\Contracts\Runnable;
8
-
9
- class Validator extends \ITSEC_Validator implements Runnable {
10
-
11
- /** @var Database_Repository */
12
- private $repository;
13
-
14
- /**
15
- * ITSEC_Ban_Users_Validator constructor.
16
- *
17
- * @param Database_Repository $repository
18
- */
19
- public function __construct( Database_Repository $repository ) {
20
- $this->repository = $repository;
21
-
22
- parent::__construct();
23
- }
24
-
25
- public function run() {
26
- \ITSEC_Modules::register_validator( $this );
27
- }
28
-
29
- public function get_id() {
30
- return 'ban-users';
31
- }
32
-
33
- protected function sanitize_settings() {
34
- $this->vars_to_skip_validate_matching_fields[] = 'host_list';
35
-
36
- $this->sanitize_setting( 'bool', 'default', __( 'Default Ban List', 'better-wp-security' ) );
37
- $this->sanitize_setting( 'bool', 'enable_ban_lists', __( 'Ban Lists', 'better-wp-security' ) );
38
- $this->sanitize_setting( 'positive-int', 'server_config_limit', __( 'Limit Banned IPs in Server Config', 'better-wp-security' ) );
39
-
40
- if ( isset( $this->settings['host_list'] ) && is_array( $this->settings['host_list'] ) ) {
41
- $this->sanitize_setting( 'newline-separated-ips', 'host_list', __( 'Ban Hosts', 'better-wp-security' ) );
42
- require_once( \ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
43
-
44
- $whitelisted_hosts = array();
45
- $current_ip = \ITSEC_Lib::get_ip();
46
-
47
- foreach ( $this->settings['host_list'] as $host ) {
48
- if ( is_user_logged_in() && \ITSEC_Lib_IP_Tools::intersect( $current_ip, \ITSEC_Lib_IP_Tools::ip_wild_to_ip_cidr( $host ) ) ) {
49
- $this->set_can_save( false );
50
-
51
- /* translators: 1: input name, 2: invalid host */
52
- $this->add_error( sprintf( __( 'The following host in %1$s matches your current IP and cannot be banned: %2$s', 'better-wp-security' ), __( 'Ban Hosts', 'better-wp-security' ), $host ) );
53
-
54
- continue;
55
- }
56
-
57
- if ( \ITSEC_Lib::is_ip_whitelisted( $host ) ) {
58
- $whitelisted_hosts[] = $host;
59
- }
60
- }
61
-
62
- if ( ! empty( $whitelisted_hosts ) ) {
63
- $this->set_can_save( false );
64
-
65
- /* translators: 1: input name, 2: invalid host list */
66
- $this->add_error( wp_sprintf( _n( 'The following IP in %1$s is on the authorized hosts list and cannot be banned: %2$l', 'The following IPs in %1$s are on the authorized hosts list and cannot be banned: %2$l', count( $whitelisted_hosts ), 'better-wp-security' ), __( 'Ban Hosts', 'better-wp-security' ), $whitelisted_hosts ) );
67
- }
68
- }
69
-
70
- $this->sanitize_setting( array( $this, 'sanitize_agent_list_entry' ), 'agent_list', __( 'Ban User Agents', 'better-wp-security' ) );
71
- }
72
-
73
- protected function sanitize_agent_list_entry( $entry ) {
74
- return trim( sanitize_text_field( $entry ) );
75
- }
76
-
77
- protected function validate_settings() {
78
- if ( ! $this->can_save() ) {
79
- return;
80
- }
81
-
82
- if ( isset( $this->settings['host_list'] ) ) {
83
- $this->sync_host_list( $this->settings['host_list'] );
84
- unset( $this->settings['host_list'] );
85
- }
86
-
87
- if ( ! \ITSEC_Core::is_interactive() ) {
88
- return;
89
- }
90
-
91
- $previous_settings = \ITSEC_Modules::get_settings( $this->get_id() );
92
-
93
- foreach ( $this->settings as $key => $val ) {
94
- if ( ! isset( $previous_settings[ $key ] ) || $previous_settings[ $key ] !== $val ) {
95
- \ITSEC_Response::regenerate_server_config();
96
- break;
97
- }
98
- }
99
- }
100
-
101
- /**
102
- * Syncs the list of hosts to the Database Repository.
103
- *
104
- * @param string[] $new_hosts List of IP addresses.
105
- */
106
- protected function sync_host_list( $new_hosts ) {
107
- $old_hosts = $this->repository->get_legacy_hosts();
108
-
109
- foreach ( $old_hosts as $id => $host ) {
110
- if ( in_array( $host, $new_hosts, true ) ) {
111
- continue;
112
- }
113
-
114
- $this->delete_host( $id );
115
- }
116
-
117
- foreach ( $new_hosts as $host ) {
118
- if ( in_array( $host, $old_hosts, true ) ) {
119
- continue;
120
- }
121
-
122
- $this->add_host( $host );
123
- }
124
- }
125
-
126
- protected function delete_host( $id ) {
127
- if ( ! $ban = $this->repository->get( (int) $id ) ) {
128
- return;
129
- }
130
-
131
- try {
132
- $this->repository->delete( $ban );
133
- } catch ( \iThemesSecurity\Exception\WP_Error $e ) {
134
- $this->add_error( $e->get_error() );
135
- }
136
- }
137
-
138
- protected function add_host( $host ) {
139
- $actor = null;
140
- $comment = '';
141
-
142
- if ( is_user_logged_in() ) {
143
- $actor = new User( wp_get_current_user() );
144
- } elseif ( defined( 'WP_CLI' ) && WP_CLI ) {
145
- $comment = __( 'Added via WP CLI', 'better-wp-security' );
146
- }
147
-
148
- try {
149
- $this->repository->persist( new Ban( $host, $actor, $comment ) );
150
- } catch ( \iThemesSecurity\Exception\WP_Error $e ) {
151
- $this->add_error( $e->get_error() );
152
- }
153
- }
154
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/ban-users/REST.php ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Ban_Users;
4
+
5
+ use iThemesSecurity\Actor\User;
6
+ use iThemesSecurity\Contracts\Runnable;
7
+
8
+ final class REST implements Runnable {
9
+
10
+ /** @var Database_Repository */
11
+ private $repository;
12
+
13
+ /** @var array */
14
+ private $schema;
15
+
16
+ /**
17
+ * REST constructor.
18
+ *
19
+ * @param Database_Repository $repository
20
+ */
21
+ public function __construct( Database_Repository $repository ) { $this->repository = $repository; }
22
+
23
+ public function run() {
24
+ $this->setup_schema();
25
+ $this->register_routes();
26
+ add_filter( 'itsec_ban_hosts_rest_schema', [ $this, 'add_many_link' ] );
27
+ }
28
+
29
+ private function setup_schema() {
30
+ $this->schema = [
31
+ 'title' => __( 'Add Many', 'better-wp-security' ),
32
+ 'type' => 'object',
33
+ 'required' => [ 'bans' ],
34
+ 'properties' => [
35
+ 'bans' => [
36
+ 'title' => __( 'IPs to Ban', 'better-wp-security' ),
37
+ 'description' => __( 'Enter one IP address per-line. Optionally, include a note by ending the line with a # sign.' ),
38
+ 'type' => 'array',
39
+ 'items' => [
40
+ 'type' => 'string',
41
+ 'default' => '',
42
+ ],
43
+ 'minItems' => 1,
44
+ ],
45
+ ],
46
+ 'uiSchema' => [
47
+ 'bans' => [
48
+ 'ui:field' => 'TextareaListField',
49
+ 'ui:rows' => 10,
50
+ 'ui:placeholder' => '127.0.0.1 # This is my note',
51
+ ],
52
+ ]
53
+ ];
54
+ }
55
+
56
+ private function register_routes() {
57
+ register_rest_route( 'ithemes-security/rpc', 'ban-users/add-many', [
58
+ [
59
+ 'args' => \ITSEC_Lib_REST::get_endpoint_args_for_schema( $this->schema ),
60
+ 'methods' => \WP_REST_Server::CREATABLE,
61
+ 'callback' => [ $this, 'add_many_callback' ],
62
+ 'permission_callback' => 'ITSEC_Core::current_user_can_manage',
63
+ ],
64
+ 'schema' => function () { return $this->schema; }
65
+ ] );
66
+ }
67
+
68
+ public function add_many_callback( \WP_REST_Request $request ) {
69
+ $bans = $request['bans'];
70
+ $to_add = [];
71
+ $error = new \WP_Error();
72
+
73
+ foreach ( $bans as $i => $ban ) {
74
+ list( $ip, $note ) = array_pad( explode( ' #', $ban ), 2, '' );
75
+
76
+ $valid = \ITSEC_Lib_REST::validate_ip( $ip, $request, "bans.{$i}" );
77
+
78
+ if ( is_wp_error( $valid ) ) {
79
+ $error->merge_from( $valid );
80
+ continue;
81
+ }
82
+
83
+ $ip = \ITSEC_Lib_IP_Tools::ip_wild_to_ip_cidr( $ip );
84
+ $note = trim( $note, " \t\n\r\0\x0B#" );
85
+
86
+ $ban = new Ban( $ip, new User( wp_get_current_user() ), $note );
87
+ $to_add[] = $ban;
88
+ }
89
+
90
+ if ( $error->has_errors() ) {
91
+ $error->add_data( [ 'status' => \WP_Http::BAD_REQUEST ] );
92
+
93
+ return $error;
94
+ }
95
+
96
+ $persisted = [];
97
+
98
+ foreach ( $to_add as $ban ) {
99
+ try {
100
+ $persisted[] = $this->repository->persist( $ban )->get_id();
101
+ } catch ( \Exception $e ) {
102
+
103
+ }
104
+ }
105
+
106
+ return $persisted;
107
+ }
108
+
109
+ public function add_many_link( $schema ) {
110
+ $schema['links'][] = [
111
+ 'rel' => 'create-form',
112
+ 'href' => rest_url( 'ithemes-security/rpc/ban-users/add-many' ),
113
+ 'submissionSchema' => \ITSEC_Lib_REST::sanitize_schema_for_output( $this->schema ),
114
+ 'title' => $this->schema['title'],
115
+ ];
116
+
117
+ return $schema;
118
+ }
119
+ }
core/modules/ban-users/container.php CHANGED
@@ -3,12 +3,13 @@
3
  namespace iThemesSecurity\Ban_Users;
4
 
5
  use iThemesSecurity\Actor\Multi_Actor_Factory;
 
6
  use Pimple\Container;
7
  use Pimple\Exception\FrozenServiceException;
8
 
9
  return static function ( Container $c ) {
10
  $c['module.ban-users.files'] = [
11
- 'validator.php' => Validator::class,
12
  ];
13
 
14
  try {
@@ -23,10 +24,6 @@ return static function ( Container $c ) {
23
 
24
  }
25
 
26
- $c[ Validator::class ] = static function ( Container $c ) {
27
- return new Validator( $c[ Database_Repository::class ] );
28
- };
29
-
30
  $c[ Database_Repository::class ] = static function ( Container $c ) {
31
  return new Database_Repository(
32
  $c[ Multi_Actor_Factory::class ],
@@ -34,4 +31,32 @@ return static function ( Container $c ) {
34
  );
35
  };
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  };
3
  namespace iThemesSecurity\Ban_Users;
4
 
5
  use iThemesSecurity\Actor\Multi_Actor_Factory;
6
+ use iThemesSecurity\Ban_Hosts\Filters;
7
  use Pimple\Container;
8
  use Pimple\Exception\FrozenServiceException;
9
 
10
  return static function ( Container $c ) {
11
  $c['module.ban-users.files'] = [
12
+ 'rest.php' => REST::class,
13
  ];
14
 
15
  try {
24
 
25
  }
26
 
 
 
 
 
27
  $c[ Database_Repository::class ] = static function ( Container $c ) {
28
  return new Database_Repository(
29
  $c[ Multi_Actor_Factory::class ],
31
  );
32
  };
33
 
34
+ $c[ REST::class ] = static function ( Container $c ) {
35
+ return new REST( $c[ Database_Repository::class ] );
36
+ };
37
+
38
+ \ITSEC_Lib::extend_if_able( $c, 'dashboard.cards', function ( $cards ) use ( $c ) {
39
+ $cards[] = new \ITSEC_Dashboard_Card_Pie_Chart( 'banned-users', __( 'Bans Overview', 'better-wp-security' ), [
40
+ [
41
+ 'events' => 'blacklist-brute_force',
42
+ 'label' => __( 'Login Attempts', 'better-wp-security' ),
43
+ ],
44
+ [
45
+ 'events' => 'blacklist-brute_force_admin_user',
46
+ 'label' => __( 'Login Using "admin"', 'better-wp-security' ),
47
+ ],
48
+ [
49
+ 'events' => 'blacklist-recaptcha',
50
+ 'label' => __( 'Recaptcha', 'better-wp-security' ),
51
+ ],
52
+ ], [
53
+ 'circle_label' => _x( 'Banned', 'Total Banned IPs', 'better-wp-security' ),
54
+ 'circle_callback' => function () use ( $c ) {
55
+ return $c[ Database_Repository::class ]->count_bans( new Filters() );
56
+ },
57
+ ] );
58
+
59
+ return $cards;
60
+ } );
61
+
62
  };
core/modules/ban-users/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/modules/ban-users/js/admin-ban_users.js DELETED
@@ -1,17 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( "#itsec_ban_users_enabled" ).change(function () {
4
-
5
- if ( jQuery( "#itsec_ban_users_enabled" ).is( ':checked' ) || ( jQuery( "#setting-error-settings_updated" ).length > 0 && jQuery( "#setting-error-settings_updated" ).hasClass( "error" ) ) ) {
6
-
7
- jQuery( "#ban_users_settings" ).show();
8
-
9
- } else {
10
-
11
- jQuery( "#ban_users_settings" ).hide();
12
-
13
- }
14
-
15
- } ).change();
16
-
17
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/ban-users/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/ban-users/module.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "ban-users",
3
+ "status": "default-active",
4
+ "type": "lockout",
5
+ "title": "Ban Users",
6
+ "description": "Block specific IP addresses and user agents from accessing the site.",
7
+ "help": "iThemes Security automatically adds an IP to the ban list once it meets the Ban Threshold requirements. The Ban Threshold setting can be adjusted in the [Global Settings](itsec://settings/configure/global). You can manually add IPs to the ban list from the Security Dashboard using the Banned Users card.",
8
+ "keywords": [
9
+ "blacklist"
10
+ ],
11
+ "settings": {
12
+ "type": "object",
13
+ "properties": {
14
+ "default": {
15
+ "type": "boolean",
16
+ "default": false,
17
+ "title": "Default Ban List",
18
+ "description": "As a getting-started point you can include the HackRepair.com ban list developed by Jim Walker."
19
+ },
20
+ "enable_ban_lists": {
21
+ "type": "boolean",
22
+ "default": true,
23
+ "title": "Enable Ban Lists"
24
+ },
25
+ "server_config_limit": {
26
+ "type": "integer",
27
+ "minimum": 0,
28
+ "maximum": 9999,
29
+ "default": 100,
30
+ "title": "Limit Banned IPs in Server Configuration Files",
31
+ "description": "Limiting the number of IPs blocked by the Server Configuration Files (.htaccess and nginx.conf) will help reduce the risk of a server timeout when updating the configuration file. If the number of IPs in the banned list exceeds the Server Configuration File limit, the additional IPs will be blocked using PHP. Blocking IPs at the server level is more efficient than blocking IPs at the application level using PHP."
32
+ },
33
+ "agent_list": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "string",
37
+ "minLength": 1
38
+ },
39
+ "uniqueItems": true,
40
+ "default": [],
41
+ "title": "Ban User Agents",
42
+ "description": "Enter a list of user agents that will not be allowed access to your site. Add one user agent per-line."
43
+ }
44
+ },
45
+ "uiSchema": {
46
+ "ui:sections": [
47
+ {
48
+ "title": "Custom Bans",
49
+ "fields": [
50
+ "enable_ban_lists",
51
+ "server_config_limit",
52
+ "agent_list"
53
+ ]
54
+ }
55
+ ],
56
+ "agent_list": {
57
+ "ui:field": "TextareaListField",
58
+ "ui:rows": 10
59
+ }
60
+ }
61
+ },
62
+ "conditional-settings": {
63
+ "server_config_limit": {
64
+ "settings": {
65
+ "type": "object",
66
+ "properties": {
67
+ "enable_ban_lists": {
68
+ "type": "boolean",
69
+ "enum": [
70
+ true
71
+ ]
72
+ }
73
+ }
74
+ }
75
+ },
76
+ "agent_list": {
77
+ "settings": {
78
+ "type": "object",
79
+ "properties": {
80
+ "enable_ban_lists": {
81
+ "type": "boolean",
82
+ "enum": [
83
+ true
84
+ ]
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
core/modules/ban-users/settings-page.php DELETED
@@ -1,89 +0,0 @@
1
- <?php
2
-
3
- use iThemesSecurity\Ban_Users\Database_Repository;
4
-
5
- final class ITSEC_Ban_Users_Settings_Page extends ITSEC_Module_Settings_Page {
6
-
7
- public function __construct() {
8
- $this->id = 'ban-users';
9
- $this->title = __( 'Banned Users', 'better-wp-security' );
10
- $this->description = __( 'Block specific IP addresses and user agents from accessing the site.', 'better-wp-security' );
11
- $this->type = 'recommended';
12
-
13
- parent::__construct();
14
- }
15
-
16
- protected function render_description( $form ) {
17
-
18
- ?>
19
- <p><?php _e( 'This feature allows you to completely ban hosts and user agents from your site without having to manage any configuration of your server. Any IP addresses or user agents found in the lists below will not be allowed any access to your site.', 'better-wp-security' ); ?></p>
20
- <?php
21
-
22
- }
23
-
24
- protected function render_settings( $form ) {
25
- $hosts = ITSEC_Modules::get_container()->get( Database_Repository::class )->get_legacy_hosts();
26
- $form->set_option( 'host_list', $hosts );
27
-
28
- ?>
29
- <table class="form-table itsec-settings-section">
30
- <tr>
31
- <th scope="row"><label for="itsec-ban-users-default"><?php _e( 'Default Ban List', 'better-wp-security' ); ?></label></th>
32
- <td>
33
- <?php $form->add_checkbox( 'default' ); ?>
34
- <label for="itsec-ban-users-default"><?php _e( 'Enable HackRepair.com\'s ban list feature', 'better-wp-security' ); ?></label>
35
- <p class="description"><?php esc_html_e( 'As a getting-started point you can include the ban list developed by Jim Walker.', 'better-wp-security' ); ?></p>
36
- </td>
37
- </tr>
38
- <tr>
39
- <th scope="row"><label for="itsec-ban-users-enable_ban_lists"><?php _e( 'Ban Lists', 'better-wp-security' ); ?></label></th>
40
- <td>
41
- <?php $form->add_checkbox( 'enable_ban_lists', array( 'class' => 'itsec-settings-toggle' ) ); ?>
42
- <label for="itsec-ban-users-enable_ban_lists"><?php _e( 'Enable Ban Lists', 'better-wp-security' ); ?></label>
43
- </td>
44
- </tr>
45
- <tr class="itsec-ban-users-enable_ban_lists-content">
46
- <th scope="row"><label for="itsec-ban-users-host_list"><?php _e( 'Ban Hosts', 'better-wp-security' ); ?></label></th>
47
- <td>
48
- <?php $form->add_textarea( 'host_list', array( 'wrap' => 'off' ) ); ?>
49
- <p><?php _e( 'Use the guidelines below to enter hosts that will not be allowed access to your site.', 'better-wp-security' ); ?></p>
50
- <ul>
51
- <li>
52
- <?php _e( 'You may ban users by individual IP address or IP address range using wildcards or CIDR notation.', 'better-wp-security' ); ?>
53
- <ul>
54
- <li><?php _e( 'Individual IP addresses must be in IPv4 or IPv6 standard format (###.###.###.### or ####:####:####:####:####:####:####:####).', 'better-wp-security' ); ?></li>
55
- <li><?php _e( 'CIDR notation is allowed to specify a range of IP addresses (###.###.###.###/## or ####:####:####:####:####:####:####:####/###).', 'better-wp-security' ); ?></li>
56
- <li><?php _e( 'Wildcards are also supported with some limitations. If using wildcards (*), you must start with the right-most chunk in the IP address. For example ###.###.###.* and ###.###.*.* are permitted but ###.###.*.### is not. Wildcards are only for convenient entering of IP addresses, and will be automatically converted to their appropriate CIDR notation format on save.', 'better-wp-security' ); ?></li>
57
- </ul>
58
- </li>
59
- <li><?php _e( 'Enter only 1 IP address or 1 IP address range per line.', 'better-wp-security' ); ?></li>
60
- <li><?php _e( 'Note: You cannot ban yourself.', 'better-wp-security' ); ?></li>
61
- </ul>
62
- <p><a href="<?php echo esc_url( ITSEC_Lib::get_trace_ip_link() ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Lookup IP Address.', 'better-wp-security' ); ?></a></p>
63
- </td>
64
- </tr>
65
- <tr class="itsec-ban-users-enable_ban_lists-content">
66
- <th scope="row"><label for="itsec-ban-users-server_config_limit"><?php esc_html_e( 'Limit Banned IPs in Server Configuration Files', 'better-wp-security'); ?></label></th>
67
- <td>
68
- <?php $form->add_html5_input( 'server_config_limit', 'number', [ 'min' => 0, 'max' => 9999, 'step' => 1 ] ); ?>
69
- <p><?php esc_html_e( 'Limiting the number of IPs blocked by the Server Configuration Files (.htaccess and nginx.conf) will help reduce the risk of a server timeout when updating the configuration file.', 'better-wp-security' ); ?></p>
70
- <p><?php esc_html_e( 'If the number of IPs in the banned list exceeds the Server Configuration File limit, the additional IPs will be blocked using PHP. Blocking IPs at the server level is more efficient than blocking IPs at the application level using PHP.', 'better-wp-security' ); ?></p>
71
- </td>
72
- </tr>
73
- <tr class="itsec-ban-users-enable_ban_lists-content">
74
- <th scope="row"><label for="itsec-ban-users-agent_list"><?php _e( 'Ban User Agents', 'better-wp-security' ); ?></label></th>
75
- <td>
76
- <?php $form->add_textarea( 'agent_list', array( 'wrap' => 'off' ) ); ?>
77
- <p><?php _e( 'Use the guidelines below to enter user agents that will not be allowed access to your site.', 'better-wp-security' ); ?></p>
78
- <ul>
79
- <li><?php _e( 'Enter only 1 user agent per line.', 'better-wp-security' ); ?></li>
80
- </ul>
81
- </td>
82
- </tr>
83
- </table>
84
- <?php
85
-
86
- }
87
- }
88
-
89
- new ITSEC_Ban_Users_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/ban-users/settings.php CHANGED
@@ -1,12 +1,9 @@
1
  <?php
2
 
3
  use iThemesSecurity\Ban_Users\Database_Repository;
 
4
 
5
- final class ITSEC_Ban_Users_Settings extends ITSEC_Settings {
6
- public function get_id() {
7
- return 'ban-users';
8
- }
9
-
10
  public function get( $name, $default = null ) {
11
  if ( $name === 'host_list' ) {
12
  return ITSEC_Modules::get_container()->get( Database_Repository::class )->get_legacy_hosts();
@@ -15,14 +12,16 @@ final class ITSEC_Ban_Users_Settings extends ITSEC_Settings {
15
  return parent::get( $name, $default );
16
  }
17
 
18
- public function get_defaults() {
19
- return array(
20
- 'default' => false,
21
- 'enable_ban_lists' => true,
22
- 'agent_list' => array(),
23
- 'server_config_limit' => 100,
24
- );
 
 
25
  }
26
  }
27
 
28
- ITSEC_Modules::register_settings( new ITSEC_Ban_Users_Settings() );
1
  <?php
2
 
3
  use iThemesSecurity\Ban_Users\Database_Repository;
4
+ use iThemesSecurity\Config_Settings;
5
 
6
+ final class ITSEC_Ban_Users_Settings extends Config_Settings {
 
 
 
 
7
  public function get( $name, $default = null ) {
8
  if ( $name === 'host_list' ) {
9
  return ITSEC_Modules::get_container()->get( Database_Repository::class )->get_legacy_hosts();
12
  return parent::get( $name, $default );
13
  }
14
 
15
+ protected function handle_settings_changes( $old_settings ) {
16
+ parent::handle_settings_changes( $old_settings );
17
+
18
+ foreach ( $this->settings as $key => $val ) {
19
+ if ( ! isset( $old_settings[ $key ] ) || $old_settings[ $key ] !== $val ) {
20
+ \ITSEC_Response::regenerate_server_config();
21
+ break;
22
+ }
23
+ }
24
  }
25
  }
26
 
27
+ ITSEC_Modules::register_settings( new ITSEC_Ban_Users_Settings( ITSEC_Modules::get_config( 'ban-users' ) ) );
core/modules/brute-force/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/modules/brute-force/js/admin-brute-force.js DELETED
@@ -1,17 +0,0 @@
1
- jQuery( document ).ready( function () {
2
-
3
- jQuery( "#itsec_brute_force_enabled" ).change(function () {
4
-
5
- if ( jQuery( "#itsec_brute_force_enabled" ).is( ':checked' ) ) {
6
-
7
- jQuery( "#brute_force-settings, .itsec_brute_force_lockout_information" ).show();
8
-
9
- } else {
10
-
11
- jQuery( "#brute_force-settings, .itsec_brute_force_lockout_information" ).hide();
12
-
13
- }
14
-
15
- } ).change();
16
-
17
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/brute-force/js/index.php DELETED
@@ -1 +0,0 @@
1
- <?php //You don't belong here. ?>
 
core/modules/brute-force/module.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "brute-force",
3
+ "status": "default-active",
4
+ "type": "lockout",
5
+ "onboard": true,
6
+ "title": "Local Brute Force",
7
+ "description": "Protect your site against attackers that try to randomly guess login details to your site.",
8
+ "help": "If one had unlimited time and wanted to try an unlimited number of password combinations to get into your site they eventually would, right? This method of attack, known as a brute force attack, is something that WordPress is acutely susceptible to as, by default, the system doesn’t care how many attempts a user makes to login. It will always let you try again. Enabling login limits will ban the host user from attempting to login again after the specified bad login threshold has been reached.",
9
+ "settings": {
10
+ "type": "object",
11
+ "properties": {
12
+ "auto_ban_admin": {
13
+ "type": "boolean",
14
+ "default": false,
15
+ "title": "Automatically ban “admin” user",
16
+ "description": "Immediately ban a host that attempts to login using the “admin” username."
17
+ },
18
+ "max_attempts_host": {
19
+ "type": "integer",
20
+ "minimum": 0,
21
+ "default": 5,
22
+ "title": "Max Login Attempts Per Host",
23
+ "description": "The number of login attempts a user has before their host or computer is locked out of the system. Set to 0 to record bad login attempts without locking out the host."
24
+ },
25
+ "max_attempts_user": {
26
+ "type": "integer",
27
+ "minimum": 0,
28
+ "default": 10,
29
+ "title": "Max Login Attempts Per User",
30
+ "description": "The number of login attempts a user has before their username is locked out of the system. Note that this is different from hosts in case an attacker is using multiple computers. In addition, if they are using your login name you could be locked out yourself. Set to 0 to log bad login attempts per user without ever locking the user out (this is not recommended)."
31
+ },
32
+ "check_period": {
33
+ "type": "integer",
34
+ "minimum": 1,
35
+ "default": 5,
36
+ "title": "Minutes to Remember Bad Login (check period)",
37
+ "description": "The number of minutes in which bad logins should be remembered."
38
+ }
39
+ },
40
+ "uiSchema": {
41
+ "ui:sections": [
42
+ {
43
+ "title": "Login Attempts",
44
+ "fields": [
45
+ "max_attempts_host",
46
+ "max_attempts_user"
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ }
52
+ }
core/modules/brute-force/settings-page.php DELETED
@@ -1,66 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Brute_Force_Settings_Page extends ITSEC_Module_Settings_Page {
4
- public function __construct() {
5
- $this->id = 'brute-force';
6
- $this->title = __( 'Local Brute Force Protection', 'better-wp-security' );
7
- $this->description = __( 'Protect your site against attackers that try to randomly guess login details to your site.', 'better-wp-security' );
8
- $this->type = 'recommended';
9
-
10
- parent::__construct();
11
- }
12
-
13
- protected function render_description( $form ) {
14
-
15
- ?>
16
- <p><?php _e( 'If one had unlimited time and wanted to try an unlimited number of password combinations to get into your site they eventually would, right? This method of attack, known as a brute force attack, is something that WordPress is acutely susceptible to as, by default, the system doesn\'t care how many attempts a user makes to login. It will always let you try again. Enabling login limits will ban the host user from attempting to login again after the specified bad login threshold has been reached.', 'better-wp-security' ); ?></p>
17
- <?php
18
-
19
- }
20
-
21
- protected function render_settings( $form ) {
22
-
23
- /** @var ITSEC_Lockout $itsec_lockout */
24
- global $itsec_lockout;
25
-
26
- ?>
27
- <?php echo $itsec_lockout->get_lockout_description(); ?>
28
- <table class="form-table" id="brute_force-settings">
29
- <tr>
30
- <th scope="row"><label for="itsec-brute-force-max_attempts_host"><?php _e( 'Max Login Attempts Per Host', 'better-wp-security' ); ?></label></th>
31
- <td>
32
- <?php $form->add_text( 'max_attempts_host', array( 'class' => 'small-text' ) ); ?>
33
- <label for="itsec-brute-force-max_attempts_host"><?php _e( 'Attempts', 'better-wp-security' ); ?></label>
34
- <p class="description"><?php _e( 'The number of login attempts a user has before their host or computer is locked out of the system. Set to 0 to record bad login attempts without locking out the host.', 'better-wp-security' ); ?></p>
35
- </td>
36
- </tr>
37
- <tr>
38
- <th scope="row"><label for="itsec-brute-force-max_attempts_user"><?php _e( 'Max Login Attempts Per User', 'better-wp-security' ); ?></label></th>
39
- <td>
40
- <?php $form->add_text( 'max_attempts_user', array( 'class' => 'small-text' ) ); ?>
41
- <label for="itsec-brute-force-max_attempts_user"><?php _e( 'Attempts', 'better-wp-security' ); ?></label>
42
- <p class="description"><?php _e( 'The number of login attempts a user has before their username is locked out of the system. Note that this is different from hosts in case an attacker is using multiple computers. In addition, if they are using your login name you could be locked out yourself. Set to 0 to log bad login attempts per user without ever locking the user out (this is not recommended).', 'better-wp-security' ); ?></p>
43
- </td>
44
- </tr>
45
- <tr>
46
- <th scope="row"><label for="itsec-brute-force-check_period"><?php _e( 'Minutes to Remember Bad Login (check period)', 'better-wp-security' ); ?></label></th>
47
- <td>
48
- <?php $form->add_text( 'check_period', array( 'class' => 'small-text' ) ); ?>
49
- <label for="itsec-brute-force-check_period"><?php _e( 'Minutes', 'better-wp-security' ); ?></label>
50
- <p class="description"><?php _e( 'The number of minutes in which bad logins should be remembered.', 'better-wp-security' ); ?></p>
51
- </td>
52
- </tr>
53
- <tr>
54
- <th scope="row"><label for="itsec-brute-force-auto_ban_admin"><?php _e( 'Automatically ban "admin" user', 'better-wp-security' ); ?></label></th>
55
- <td>
56
- <?php $form->add_checkbox( 'auto_ban_admin' ); ?>
57
- <label for="itsec-brute-force-auto_ban_admin"><?php _e( 'Immediately ban a host that attempts to login using the "admin" username.', 'better-wp-security' ); ?></label>
58
- </td>
59
- </tr>
60
- </table>
61
- <?php
62
-
63
- }
64
- }
65
-
66
- new ITSEC_Brute_Force_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/brute-force/settings.php DELETED
@@ -1,18 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Brute_Force_Settings extends ITSEC_Settings {
4
- public function get_id() {
5
- return 'brute-force';
6
- }
7
-
8
- public function get_defaults() {
9
- return array(
10
- 'max_attempts_host' => 5,
11
- 'max_attempts_user' => 10,
12
- 'check_period' => 5,
13
- 'auto_ban_admin' => false,
14
- );
15
- }
16
- }
17
-
18
- ITSEC_Modules::register_settings( new ITSEC_Brute_Force_Settings() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/brute-force/setup.php CHANGED
@@ -1,100 +1,64 @@
1
  <?php
2
 
3
- if ( ! class_exists( 'ITSEC_Brute_Force_Setup' ) ) {
4
 
5
- class ITSEC_Brute_Force_Setup {
6
-
7
- private
8
- $defaults;
9
 
10
- public function __construct() {
 
 
 
 
 
 
 
11
 
12
- add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
13
- add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
14
- add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
15
- add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
 
 
 
16
 
17
- }
18
 
19
- /**
20
- * Execute module activation.
21
- *
22
- * @since 4.0
23
- *
24
- * @return void
25
- */
26
- public function execute_activate() {
27
- }
28
 
29
- /**
30
- * Execute module deactivation
31
- *
32
- * @return void
33
- */
34
- public function execute_deactivate() {
35
- }
36
 
37
- /**
38
- * Execute module uninstall
39
- *
40
- * @return void
41
- */
42
- public function execute_uninstall() {
43
 
44
- $this->execute_deactivate();
45
-
46
- delete_site_option( 'itsec_brute_force' );
47
 
 
48
  }
49
 
50
- /**
51
- * Execute module upgrade
52
- *
53
- * @return void
54
- */
55
- public function execute_upgrade( $itsec_old_version ) {
56
-
57
- if ( $itsec_old_version < 4000 ) {
58
-
59
- global $itsec_bwps_options;
60
-
61
- $current_options = get_site_option( 'itsec_brute_force' );
62
-
63
- // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
64
- if ( false !== $current_options ) {
65
-
66
- $current_options['enabled'] = isset( $itsec_bwps_options['ll_enabled'] ) && $itsec_bwps_options['ll_enabled'] == 1 ? true : false;
67
- $current_options['max_attempts_host'] = isset( $itsec_bwps_options['ll_maxattemptshost'] ) ? intval( $itsec_bwps_options['ll_maxattemptshost'] ) : 5;
68
- $current_options['max_attempts_user'] = isset( $itsec_bwps_options['ll_maxattemptsuser'] ) ? intval( $itsec_bwps_options['ll_maxattemptsuser'] ) : 10;
69
- $current_options['check_period'] = isset( $itsec_bwps_options['ll_checkinterval'] ) ? intval( $itsec_bwps_options['ll_checkinterval'] ) : 5;
70
-
71
- update_site_option( 'itsec_brute_force', $current_options );
72
 
 
 
 
 
 
 
 
73
  }
74
- }
75
-
76
- if ( $itsec_old_version < 4041 ) {
77
- $current_options = get_site_option( 'itsec_brute_force' );
78
 
79
- // If there are no current options, go with the new defaults by not saving anything
80
- if ( is_array( $current_options ) ) {
81
- // Make sure the new module is properly activated or deactivated
82
- if ( $current_options['enabled'] ) {
83
- ITSEC_Modules::activate( 'brute-force' );
84
- } else {
85
- ITSEC_Modules::deactivate( 'brute-force' );
86
- }
87
-
88
- // remove 'enabled' which isn't use in the new module
89
- unset( $current_options['enabled'] );
90
- ITSEC_Modules::set_settings( 'brute-force', $current_options );
91
- }
92
  }
93
-
94
  }
95
-
96
  }
97
-
98
  }
99
 
100
  new ITSEC_Brute_Force_Setup();
1
  <?php
2
 
3
+ class ITSEC_Brute_Force_Setup {
4
 
5
+ public function __construct() {
6
+ add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
7
+ add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ) );
8
+ }
9
 
10
+ /**
11
+ * Execute module uninstall
12
+ *
13
+ * @return void
14
+ */
15
+ public function execute_uninstall() {
16
+ delete_site_option( 'itsec_brute_force' );
17
+ }
18
 
19
+ /**
20
+ * Execute module upgrade
21
+ *
22
+ * @return void
23
+ */
24
+ public function execute_upgrade( $itsec_old_version ) {
25
+ if ( $itsec_old_version < 4000 ) {
26
 
27
+ global $itsec_bwps_options;
28
 
29
+ $current_options = get_site_option( 'itsec_brute_force' );
 
 
 
 
 
 
 
 
30
 
31
+ // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
32
+ if ( false !== $current_options ) {
 
 
 
 
 
33
 
34
+ $current_options['enabled'] = isset( $itsec_bwps_options['ll_enabled'] ) && $itsec_bwps_options['ll_enabled'] == 1 ? true : false;
35
+ $current_options['max_attempts_host'] = isset( $itsec_bwps_options['ll_maxattemptshost'] ) ? intval( $itsec_bwps_options['ll_maxattemptshost'] ) : 5;
36
+ $current_options['max_attempts_user'] = isset( $itsec_bwps_options['ll_maxattemptsuser'] ) ? intval( $itsec_bwps_options['ll_maxattemptsuser'] ) : 10;
37
+ $current_options['check_period'] = isset( $itsec_bwps_options['ll_checkinterval'] ) ? intval( $itsec_bwps_options['ll_checkinterval'] ) : 5;
 
 
38
 
39
+ update_site_option( 'itsec_brute_force', $current_options );
 
 
40
 
41
+ }
42
  }
43
 
44
+ if ( $itsec_old_version < 4041 ) {
45
+ $current_options = get_site_option( 'itsec_brute_force' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ // If there are no current options, go with the new defaults by not saving anything
48
+ if ( is_array( $current_options ) ) {
49
+ // Make sure the new module is properly activated or deactivated
50
+ if ( $current_options['enabled'] ) {
51
+ ITSEC_Modules::activate( 'brute-force' );
52
+ } else {
53
+ ITSEC_Modules::deactivate( 'brute-force' );
54
  }
 
 
 
 
55
 
56
+ // remove 'enabled' which isn't use in the new module
57
+ unset( $current_options['enabled'] );
58
+ ITSEC_Modules::set_settings( 'brute-force', $current_options );
 
 
 
 
 
 
 
 
 
 
59
  }
 
60
  }
 
61
  }
 
62
  }
63
 
64
  new ITSEC_Brute_Force_Setup();
core/modules/brute-force/validator.php DELETED
@@ -1,17 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Brute_Force_Validator extends ITSEC_Validator {
4
- public function get_id() {
5
- return 'brute-force';
6
- }
7
-
8
- protected function sanitize_settings() {
9
- $this->sanitize_setting( 'positive-int', 'max_attempts_host', __( 'Max Login Attempts Per Host', 'better-wp-security' ) );
10
- $this->sanitize_setting( 'positive-int', 'max_attempts_user', __( 'Max Login Attempts Per User', 'better-wp-security' ) );
11
- $this->sanitize_setting( 'positive-int', 'check_period', __( 'Minutes to Remember Bad Login (check period)', 'better-wp-security' ) );
12
-
13
- $this->sanitize_setting( 'bool', 'auto_ban_admin', __( 'Automatically ban "admin" user', 'better-wp-security' ) );
14
- }
15
- }
16
-
17
- ITSEC_Modules::register_validator( new ITSEC_Brute_Force_Validator() );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/content-directory/index.php CHANGED
@@ -1 +1 @@
1
- <?php //You don't belong here. ?>
1
+ <?php // Silence is golden.
core/modules/content-directory/module.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "admin-user",
3
+ "type": "tool",
4
+ "status": "default-inactive",
5
+ "deprecated": "8.0.0",
6
+ "title": "Change Content Directory",
7
+ "description": "Advanced feature to rename the wp-content directory to a different name."
8
+ }
core/modules/content-directory/settings-page.php DELETED
@@ -1,125 +0,0 @@
1
- <?php
2
-
3
- final class ITSEC_Content_Directory_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $version = 1;
5
-
6
-
7
- public function __construct() {
8
- $this->id = 'content-directory';
9
- $this->title = __( 'Change Content Directory', 'better-wp-security' );
10
- $this->description = __( 'Advanced feature to rename the wp-content directory to a different name.', 'better-wp-security' );
11
- $this->type = 'advanced';
12
-
13
- parent::__construct();
14
- }
15
-
16
- protected function render_description( $form ) {
17
-
18
- ?>
19
- <p><?php _e( 'Change the location of the <code>wp-content</code> directory so that it uses a different name.', 'better-wp-security' ); ?></p>
20
- <?php
21
-
22
- }
23
-
24
- private function show_current_wp_content_dir() {
25
- $dir_name = substr( WP_CONTENT_DIR, strrpos( WP_CONTENT_DIR, '/' ) + 1 );
26
-
27
- ?>
28
- <p><?php printf( __( 'The <code>wp-content</code> directory is available at <code>%s</code>.', 'better-wp-security' ), esc_html( $dir_name ) ); ?></p>
29
- <?php
30
-
31
- }
32
-
33
- protected function render_settings( $form ) {
34
- require_once( dirname( __FILE__ ) . '/utility.php' );
35
-
36
- $yes_or_no = array(
37
- 'yes' => __( 'Yes', 'better-wp-security' ),
38
- 'no' => __( 'No', 'better-wp-security' ),
39
- );
40
-
41
- $form->set_option( 'undo_change', 'no' );
42
-
43
- ?>
44
- <?php if ( ITSEC_Content_Directory_Utility::is_custom_directory() && ! ITSEC_Content_Directory_Utility::is_modified_by_it_security() ) : ?>
45
- <?php $this->show_current_wp_content_dir(); ?>
46
- <p><?php _e( 'The content directory was changed by something other than iThemes Security. No further actions are available on this page.', 'better-wp-security' ); ?></p>
47
- <?php else : ?>
48
- <div class="itsec-write-files-disabled">
49
- <div class="itsec-warning-message"><?php _e( 'The "Write to Files" setting is disabled in Global Settings. In order to use this feature, you must enable the "Write to Files" setting.', 'better-wp-security' ); ?></div>
50
- </div>
51
-
52
- <div class="itsec-write-files-enabled">
53
- <?php if ( ITSEC_Content_Directory_Utility::is_custom_directory() || ITSEC_Content_Directory_Utility::is_modified_by_it_security() ) : ?>
54
- <?php $this->show_current_wp_content_dir(); ?>
55
-
56
- <div class="itsec-warning-message"><?php printf( __( '<span>IMPORTANT:</span> Ensure that you <a href="%s" data-module-link="backup">create a database backup</a> before undoing the Content Directory change.', 'better-wp-security' ), ITSEC_Core::get_backup_creation_page_url() ); ?></div>
57
- <div class="itsec-warning-message"><?php _e( '<span>WARNING:</span> Undoing the Content Directory change when images and other content were added after the change <strong>will break your site</strong>. Only undo the Content Directory change if absolutely necessary.', 'better-wp-security' ); ?></div>
58
-
59
- <table class="form-table itsec-settings-section">
60
- <tr>
61
- <th scope="row"><label for="itsec-content-directory-undo_change"><?php _e( 'Undo Content Directory Change', 'better-wp-security' ); ?></label></th>
62
- <td>
63
- <?php $form->add_select( 'undo_change', $yes_or_no ); ?>
64
- <p class="description"><?php _e( 'Select "Yes" and save the settings to undo the content directory change.', 'better-wp-security' ); ?></p>
65
- </td>
66
- </tr>
67
- </table>
68
- <?php else : ?>
69
- <p><?php _e( 'By default, WordPress stores files for plugins, themes, and uploads in a directory called <code>wp-content</code>. Some older and less intelligent bots hard coded this directory in order to look for vulnerable files. Modern bots are intelligent enough to locate this folder programmatically, thus changing the Content Directory is no longer a recommended security step.', 'better-wp-security' ); ?></p>
70
- <p><?php _e( 'This tool provides an undo feature after changing the Content Directory. Since not all plugins, themes, or site contents function properly with a renamed Content Directory, please verify that the site is functioning correctly after the change. If any issues are encountered, the undo feature should be used to undo the change. Please note that the undo feature is only available when the changes added to the <code>wp-config.php</code> file for this feature are unmodified.', 'better-wp-security' ); ?></p>
71
- <div class="itsec-warning-message"><?php _e( '<span>IMPORTANT:</span> Deactivating or uninstalling this plugin will not revert the changes made by this feature.', 'better-wp-security' ); ?></div>
72
- <div class="itsec-warning-message"><?php printf( __( '<span>IMPORTANT:</span> Ensure that you <a href="%s">create a database backup</a> before changing the Content Directory.', 'better-wp-security' ), ITSEC_Core::get_backup_creation_page_url() ); ?></div>
73
- <div class="itsec-warning-message"><?php _e( '<span>WARNING:</span> Changing the name of the Content Directory on a site that already has images and other content referencing it <strong>will break your site</strong>. For this reason, we highly recommend only changing the Content Directory on a fresh WordPress install.', 'better-wp-security' ); ?></div>
74
-
75
- <table class="form-table itsec-settings-section">
76
- <tr>
77
- <th scope="row"><label for="itsec-content-directory-new_directory_name"><?php _e( 'New Directory Name', 'better-wp-security' ); ?></label></th>
78
- <td>
79
- <?php $form->add_text( 'new_directory_name' ); ?>
80
- <br />
81
- <p class="description"><?php _e( 'Supply a new directory name and save the settings to change the location of the <code>wp-content</code> directory. You may need to log in again after performing this operation.', 'better-wp-security' ); ?></p>
82
- </td>
83
- </tr>
84
- </table>
85
- <?php endif; ?>
86
- </div>
87
- <?php endif; ?>
88
- <?php
89
-
90
- }
91
-
92
- public function handle_form_post( $data ) {
93
- require_once( dirname( __FILE__ ) . '/utility.php' );
94
-
95
- if ( ! empty( $data['new_directory_name'] ) ) {
96
- $results = ITSEC_Content_Directory_Utility::change_content_directory( $data['new_directory_name'] );
97
-
98
- if ( is_wp_error( $results ) ) {
99
- ITSEC_Response::add_error( $results );
100
- ITSEC_Response::add_error( new WP_Error( 'itsec-content-directory-settings-page-unable-to-change-content-directory', __( 'Unable to change the content directory. If the above error cannot be fixed, you may need to manually change the content directory. Instructions on how to change the content directory manually can be found <a href="https://codex.wordpress.org/Editing_wp-config.php#Moving_wp-content_folder">here</a>.', 'better-wp-security' ) ) );
101
- ITSEC_Response::set_success( false );
102
- } else {
103
- /* translators: 1: New directory name */
104
- ITSEC_Response::add_message( sprintf( __( 'The content directory was successfully changed to <code>%1$s</code>.', 'better-wp-security' ), $results ) );
105
-
106
- ITSEC_Response::reload_module( $this->id );
107
- }
108
- } else if ( isset( $data['undo_change'] ) && 'yes' === $data['undo_change'] ) {
109
- $results = ITSEC_Content_Directory_Utility::change_content_directory( 'wp-content' );
110
-
111
- if ( is_wp_error( $results ) ) {
112
- ITSEC_Response::add_error( $results );
113
- ITSEC_Response::add_error( new WP_Error( 'itsec-content-directory-settings-page-unable-to-undo-content-directory-change', __( 'Unable to change the content directory back to <code>wp-content</code>. If the above error cannot be fixed, you may need to manually change the content directory. Instructions on how to change the content directory manually can be found <a href="https://codex.wordpress.org/Editing_wp-config.php#Moving_wp-content_folder">here</a>.', 'better-wp-security' ) ) );
114
- ITSEC_Response::set_success( false );
115
- } else {
116
- /* translators: 1: New directory name */
117
- ITSEC_Response::add_message( sprintf( __( 'The content directory was successfully changed back to <code>%1$s</code>.', 'better-wp-security' ), $results ) );
118
-
119
- ITSEC_Response::reload_module( $this->id );
120
- }
121
- }
122
- }
123
- }
124
-
125
- new ITSEC_Content_Directory_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/class-itsec-admin-notices.php CHANGED
@@ -13,14 +13,6 @@ class ITSEC_Admin_Notices {
13
  if ( isset( $_GET['action'] ) && self::ACTION === $_GET['action'] ) {
14
  add_action( 'admin_init', array( $this, 'handle_admin_action' ) );
15
  }
16
-
17
- if ( ITSEC_Modules::get_setting( 'global', 'hide_admin_bar' ) ) {
18
- if ( is_multisite() ) {
19
- add_action( 'network_admin_notices', array( $this, 'display_notices' ) );
20
- } else {
21
- add_action( 'admin_notices', array( $this, 'display_notices' ) );
22
- }
23
- }
24
  }
25
 
26
  public function rest_api_init() {
13
  if ( isset( $_GET['action'] ) && self::ACTION === $_GET['action'] ) {
14
  add_action( 'admin_init', array( $this, 'handle_admin_action' ) );
15
  }
 
 
 
 
 
 
 
 
16
  }
17
 
18
  public function rest_api_init() {
core/modules/core/class-itsec-core-active.php CHANGED
@@ -2,11 +2,16 @@
2
 
3
  class ITSEC_Core_Active {
4
 
 
 
 
5
  public function run() {
6
  add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
7
  add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
8
  add_action( 'login_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
9
  add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
 
 
10
  }
11
 
12
  public function rest_api_init() {
@@ -27,9 +32,16 @@ class ITSEC_Core_Active {
27
 
28
  foreach ( $manifest as $name => $config ) {
29
  foreach ( $config['files'] as $file ) {
30
- $handle = $this->name_to_handle( $name );
 
31
 
32
- if ( $script_debug && file_exists( $dir . $file ) ) {
 
 
 
 
 
 
33
  $path = 'dist/' . $file;
34
  $is_debug = true;
35
  } else {
@@ -52,6 +64,10 @@ class ITSEC_Core_Active {
52
 
53
  $deps = $is_js ? $config['dependencies'] : array();
54
 
 
 
 
 
55
  if ( $is_css && in_array( 'wp-components', $config['dependencies'], true ) ) {
56
  $deps[] = 'wp-components';
57
  }
@@ -66,17 +82,15 @@ class ITSEC_Core_Active {
66
  $deps[ $i ] = $this->name_to_handle( "{$parts[1]}/{$parts[2]}" );
67
  }
68
 
69
- if ( ! $is_debug ) {
70
- foreach ( array_reverse( $config['vendors'] ) as $vendor ) {
71
- if ( ! isset( $manifest[ $vendor ] ) ) {
72
- continue;
73
- }
74
-
75
- if ( $is_js && $this->has_js( $manifest[ $vendor ]['files'] ) ) {
76
- $deps[] = $this->name_to_handle( $vendor );
77
- } elseif ( $is_css && $this->has_css( $manifest[ $vendor ]['files'] ) ) {
78
- $deps[] = $this->name_to_handle( $vendor );
79
- }
80
  }
81
  }
82
 
@@ -96,7 +110,7 @@ class ITSEC_Core_Active {
96
  );
97
  }
98
 
99
- if ( function_exists( 'wp_set_script_translations' ) && ! ITSEC_Core::is_pro() && in_array( 'wp-i18n', $deps, true ) ) {
100
  wp_set_script_translations( $handle, 'better-wp-security' );
101
  }
102
 
@@ -108,6 +122,26 @@ class ITSEC_Core_Active {
108
  }
109
  }
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  private function has_js( $files ) {
112
  foreach ( $files as $file ) {
113
  if ( ITSEC_Lib::str_ends_with( $file, '.js' ) ) {
2
 
3
  class ITSEC_Core_Active {
4
 
5
+ /** @var string[] */
6
+ private $handles = [];
7
+
8
  public function run() {
9
  add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
10
  add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
11
  add_action( 'login_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
12
  add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ), 0 );
13
+ add_action( 'wp_footer', array( $this, 'add_live_reload' ), 1000 );
14
+ add_action( 'admin_footer', array( $this, 'add_live_reload' ), 1000 );
15
  }
16
 
17
  public function rest_api_init() {
32
 
33
  foreach ( $manifest as $name => $config ) {
34
  foreach ( $config['files'] as $file ) {
35
+ $handle = $this->name_to_handle( $name );
36
+ $this->handles[] = $handle;
37
 
38
+ if ( ! ITSEC_Core::is_pro() ) {
39
+ // WordPress.org installs always use non-minified file names.
40
+ // This is to allow for WP-CLI to scan the files since, by default
41
+ // minified JS files are excluded.
42
+ $path = 'dist/' . $file;
43
+ $is_debug = false;
44
+ } elseif ( $script_debug && file_exists( $dir . $file ) ) {
45
  $path = 'dist/' . $file;
46
  $is_debug = true;
47
  } else {
64
 
65
  $deps = $is_js ? $config['dependencies'] : array();
66
 
67
+ if ( $is_js && 'runtime' !== $name ) {
68
+ $deps[] = $this->name_to_handle( 'runtime' );
69
+ }
70
+
71
  if ( $is_css && in_array( 'wp-components', $config['dependencies'], true ) ) {
72
  $deps[] = 'wp-components';
73
  }
82
  $deps[ $i ] = $this->name_to_handle( "{$parts[1]}/{$parts[2]}" );
83
  }
84
 
85
+ foreach ( array_reverse( $config['vendors'] ) as $vendor ) {
86
+ if ( ! isset( $manifest[ $vendor ] ) || $name === $vendor ) {
87
+ continue;
88
+ }
89
+
90
+ if ( $is_js && $this->has_js( $manifest[ $vendor ]['files'] ) ) {
91
+ $deps[] = $this->name_to_handle( $vendor );
92
+ } elseif ( $is_css && $this->has_css( $manifest[ $vendor ]['files'] ) ) {
93
+ $deps[] = $this->name_to_handle( $vendor );
 
 
94
  }
95
  }
96
 
110
  );
111
  }
112
 
113
+ if ( in_array( 'wp-i18n', $deps, true ) ) {
114
  wp_set_script_translations( $handle, 'better-wp-security' );
115
  }
116
 
122
  }
123
  }
124
 
125
+ public function add_live_reload() {
126
+ if ( ! ITSEC_Core::is_development() ) {
127
+ return;
128
+ }
129
+
130
+ foreach ( $this->handles as $handle ) {
131
+ if ( wp_script_is( $handle ) ) {
132
+ $url = 'http://localhost:35729/livereload.js';
133
+
134
+ if ( is_ssl() ) {
135
+ $url = set_url_scheme( $url, 'https' );
136
+ }
137
+
138
+ echo '<script src="' . esc_url( $url ) . '" async></script>';
139
+
140
+ return;
141
+ }
142
+ }
143
+ }
144
+
145
  private function has_js( $files ) {
146
  foreach ( $files as $file ) {
147
  if ( ITSEC_Lib::str_ends_with( $file, '.js' ) ) {
core/modules/core/class-itsec-core-admin.php CHANGED
@@ -9,9 +9,6 @@ class ITSEC_Core_Admin {
9
  add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 9999 );
10
  add_action( 'admin_footer', array( $this, 'render_notices_root' ) );
11
 
12
- add_action( 'itsec-settings-page-init', array( $this, 'init_settings_page' ) );
13
- add_action( 'itsec-logs-page-init', array( $this, 'init_settings_page' ) );
14
-
15
  if ( ! ITSEC_Core::is_pro() ) {
16
  add_filter( 'itsec_meta_links', array( $this, 'add_plugin_meta_links' ) );
17
  }
@@ -54,21 +51,6 @@ class ITSEC_Core_Admin {
54
  return ITSEC_Core::current_user_can_manage() && ! ITSEC_Modules::get_setting( 'global', 'hide_admin_bar' );
55
  }
56
 
57
- public function init_settings_page() {
58
- if ( ! class_exists( 'backupbuddy_api' ) ) {
59
- require_once( dirname( __FILE__ ) . '/sidebar-widget-backupbuddy-cross-promo.php' );
60
- }
61
-
62
- if ( ITSEC_Core::is_pro() ) {
63
- return;
64
- }
65
-
66
- require_once( dirname( __FILE__ ) . '/sidebar-widget-pro-upsell.php' );
67
- require_once( dirname( __FILE__ ) . '/sidebar-widget-sync-cross-promo.php' );
68
- require_once( dirname( __FILE__ ) . '/sidebar-widget-mail-list-signup.php' );
69
- require_once( dirname( __FILE__ ) . '/sidebar-widget-support.php' );
70
- }
71
-
72
  /**
73
  * Adds links to the plugin row meta
74
  *
9
  add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 9999 );
10
  add_action( 'admin_footer', array( $this, 'render_notices_root' ) );
11
 
 
 
 
12
  if ( ! ITSEC_Core::is_pro() ) {
13
  add_filter( 'itsec_meta_links', array( $this, 'add_plugin_meta_links' ) );
14
  }
51
  return ITSEC_Core::current_user_can_manage() && ! ITSEC_Modules::get_setting( 'global', 'hide_admin_bar' );
52
  }
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  /**
55
  * Adds links to the plugin row meta
56
  *
core/modules/core/class-itsec-rest-actor-types-controller.php CHANGED
@@ -29,7 +29,7 @@ class ITSEC_REST_Actor_Types_Controller extends WP_REST_Controller {
29
  }
30
 
31
  public function get_items_permissions_check( $request ) {
32
- return ITSEC_Core::current_user_can_manage();
33
  }
34
 
35
  public function get_items( $request ) {
29
  }
30
 
31
  public function get_items_permissions_check( $request ) {
32
+ return ITSEC_Core::current_user_can_manage() || current_user_can( 'itsec_dashboard_access' );
33
  }
34
 
35
  public function get_items( $request ) {
core/modules/core/class-itsec-rest-actors-controller.php CHANGED
@@ -30,7 +30,7 @@ class ITSEC_REST_Actors_Controller extends WP_REST_Controller {
30
  }
31
 
32
  public function get_items_permissions_check( $request ) {
33
- return ITSEC_Core::current_user_can_manage();
34
  }
35
 
36
  public function get_items( $request ) {
30
  }
31
 
32
  public function get_items_permissions_check( $request ) {
33
+ return ITSEC_Core::current_user_can_manage() || current_user_can( 'itsec_dashboard_access' );
34
  }
35
 
36
  public function get_items( $request ) {
core/modules/core/entries/admin-notices-dashboard-admin-bar.js CHANGED
@@ -7,7 +7,15 @@ import { registerPlugin } from '@wordpress/plugins';
7
  * Internal dependencies
8
  */
9
  import '@ithemes/security.core.admin-notices-api';
10
- import AdminBar from './admin-notices/components/admin-bar';
 
 
11
  registerPlugin( 'itsec-admin-notices-dashboard-admin-bar', {
12
- render: AdminBar,
 
 
 
 
 
 
13
  } );
7
  * Internal dependencies
8
  */
9
  import '@ithemes/security.core.admin-notices-api';
10
+ import { AdminBarFill } from '@ithemes/security.dashboard.api';
11
+ import ToolbarButton from './admin-notices/components/toolbar-button';
12
+
13
  registerPlugin( 'itsec-admin-notices-dashboard-admin-bar', {
14
+ render() {
15
+ return (
16
+ <AdminBarFill>
17
+ <ToolbarButton />
18
+ </AdminBarFill>
19
+ );
20
+ },
21
  } );
core/modules/core/entries/admin-notices.js CHANGED
@@ -14,7 +14,9 @@ setLocaleData( { '': {} }, 'better-wp-security' );
14
  import App from './admin-notices/app.js';
15
 
16
  domReady( () => {
17
- const containerEl = document.getElementById( 'wp-admin-bar-itsec_admin_bar_menu' );
 
 
18
  const portalEl = document.getElementById( 'itsec-admin-notices-root' );
19
 
20
  return render( <App portalEl={ portalEl } />, containerEl );
14
  import App from './admin-notices/app.js';
15
 
16
  domReady( () => {
17
+ const containerEl = document.getElementById(
18
+ 'wp-admin-bar-itsec_admin_bar_menu'
19
+ );
20
  const portalEl = document.getElementById( 'itsec-admin-notices-root' );
21
 
22
  return render( <App portalEl={ portalEl } />, containerEl );
core/modules/core/entries/admin-notices/components/admin-bar/index.js DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- * External dependencies
3
- */
4
- import classnames from 'classnames';
5
-
6
- /**
7
- * WordPress dependencies
8
- */
9
- import { Button, Dashicon, Popover } from '@wordpress/components';
10
- import { __ } from '@wordpress/i18n';
11
- import { compose, withState } from '@wordpress/compose';
12
- import { withSelect } from '@wordpress/data';
13
-
14
- /**
15
- * Internal dependencies
16
- */
17
- import { AdminBarFill } from '@ithemes/security.dashboard.api';
18
- import { doesElementBelongToPanel } from '../../utils';
19
- import Panel from '../panel';
20
- import './style.scss';
21
-
22
- function AdminBar( { notices, noticesLoaded, isToggled, setState } ) {
23
- return (
24
- <AdminBarFill>
25
- <div className="itsec-admin-bar__admin-notices">
26
- <div className={ classnames( 'itsec-admin-bar-admin-notices__trigger', { 'itsec-admin-bar-admin-notices__trigger--has-notices': notices.length > 0 } ) }>
27
- <Button aria-expanded={ isToggled } onClick={ () => setState( { isToggled: ! isToggled } ) } isSecondary>
28
- <Dashicon icon="megaphone" size={ 15 } />
29
- { __( 'Notifications', 'better-wp-security' ) }
30
- </Button>
31
- { isToggled && (
32
- <Popover
33
- className="itsec-admin-bar-admin-notices__content"
34
- expandOnMobile
35
- focusOnMount="container"
36
- position="bottom left"
37
- headerTitle={ __( 'Notifications', 'better-wp-security' ) }
38
- onClose={ () => setState( { isToggled: false } ) }
39
- onClickOutside={ ( e ) => {
40
- if (
41
- ! e.target || (
42
- e.target.id !== 'itsec-admin-notices-toolbar-trigger' &&
43
- e.target.parentNode.id !== 'itsec-admin-notices-toolbar-trigger' &&
44
- ! doesElementBelongToPanel( e.target )
45
- )
46
- ) {
47
- setState( { isToggled: false } );
48
- }
49
- } }
50
- onFocusOutside={ () => {
51
- const activeElement = document.activeElement;
52
-
53
- if (
54
- activeElement.id !== 'itsec-admin-notices-toolbar-trigger' &&
55
- ( ! activeElement.parentNode || activeElement.parentNode.id !== 'itsec-admin-notices-toolbar-trigger' ) &&
56
- ! doesElementBelongToPanel( activeElement )
57
- ) {
58
- setState( { isToggled: false } );
59
- }
60
- } }
61
- >
62
- <Panel notices={ notices } loaded={ noticesLoaded } close={ () => setState( { isToggled: false } ) } />
63
- </Popover>
64
- ) }
65
- </div>
66
- </div>
67
- </AdminBarFill>
68
- );
69
- }
70
-
71
- export default compose( [
72
- withSelect( ( select ) => ( {
73
- notices: select( 'ithemes-security/admin-notices' ).getNotices(),
74
- noticesLoaded: select( 'ithemes-security/admin-notices' ).areNoticesLoaded(),
75
- } ) ),
76
- withState( { isToggled: false } ),
77
- ] )( AdminBar );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/entries/admin-notices/components/admin-bar/style.scss DELETED
@@ -1,56 +0,0 @@
1
- @import "colors.scss";
2
- @import "breakpoints.scss";
3
-
4
- .itsec-admin-bar__admin-notices {
5
- margin-left: auto;
6
-
7
- & .itsec-admin-bar-admin-notices__trigger > .components-button {
8
- color: $main-blue;
9
- border: none;
10
- background: transparent !important;
11
- box-shadow: none;
12
-
13
- &:hover {
14
- color: lighten($main-blue, 20%);
15
- box-shadow: none;
16
- }
17
-
18
- & svg {
19
- margin-right: .5em
20
- }
21
- }
22
-
23
- & .itsec-admin-bar-admin-notices__trigger .components-button {
24
- position: relative;
25
- }
26
-
27
- & .itsec-admin-bar-admin-notices__trigger .components-button::before {
28
- content: '';
29
- background: #d8514f;
30
- height: 5px;
31
- width: 5px;
32
- border-radius: 5px;
33
- vertical-align: middle;
34
- border: 1px solid #f1f1f1;
35
- z-index: 1;
36
- position: absolute;
37
- left: 4px;
38
- top: 3px;
39
- opacity: 0;
40
- transition: opacity 1000ms ease-in-out;
41
- }
42
-
43
- & .itsec-admin-bar-admin-notices__trigger--has-notices .components-button::before {
44
- opacity: 1;
45
- }
46
- }
47
-
48
- .itsec-admin-bar-admin-notices__content .components-popover__content {
49
- box-shadow: rgba(0, 0, 0, 0.19) 0 4px 5px;
50
- }
51
-
52
- @media screen and (max-width: $small) {
53
- .itsec-admin-bar .itsec-admin-bar__admin-notices {
54
- margin-left: 0;
55
- }
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/entries/admin-notices/components/notice-actions/index.js CHANGED
@@ -6,7 +6,12 @@ import { isEmpty, map } from 'lodash';
6
  /**
7
  * WordPress dependencies
8
  */
9
- import { Dropdown, NavigableMenu, IconButton, Button, Spinner } from '@wordpress/components';
 
 
 
 
 
10
  import { __ } from '@wordpress/i18n';
11
  import { compose } from '@wordpress/compose';
12
  import { withDispatch, withSelect } from '@wordpress/data';
@@ -27,25 +32,30 @@ function NoticeActions( { notice, doAction, inProgress } ) {
27
  const action = notice.actions[ slug ];
28
 
29
  if ( action.style === 'close' ) {
30
- actions.push( (
31
- <li key={ slug } >
32
- <IconButton icon="dismiss" label={ action.title } onClick={ () => doAction( notice.id, slug ) } isBusy={ inProgress.includes( slug ) } />
 
 
 
 
 
33
  </li>
34
- ) );
35
  }
36
  }
37
 
38
  const generic = getGenericActions( notice );
39
 
40
  if ( ! isEmpty( generic ) ) {
41
- actions.push( (
42
  <li key="more">
43
  <Dropdown
44
  position="bottom right"
45
  className="itsec-admin-notice-list-actions__more-menu"
46
  contentClassName="itsec-admin-notice-list-actions__more-menu-items"
47
  renderToggle={ ( { isOpen, onToggle } ) => (
48
- <IconButton
49
  icon="ellipsis"
50
  label={ __( 'More Actions', 'better-wp-security' ) }
51
  onClick={ onToggle }
@@ -55,20 +65,31 @@ function NoticeActions( { notice, doAction, inProgress } ) {
55
  ) }
56
  renderContent={ () => (
57
  <NavigableMenu role="menu">
58
- { map( generic, ( action, slug ) => (
59
- action.uri ?
60
- <Button key={ slug } href={ action.uri }>{ action.title }</Button> :
61
- <Button key={ slug } onClick={ () => doAction( notice.id, slug ) } disabled={ inProgress.includes( slug ) }>
62
  { action.title }
63
- { inProgress.includes( slug ) && <Spinner /> }
64
  </Button>
65
- ) ) }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </NavigableMenu>
67
  ) }
68
  />
69
-
70
  </li>
71
- ) );
72
  }
73
 
74
  return <ul className="itsec-admin-notice-list-actions">{ actions }</ul>;
@@ -79,7 +100,9 @@ export default compose( [
79
  doAction: dispatch( 'ithemes-security/admin-notices' ).doNoticeAction,
80
  } ) ),
81
  withSelect( ( select, ownProps ) => ( {
82
- inProgress: select( 'ithemes-security/admin-notices' ).getInProgressActions( ownProps.notice.id ),
 
 
83
  } ) ),
84
  ] )( NoticeActions );
85
 
6
  /**
7
  * WordPress dependencies
8
  */
9
+ import {
10
+ Dropdown,
11
+ NavigableMenu,
12
+ Button,
13
+ Spinner,
14
+ } from '@wordpress/components';
15
  import { __ } from '@wordpress/i18n';
16
  import { compose } from '@wordpress/compose';
17
  import { withDispatch, withSelect } from '@wordpress/data';
32
  const action = notice.actions[ slug ];
33
 
34
  if ( action.style === 'close' ) {
35
+ actions.push(
36
+ <li key={ slug }>
37
+ <Button
38
+ icon="dismiss"
39
+ label={ action.title }
40
+ onClick={ () => doAction( notice.id, slug ) }
41
+ isBusy={ inProgress.includes( slug ) }
42
+ />
43
  </li>
44
+ );
45
  }
46
  }
47
 
48
  const generic = getGenericActions( notice );
49
 
50
  if ( ! isEmpty( generic ) ) {
51
+ actions.push(
52
  <li key="more">
53
  <Dropdown
54
  position="bottom right"
55
  className="itsec-admin-notice-list-actions__more-menu"
56
  contentClassName="itsec-admin-notice-list-actions__more-menu-items"
57
  renderToggle={ ( { isOpen, onToggle } ) => (
58
+ <Button
59
  icon="ellipsis"
60
  label={ __( 'More Actions', 'better-wp-security' ) }
61
  onClick={ onToggle }
65
  ) }
66
  renderContent={ () => (
67
  <NavigableMenu role="menu">
68
+ { map( generic, ( action, slug ) =>
69
+ action.uri ? (
70
+ <Button key={ slug } href={ action.uri }>
 
71
  { action.title }
 
72
  </Button>
73
+ ) : (
74
+ <Button
75
+ key={ slug }
76
+ onClick={ () =>
77
+ doAction( notice.id, slug )
78
+ }
79
+ disabled={ inProgress.includes( slug ) }
80
+ >
81
+ { action.title }
82
+ { inProgress.includes( slug ) && (
83
+ <Spinner />
84
+ ) }
85
+ </Button>
86
+ )
87
+ ) }
88
  </NavigableMenu>
89
  ) }
90
  />
 
91
  </li>
92
+ );
93
  }
94
 
95
  return <ul className="itsec-admin-notice-list-actions">{ actions }</ul>;
100
  doAction: dispatch( 'ithemes-security/admin-notices' ).doNoticeAction,
101
  } ) ),
102
  withSelect( ( select, ownProps ) => ( {
103
+ inProgress: select(
104
+ 'ithemes-security/admin-notices'
105
+ ).getInProgressActions( ownProps.notice.id ),
106
  } ) ),
107
  ] )( NoticeActions );
108
 
core/modules/core/entries/admin-notices/components/notice-actions/style.scss CHANGED
@@ -1,12 +1,10 @@
1
- @import "colors.scss";
2
 
3
  .itsec-admin-notice-list-actions {
4
- & .components-icon-button {
5
  padding: 2px !important;
6
 
7
- &:hover {
8
- background-color: darken(#E5EAEE, 10) !important;
9
- box-shadow: none !important;
10
  }
11
  }
12
  }
 
1
 
2
  .itsec-admin-notice-list-actions {
3
+ & .components-button.has-icon {
4
  padding: 2px !important;
5
 
6
+ .dashicon {
7
+ margin: 0;
 
8
  }
9
  }
10
  }
core/modules/core/entries/admin-notices/components/notice-list/index.js CHANGED
@@ -9,7 +9,10 @@ function NoticeList( { notices } ) {
9
  return (
10
  <ul className="itsec-admin-notice-list">
11
  { notices.map( ( notice ) => (
12
- <li className="itsec-admin-notice-list-item-container" key={ notice.id }>
 
 
 
13
  <NoticeActions notice={ notice } />
14
  <Notice notice={ notice } noticeId={ notice.id } />
15
  </li>
9
  return (
10
  <ul className="itsec-admin-notice-list">
11
  { notices.map( ( notice ) => (
12
+ <li
13
+ className="itsec-admin-notice-list-item-container"
14
+ key={ notice.id }
15
+ >
16
  <NoticeActions notice={ notice } />
17
  <Notice notice={ notice } noticeId={ notice.id } />
18
  </li>
core/modules/core/entries/admin-notices/components/notice/index.js CHANGED
@@ -15,44 +15,55 @@ import { Button } from '@wordpress/components';
15
  */
16
  import './style.scss';
17
 
18
- /**
19
- * Notice Component.
20
- *
21
- * @param {string|number} noticeId
22
- * @param {Object} notice
23
- * @param {string} notice.severity
24
- * @param {string} notice.title
25
- * @param {string} notice.message
26
- * @param {Object} notice.meta
27
- * @param {Array.<{title: string, style: string, uri: string}>} notice.actions
28
- * @return {Component} Notice component.
29
- */
30
  export default function Notice( { notice } ) {
31
  return (
32
- <article className={ `itsec-admin-notice itsec-admin-notice--severity-${ notice.severity }` }>
 
 
33
  <header className="itsec-admin-notice__header">
34
  <div className="itsec-admin-notice__header-inset">
35
- <h4 dangerouslySetInnerHTML={ { __html: notice.title || formatMessage( notice.message, notice ) } } />
36
- { map( notice.actions, ( action, slug ) => ( action.style === 'primary' && (
37
- <Button key={ slug } href={ action.uri }>{ action.title }</Button>
38
- ) ) ) }
 
 
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
  </header>
41
 
42
  { notice.title && notice.message && (
43
- <section className="itsec-admin-notice__message" dangerouslySetInnerHTML={ { __html: autop( formatMessage( notice.message, notice ) ) } } />
 
 
 
 
 
 
 
44
  ) }
45
 
46
  { hasMeta( notice ) && (
47
  <dl className="itsec-admin-notice__meta">
48
- { map( notice.meta, ( meta, key ) => (
49
- key !== 'created_at' && (
50
- <Fragment key={ key }>
51
- <dt>{ meta.label }</dt>
52
- <dd>{ meta.formatted }</dd>
53
- </Fragment>
54
- )
55
- ) ) }
 
 
56
  </dl>
57
  ) }
58
 
@@ -72,7 +83,10 @@ function hasMeta( notice ) {
72
  return false;
73
  }
74
 
75
- if ( size( notice.meta ) === 1 && notice.meta.hasOwnProperty( 'created_at' ) ) {
 
 
 
76
  return false;
77
  }
78
 
@@ -89,7 +103,10 @@ function formatMessage( message, notice ) {
89
  continue;
90
  }
91
 
92
- message = message.replace( '{{ $' + action + ' }}', notice.actions[ action ].uri );
 
 
 
93
  }
94
 
95
  return message;
15
  */
16
  import './style.scss';
17
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export default function Notice( { notice } ) {
19
  return (
20
+ <article
21
+ className={ `itsec-admin-notice itsec-admin-notice--severity-${ notice.severity }` }
22
+ >
23
  <header className="itsec-admin-notice__header">
24
  <div className="itsec-admin-notice__header-inset">
25
+ <h4
26
+ dangerouslySetInnerHTML={ {
27
+ __html:
28
+ notice.title ||
29
+ formatMessage( notice.message, notice ),
30
+ } }
31
+ />
32
+ { map(
33
+ notice.actions,
34
+ ( action, slug ) =>
35
+ action.style === 'primary' && (
36
+ <Button key={ slug } href={ action.uri }>
37
+ { action.title }
38
+ </Button>
39
+ )
40
+ ) }
41
  </div>
42
  </header>
43
 
44
  { notice.title && notice.message && (
45
+ <section
46
+ className="itsec-admin-notice__message"
47
+ dangerouslySetInnerHTML={ {
48
+ __html: autop(
49
+ formatMessage( notice.message, notice )
50
+ ),
51
+ } }
52
+ />
53
  ) }
54
 
55
  { hasMeta( notice ) && (
56
  <dl className="itsec-admin-notice__meta">
57
+ { map(
58
+ notice.meta,
59
+ ( meta, key ) =>
60
+ key !== 'created_at' && (
61
+ <Fragment key={ key }>
62
+ <dt>{ meta.label }</dt>
63
+ <dd>{ meta.formatted }</dd>
64
+ </Fragment>
65
+ )
66
+ ) }
67
  </dl>
68
  ) }
69
 
83
  return false;
84
  }
85
 
86
+ if (
87
+ size( notice.meta ) === 1 &&
88
+ notice.meta.hasOwnProperty( 'created_at' )
89
+ ) {
90
  return false;
91
  }
92
 
103
  continue;
104
  }
105
 
106
+ message = message.replace(
107
+ '{{ $' + action + ' }}',
108
+ notice.actions[ action ].uri
109
+ );
110
  }
111
 
112
  return message;
core/modules/core/entries/admin-notices/components/notice/style.scss CHANGED
@@ -1,4 +1,3 @@
1
- @import "colors.scss";
2
  @import "mixins.scss";
3
 
4
  $border-radius: 3px;
@@ -8,97 +7,117 @@ $side-padding: 20px;
8
  background: white;
9
  border-radius: $border-radius;
10
  box-shadow: 0 0 5px rgba(211, 211, 211, 0.35);
11
- }
12
-
13
- .itsec-admin-notice__header {
14
- background: #eeecec;
15
- padding: $side-padding / 2;
16
- border-top-left-radius: $border-radius;
17
- border-top-right-radius: $border-radius;
18
 
19
- & .components-button {
20
- margin-top: 1em;
 
 
 
21
 
22
- @include bordered-button('blue');
23
- }
24
-
25
- .itsec-admin-notice--severity-error & {
26
- background: $alert-red;
27
 
28
  & .components-button {
29
- @include bordered-button('red');
 
 
30
  }
31
- }
32
 
33
- .itsec-admin-notice--severity-warning & {
34
- background: $alert-orange;
 
35
 
36
- & .components-button {
37
- @include bordered-button('orange');
38
- }
39
- }
40
 
41
- .itsec-admin-notice--severity-info & {
42
- background: $alert-blue;
43
- }
44
 
45
- .itsec-admin-notice--severity-success & {
46
- background: $alert-green;
 
 
47
 
48
- & .components-button {
49
- @include bordered-button('green');
 
 
 
 
 
 
 
 
 
50
  }
51
  }
52
- }
53
 
54
- .itsec-admin-notice .itsec-admin-notice__header:last-child {
55
- border-bottom-left-radius: $border-radius;
56
- border-bottom-right-radius: $border-radius;
57
- }
58
 
59
- .itsec-admin-notice__header .itsec-admin-notice__header-inset {
60
- background: white;
61
- border-radius: 5px;
62
- padding: $side-padding / 2;
63
- }
64
 
65
- .itsec-admin-notice__header h4 {
66
- font-size: 14px;
67
- margin: 0;
68
- }
 
 
 
69
 
70
- .itsec-admin-notice__message {
71
- padding: 10px $side-padding;
 
 
72
 
73
- & p:first-child {
74
- margin-top: 0;
75
- }
76
 
77
- & p:last-child {
78
- margin-bottom: 0;
 
79
  }
80
- }
81
-
82
- .itsec-admin-notice__meta {
83
- background: #eeecec;
84
- padding: 5px $side-padding;
85
- margin: 0;
86
- display: grid;
87
- grid-template: auto / min-content 1fr;
88
- grid-gap: 5px 20px;
89
 
90
- & dt,
91
- & dd {
 
 
 
92
  margin: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
 
95
- & dt {
96
- text-transform: uppercase;
97
- color: darken(#444444, 25);
 
 
98
  }
99
- }
100
 
101
- .itsec-admin-notice__footer {
102
- padding: 5px $side-padding;
103
- font-size: 11px;
104
  }
 
1
  @import "mixins.scss";
2
 
3
  $border-radius: 3px;
7
  background: white;
8
  border-radius: $border-radius;
9
  box-shadow: 0 0 5px rgba(211, 211, 211, 0.35);
 
 
 
 
 
 
 
10
 
11
+ .itsec-admin-notice__header {
12
+ background: #eeecec;
13
+ padding: $side-padding / 2;
14
+ border-top-left-radius: $border-radius;
15
+ border-top-right-radius: $border-radius;
16
 
17
+ &:last-child {
18
+ border-bottom-left-radius: $border-radius;
19
+ border-bottom-right-radius: $border-radius;
20
+ }
 
21
 
22
  & .components-button {
23
+ margin-top: 1em;
24
+
25
+ @include bordered-button('blue');
26
  }
 
27
 
28
+ @at-root {
29
+ .itsec-admin-notice--severity-error#{&} {
30
+ background: $alert-red;
31
 
32
+ & .components-button {
33
+ @include bordered-button('red');
34
+ }
35
+ }
36
 
37
+ .itsec-admin-notice--severity-warning#{&} {
38
+ background: $alert-orange;
 
39
 
40
+ & .components-button {
41
+ @include bordered-button('orange');
42
+ }
43
+ }
44
 
45
+ .itsec-admin-notice--severity-info#{&} {
46
+ background: $alert-blue;
47
+ }
48
+
49
+ .itsec-admin-notice--severity-success#{&} {
50
+ background: $alert-green;
51
+
52
+ & .components-button {
53
+ @include bordered-button('green');
54
+ }
55
+ }
56
  }
57
  }
 
58
 
59
+ .itsec-admin-notice .itsec-admin-notice__header:last-child {
60
+ border-bottom-left-radius: $border-radius;
61
+ border-bottom-right-radius: $border-radius;
62
+ }
63
 
64
+ .itsec-admin-notice__header .itsec-admin-notice__header-inset {
65
+ background: white;
66
+ border-radius: 5px;
67
+ padding: $side-padding / 2;
68
+ }
69
 
70
+ .itsec-admin-notice__header h4 {
71
+ font-size: 14px;
72
+ margin: 0;
73
+ color: $dark-text;
74
+ font-weight: 600;
75
+ text-transform: none;
76
+ }
77
 
78
+ .itsec-admin-notice__message {
79
+ border-left: 1px solid $border-color;
80
+ border-right: 1px solid $border-color;
81
+ padding: 10px $side-padding;
82
 
83
+ & p:first-child {
84
+ margin-top: 0;
85
+ }
86
 
87
+ & p:last-child {
88
+ margin-bottom: 0;
89
+ }
90
  }
 
 
 
 
 
 
 
 
 
91
 
92
+ .itsec-admin-notice__meta {
93
+ border-left: 1px solid $border-color;
94
+ border-right: 1px solid $border-color;
95
+ background: $focused-blue;
96
+ padding: 5px $side-padding;
97
  margin: 0;
98
+ display: grid;
99
+ grid-template: auto / min-content 1fr;
100
+ grid-gap: 5px 20px;
101
+
102
+ & dt,
103
+ & dd {
104
+ margin: 0;
105
+ }
106
+
107
+ & dt {
108
+ text-transform: uppercase;
109
+ color: darken(#444444, 25);
110
+ }
111
  }
112
 
113
+ .itsec-admin-notice__footer {
114
+ border-left: 1px solid $border-color;
115
+ border-right: 1px solid $border-color;
116
+ padding: 5px $side-padding;
117
+ font-size: 11px;
118
  }
 
119
 
120
+ & > :last-child:not(.itsec-admin-notice__header) {
121
+ border-bottom: 1px solid $border-color;
122
+ }
123
  }
core/modules/core/entries/admin-notices/components/panel/index.js CHANGED
@@ -7,7 +7,7 @@ import { get, size } from 'lodash';
7
  * WordPress dependencies
8
  */
9
  import { __ } from '@wordpress/i18n';
10
- import { IconButton, FormToggle } from '@wordpress/components';
11
  import { compose, withState } from '@wordpress/compose';
12
  import { withSelect, withDispatch } from '@wordpress/data';
13
 
@@ -42,39 +42,79 @@ function getAvailableHighlights() {
42
  ];
43
  }
44
 
45
- function Panel( { notices, loaded, mutedHighlights, mutedHighlightUpdatesInFlight, updateMutedHighlight, isConfiguring, setState } ) {
 
 
 
 
 
 
 
 
46
  return (
47
- <div className={ classnames( 'itsec-admin-notice-panel', {
48
- 'itsec-admin-notice-panel--is-configuring': isConfiguring,
49
- } ) }>
50
- <IconButton icon="admin-generic" label={ __( 'Configure', 'better-wp-security' ) }
 
 
 
 
51
  className="itsec-admin-notice-panel__configure-trigger"
52
  style={ { opacity: size( mutedHighlights ) > 0 ? 1 : 0 } }
53
- onClick={ () => setState( { isConfiguring: ! isConfiguring } ) } />
 
54
  <header className="itsec-admin-notice-panel__header">
55
  <h3>{ __( 'Security Admin Messages', 'better-wp-security' ) }</h3>
56
- <p>{ __( 'Important notices from iThemes Security', 'better-wp-security' ) }</p>
 
 
57
  </header>
58
  { isConfiguring && (
59
  <ul className="itsec-admin-notice-panel__configure-highlighted-logs">
60
- { getAvailableHighlights().map( ( { slug, label } ) => (
61
- mutedHighlights[ slug ] !== undefined && (
62
- <li>
63
- <label htmlFor={ `itsec-mute-highlight-${ slug }` }>{ label }</label>
64
- <FormToggle id={ `itsec-mute-highlight-${ slug }` }
65
- disabled={ ! loaded || mutedHighlightUpdatesInFlight[ slug ] }
66
- checked={ ! get( mutedHighlightUpdatesInFlight, [ slug, 'mute' ], mutedHighlights[ slug ] ) }
67
- onChange={ () => updateMutedHighlight( slug, ! mutedHighlights[ slug ] ) }
68
- />
69
- </li>
70
- )
71
- ) ) }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </ul>
73
  ) }
74
- { notices.length > 0 ?
75
- <NoticeList notices={ notices } /> :
76
- loaded && <span>{ __( 'No notices at the moment.', 'better-wp-security' ) }</span>
77
- }
 
 
 
78
  </div>
79
  );
80
  }
@@ -82,10 +122,15 @@ function Panel( { notices, loaded, mutedHighlights, mutedHighlightUpdatesInFligh
82
  export default compose( [
83
  withState( { isConfiguring: false, checked: {} } ),
84
  withSelect( ( select ) => ( {
85
- mutedHighlights: select( 'ithemes-security/admin-notices' ).getMutedHighlights(),
86
- mutedHighlightUpdatesInFlight: select( 'ithemes-security/admin-notices' ).getMutedHighlightUpdatesInFlight(),
 
 
 
 
87
  } ) ),
88
  withDispatch( ( dispatch ) => ( {
89
- updateMutedHighlight: dispatch( 'ithemes-security/admin-notices' ).updateMutedHighlight,
 
90
  } ) ),
91
  ] )( Panel );
7
  * WordPress dependencies
8
  */
9
  import { __ } from '@wordpress/i18n';
10
+ import { Button, FormToggle } from '@wordpress/components';
11
  import { compose, withState } from '@wordpress/compose';
12
  import { withSelect, withDispatch } from '@wordpress/data';
13
 
42
  ];
43
  }
44
 
45
+ function Panel( {
46
+ notices,
47
+ loaded,
48
+ mutedHighlights,
49
+ mutedHighlightUpdatesInFlight,
50
+ updateMutedHighlight,
51
+ isConfiguring,
52
+ setState,
53
+ } ) {
54
  return (
55
+ <div
56
+ className={ classnames( 'itsec-admin-notice-panel', {
57
+ 'itsec-admin-notice-panel--is-configuring': isConfiguring,
58
+ } ) }
59
+ >
60
+ <Button
61
+ icon="admin-generic"
62
+ label={ __( 'Configure', 'better-wp-security' ) }
63
  className="itsec-admin-notice-panel__configure-trigger"
64
  style={ { opacity: size( mutedHighlights ) > 0 ? 1 : 0 } }
65
+ onClick={ () => setState( { isConfiguring: ! isConfiguring } ) }
66
+ />
67
  <header className="itsec-admin-notice-panel__header">
68
  <h3>{ __( 'Security Admin Messages', 'better-wp-security' ) }</h3>
69
+ <p>
70
+ { __( 'Important notices from iThemes Security', 'better-wp-security' ) }
71
+ </p>
72
  </header>
73
  { isConfiguring && (
74
  <ul className="itsec-admin-notice-panel__configure-highlighted-logs">
75
+ { getAvailableHighlights().map(
76
+ ( { slug, label } ) =>
77
+ mutedHighlights[ slug ] !== undefined && (
78
+ <li>
79
+ <label
80
+ htmlFor={ `itsec-mute-highlight-${ slug }` }
81
+ >
82
+ { label }
83
+ </label>
84
+ <FormToggle
85
+ id={ `itsec-mute-highlight-${ slug }` }
86
+ disabled={
87
+ ! loaded ||
88
+ mutedHighlightUpdatesInFlight[
89
+ slug
90
+ ]
91
+ }
92
+ checked={
93
+ ! get(
94
+ mutedHighlightUpdatesInFlight,
95
+ [ slug, 'mute' ],
96
+ mutedHighlights[ slug ]
97
+ )
98
+ }
99
+ onChange={ () =>
100
+ updateMutedHighlight(
101
+ slug,
102
+ ! mutedHighlights[ slug ]
103
+ )
104
+ }
105
+ />
106
+ </li>
107
+ )
108
+ ) }
109
  </ul>
110
  ) }
111
+ { notices.length > 0 ? (
112
+ <NoticeList notices={ notices } />
113
+ ) : (
114
+ loaded && (
115
+ <span>{ __( 'No notices at the moment.', 'better-wp-security' ) }</span>
116
+ )
117
+ ) }
118
  </div>
119
  );
120
  }
122
  export default compose( [
123
  withState( { isConfiguring: false, checked: {} } ),
124
  withSelect( ( select ) => ( {
125
+ mutedHighlights: select(
126
+ 'ithemes-security/admin-notices'
127
+ ).getMutedHighlights(),
128
+ mutedHighlightUpdatesInFlight: select(
129
+ 'ithemes-security/admin-notices'
130
+ ).getMutedHighlightUpdatesInFlight(),
131
  } ) ),
132
  withDispatch( ( dispatch ) => ( {
133
+ updateMutedHighlight: dispatch( 'ithemes-security/admin-notices' )
134
+ .updateMutedHighlight,
135
  } ) ),
136
  ] )( Panel );
core/modules/core/entries/admin-notices/components/panel/style.scss CHANGED
@@ -1,14 +1,21 @@
1
- @import "colors.scss";
2
  @import "animations.scss";
3
 
4
- .itsec-admin-bar-admin-notices__content.components-popover::after {
5
- border-color: #E5EAEE !important;
 
 
 
 
 
 
 
6
  }
7
 
8
  .itsec-admin-notice-panel {
9
  padding: 1em;
10
- background: #E5EAEE;
11
  width: 420px;
 
12
  box-sizing: border-box;
13
 
14
  & .itsec-admin-notice-panel__header {
@@ -18,11 +25,13 @@
18
  h3 {
19
  color: $main-blue;
20
  text-align: center;
 
21
  }
22
 
23
  p {
24
  text-align: center;
25
  font-style: oblique;
 
26
  }
27
  }
28
 
@@ -43,8 +52,10 @@
43
  }
44
 
45
  & .itsec-admin-notice-panel__configure-trigger.components-button {
46
- padding: 5px;
47
- height: 30px;
 
 
48
  position: absolute;
49
  right: 1em;
50
  top: 1em;
@@ -54,6 +65,10 @@
54
  opacity: .5;
55
  box-shadow: none !important;
56
  }
 
 
 
 
57
  }
58
 
59
  &.itsec-admin-notice-panel--is-configuring .itsec-admin-notice-panel__configure-trigger.components-button {
@@ -85,7 +100,7 @@
85
  }
86
 
87
  & label {
88
- margin-left: calc(24px + 1em);
89
  font-size: 14px;
90
  font-weight: bold;
91
  }
 
1
  @import "animations.scss";
2
 
3
+ .itsec-admin-bar-admin-notices__content.components-popover {
4
+ .components-popover__content {
5
+ border-color: 1px solid $border-color;
6
+ box-shadow: $popover-shadow;
7
+ }
8
+
9
+ &::after {
10
+ border-color: #E5EAEE !important;
11
+ }
12
  }
13
 
14
  .itsec-admin-notice-panel {
15
  padding: 1em;
16
+ background: $white;
17
  width: 420px;
18
+ max-width: 100%;
19
  box-sizing: border-box;
20
 
21
  & .itsec-admin-notice-panel__header {
25
  h3 {
26
  color: $main-blue;
27
  text-align: center;
28
+ margin: 1rem 0 !important;
29
  }
30
 
31
  p {
32
  text-align: center;
33
  font-style: oblique;
34
+ margin: 1rem 0 !important;
35
  }
36
  }
37
 
52
  }
53
 
54
  & .itsec-admin-notice-panel__configure-trigger.components-button {
55
+ padding: 0;
56
+ height: auto;
57
+ width: auto;
58
+ min-width: 0;
59
  position: absolute;
60
  right: 1em;
61
  top: 1em;
65
  opacity: .5;
66
  box-shadow: none !important;
67
  }
68
+
69
+ .dashicon {
70
+ margin: 0;
71
+ }
72
  }
73
 
74
  &.itsec-admin-notice-panel--is-configuring .itsec-admin-notice-panel__configure-trigger.components-button {
100
  }
101
 
102
  & label {
103
+ margin-left: calc(36px + 1em);
104
  font-size: 14px;
105
  font-weight: bold;
106
  }
core/modules/core/entries/admin-notices/components/toolbar-button/index.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { ToolbarButton, Popover } from '@wordpress/components';
10
+ import { __ } from '@wordpress/i18n';
11
+ import { useState } from '@wordpress/element';
12
+ import { useSelect } from '@wordpress/data';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import Panel from '../panel';
18
+ import './style.scss';
19
+
20
+ export default function () {
21
+ const [ isToggled, setIsToggled ] = useState( false );
22
+ const { notices, noticesLoaded } = useSelect(
23
+ ( select ) => ( {
24
+ notices: select( 'ithemes-security/admin-notices' ).getNotices(),
25
+ noticesLoaded: select(
26
+ 'ithemes-security/admin-notices'
27
+ ).areNoticesLoaded(),
28
+ } ),
29
+ []
30
+ );
31
+
32
+ return (
33
+ <>
34
+ <ToolbarButton
35
+ aria-expanded={ isToggled }
36
+ onClick={ () => setIsToggled( ! isToggled ) }
37
+ icon="megaphone"
38
+ text={ __( 'Notifications', 'better-wp-security' ) }
39
+ className={ classnames(
40
+ 'itsec-admin-bar-admin-notices__trigger',
41
+ {
42
+ 'itsec-admin-bar-admin-notices__trigger--has-notices':
43
+ notices.length > 0,
44
+ }
45
+ ) }
46
+ />
47
+ { isToggled && (
48
+ <Popover
49
+ className="itsec-admin-bar-admin-notices__content"
50
+ expandOnMobile
51
+ focusOnMount="container"
52
+ position="bottom left"
53
+ headerTitle={ __( 'Notifications', 'better-wp-security' ) }
54
+ onClose={ () => setIsToggled( false ) }
55
+ onFocusOutside={ () => setIsToggled( false ) }
56
+ >
57
+ <Panel
58
+ notices={ notices }
59
+ loaded={ noticesLoaded }
60
+ close={ () => setIsToggled( false ) }
61
+ />
62
+ </Popover>
63
+ ) }
64
+ </>
65
+ );
66
+ }
core/modules/core/entries/admin-notices/components/toolbar-button/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/core/entries/admin-notices/components/toolbar-button/style.scss ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-admin-bar-admin-notices__trigger.components-button::after {
2
+ content: '';
3
+ background: #d8514f;
4
+ height: 10px;
5
+ width: 10px;
6
+ border-radius: 5px;
7
+ vertical-align: middle;
8
+ border: 1px solid #f1f1f1;
9
+ z-index: 1;
10
+ position: absolute;
11
+ left: 8px;
12
+ top: 0;
13
+ opacity: 0;
14
+ transition: opacity 1000ms ease-in-out;
15
+ }
16
+
17
+ .itsec-admin-bar-admin-notices__trigger--has-notices.components-button::after {
18
+ opacity: 1;
19
+ }
20
+
21
+ .itsec-admin-bar-admin-notices__content .components-popover__content {
22
+ box-shadow: rgba(0, 0, 0, 0.19) 0 4px 5px;
23
+ }
24
+
25
+ @media screen and (max-width: $small) {
26
+ .itsec-admin-bar .itsec-admin-bar__admin-notices {
27
+ margin-left: 0;
28
+ }
29
+ }
30
+
31
+ .itsec-admin-bar-admin-notices__content.is-expanded {
32
+ .components-popover__content {
33
+ overflow-y: scroll;
34
+ }
35
+
36
+ .itsec-admin-notice-panel {
37
+ width: 100%;
38
+ }
39
+ }
core/modules/core/entries/admin-notices/components/toolbar/index.js CHANGED
@@ -10,14 +10,13 @@ import { Fragment } from '@wordpress/element';
10
  import { __ } from '@wordpress/i18n';
11
  import { Popover, Button } from '@wordpress/components';
12
  import { compose, withState } from '@wordpress/compose';
 
13
 
14
  /**
15
  * Internal dependencies
16
  */
17
- import { doesElementBelongToPanel } from '../../utils';
18
  import Panel from '../panel';
19
  import './style.scss';
20
- import { withSelect } from '@wordpress/data';
21
 
22
  function Toolbar( { notices, noticesLoaded, isToggled, setState } ) {
23
  return (
@@ -25,12 +24,16 @@ function Toolbar( { notices, noticesLoaded, isToggled, setState } ) {
25
  <Button
26
  id="itsec-admin-notices-toolbar-trigger"
27
  className={ classnames( 'ab-item ab-empty-item', {
28
- 'itsec-admin-notices-toolbar--has-notices': notices.length > 0,
 
29
  } ) }
30
  onClick={ () => setState( { isToggled: ! isToggled } ) }
31
- aria-expanded={ isToggled }>
 
32
  <span className="it-icon-itsec" />
33
- <span className="itsec-toolbar-text">{ __( 'Security', 'better-wp-security' ) }</span>
 
 
34
  { notices.length > 0 && (
35
  <span className="itsec-admin-notices-toolbar-bubble">
36
  <span className="itsec-admin-notices-toolbar-bubble__count">
@@ -48,30 +51,13 @@ function Toolbar( { notices, noticesLoaded, isToggled, setState } ) {
48
  position="bottom center"
49
  headerTitle={ __( 'Security', 'better-wp-security' ) }
50
  onClose={ () => setState( { isToggled: false } ) }
51
- onClickOutside={ ( e ) => {
52
- if (
53
- ! e.target || (
54
- e.target.id !== 'itsec-admin-notices-toolbar-trigger' &&
55
- e.target.parentNode.id !== 'itsec-admin-notices-toolbar-trigger' &&
56
- ! doesElementBelongToPanel( e.target )
57
- )
58
- ) {
59
- setState( { isToggled: false } );
60
- }
61
- } }
62
- onFocusOutside={ () => {
63
- const activeElement = document.activeElement;
64
-
65
- if (
66
- activeElement.id !== 'itsec-admin-notices-toolbar-trigger' &&
67
- ( ! activeElement.parentNode || activeElement.parentNode.id !== 'itsec-admin-notices-toolbar-trigger' ) &&
68
- ! doesElementBelongToPanel( activeElement )
69
- ) {
70
- setState( { isToggled: false } );
71
- }
72
- } }
73
  >
74
- <Panel notices={ notices } loaded={ noticesLoaded } close={ () => setState( { isToggled: false } ) } />
 
 
 
 
75
  </Popover>
76
  ) }
77
  </Fragment>
@@ -81,7 +67,9 @@ function Toolbar( { notices, noticesLoaded, isToggled, setState } ) {
81
  export default compose( [
82
  withSelect( ( select ) => ( {
83
  notices: select( 'ithemes-security/admin-notices' ).getNotices(),
84
- noticesLoaded: select( 'ithemes-security/admin-notices' ).areNoticesLoaded(),
 
 
85
  } ) ),
86
  withState( { isToggled: false } ),
87
  ] )( Toolbar );
10
  import { __ } from '@wordpress/i18n';
11
  import { Popover, Button } from '@wordpress/components';
12
  import { compose, withState } from '@wordpress/compose';
13
+ import { withSelect } from '@wordpress/data';
14
 
15
  /**
16
  * Internal dependencies
17
  */
 
18
  import Panel from '../panel';
19
  import './style.scss';
 
20
 
21
  function Toolbar( { notices, noticesLoaded, isToggled, setState } ) {
22
  return (
24
  <Button
25
  id="itsec-admin-notices-toolbar-trigger"
26
  className={ classnames( 'ab-item ab-empty-item', {
27
+ 'itsec-admin-notices-toolbar--has-notices':
28
+ notices.length > 0,
29
  } ) }
30
  onClick={ () => setState( { isToggled: ! isToggled } ) }
31
+ aria-expanded={ isToggled }
32
+ >
33
  <span className="it-icon-itsec" />
34
+ <span className="itsec-toolbar-text">
35
+ { __( 'Security', 'better-wp-security' ) }
36
+ </span>
37
  { notices.length > 0 && (
38
  <span className="itsec-admin-notices-toolbar-bubble">
39
  <span className="itsec-admin-notices-toolbar-bubble__count">
51
  position="bottom center"
52
  headerTitle={ __( 'Security', 'better-wp-security' ) }
53
  onClose={ () => setState( { isToggled: false } ) }
54
+ onFocusOutside={ () => setState( { isToggled: false } ) }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  >
56
+ <Panel
57
+ notices={ notices }
58
+ loaded={ noticesLoaded }
59
+ close={ () => setState( { isToggled: false } ) }
60
+ />
61
  </Popover>
62
  ) }
63
  </Fragment>
67
  export default compose( [
68
  withSelect( ( select ) => ( {
69
  notices: select( 'ithemes-security/admin-notices' ).getNotices(),
70
+ noticesLoaded: select(
71
+ 'ithemes-security/admin-notices'
72
+ ).areNoticesLoaded(),
73
  } ) ),
74
  withState( { isToggled: false } ),
75
  ] )( Toolbar );
core/modules/core/entries/admin-notices/components/toolbar/style.scss CHANGED
@@ -1,4 +1,3 @@
1
- @import "colors.scss";
2
  @import "breakpoints.scss";
3
 
4
  #wpadminbar #wp-admin-bar-itsec_admin_bar_menu .ab-sub-wrapper {
 
1
  @import "breakpoints.scss";
2
 
3
  #wpadminbar #wp-admin-bar-itsec_admin_bar_menu .ab-sub-wrapper {
core/modules/core/entries/admin-notices/store/controls.js CHANGED
@@ -6,7 +6,10 @@ import { uniqueId } from 'lodash';
6
  /**
7
  * WordPress dependencies
8
  */
9
- import { select as selectData, dispatch as dispatchData } from '@wordpress/data';
 
 
 
10
  import { default as triggerApiFetch } from '@wordpress/api-fetch';
11
 
12
  /**
@@ -29,6 +32,7 @@ export function apiFetch( request ) {
29
 
30
  /**
31
  * Calls a selector using the current state.
 
32
  * @param {string} selectorName Selector name.
33
  * @param {Array} args Selector arguments.
34
  *
@@ -63,7 +67,7 @@ export function select( selectorName, ...args ) {
63
  * by automatically dismissed
64
  * after x milliseconds.
65
  * Defaults to `false`.
66
- * @param {?Array<WPNoticeAction>} options.actions User actions to be
67
  * presented with notice.
68
  *
69
  * @return {Object} control descriptor.
@@ -86,12 +90,21 @@ const controls = {
86
  },
87
 
88
  SELECT( { selectorName, args } ) {
89
- return selectData( 'ithemes-security/admin-notices' )[ selectorName ]( ...args );
 
 
90
  },
91
  CREATE_NOTICE( { status, content, options } ) {
92
  if ( options.autoDismiss ) {
93
  options.id = options.id || uniqueId( 'itsec-auto-dismiss-' );
94
- setTimeout( () => dispatchData( 'core/notices' ).removeNotice( options.id, options.context ), options.autoDismiss );
 
 
 
 
 
 
 
95
  }
96
 
97
  dispatchData( 'core/notices' ).createNotice( status, content, options );
6
  /**
7
  * WordPress dependencies
8
  */
9
+ import {
10
+ select as selectData,
11
+ dispatch as dispatchData,
12
+ } from '@wordpress/data';
13
  import { default as triggerApiFetch } from '@wordpress/api-fetch';
14
 
15
  /**
32
 
33
  /**
34
  * Calls a selector using the current state.
35
+ *
36
  * @param {string} selectorName Selector name.
37
  * @param {Array} args Selector arguments.
38
  *
67
  * by automatically dismissed
68
  * after x milliseconds.
69
  * Defaults to `false`.
70
+ * @param {?Array<Object>} options.actions User actions to be
71
  * presented with notice.
72
  *
73
  * @return {Object} control descriptor.
90
  },
91
 
92
  SELECT( { selectorName, args } ) {
93
+ return selectData( 'ithemes-security/admin-notices' )[ selectorName ](
94
+ ...args
95
+ );
96
  },
97
  CREATE_NOTICE( { status, content, options } ) {
98
  if ( options.autoDismiss ) {
99
  options.id = options.id || uniqueId( 'itsec-auto-dismiss-' );
100
+ setTimeout(
101
+ () =>
102
+ dispatchData( 'core/notices' ).removeNotice(
103
+ options.id,
104
+ options.context
105
+ ),
106
+ options.autoDismiss
107
+ );
108
  }
109
 
110
  dispatchData( 'core/notices' ).createNotice( status, content, options );
core/modules/core/entries/admin-notices/store/reducers.js CHANGED
@@ -29,9 +29,7 @@ export default function adminNotices( state = DEFAULT_STATE, action ) {
29
  case RECEIVE_NOTICES:
30
  return {
31
  ...state,
32
- notices: [
33
- ...action.notices,
34
- ],
35
  };
36
  case START_NOTICE_ACTION:
37
  return {
@@ -50,7 +48,9 @@ export default function adminNotices( state = DEFAULT_STATE, action ) {
50
  ...state,
51
  doingActions: {
52
  ...state.doingActions,
53
- [ action.noticeId ]: ( state.doingActions[ action.noticeId ] || [] ).filter( ( actionId ) => actionId !== action.actionId ),
 
 
54
  },
55
  };
56
  case RECEIVE_MUTED_HIGHLIGHTS:
@@ -69,7 +69,10 @@ export default function adminNotices( state = DEFAULT_STATE, action ) {
69
  case FINISH_UPDATE_MUTED_HIGHLIGHT:
70
  return {
71
  ...state,
72
- mutedHighlightUpdatesInFlight: omit( state.mutedHighlightUpdatesInFlight, action.slug ),
 
 
 
73
  mutedHighlights: {
74
  ...state.mutedHighlights,
75
  [ action.slug ]: action.mute,
@@ -78,7 +81,10 @@ export default function adminNotices( state = DEFAULT_STATE, action ) {
78
  case FAILED_UPDATE_MUTED_HIGHLIGHT:
79
  return {
80
  ...state,
81
- mutedHighlightUpdatesInFlight: omit( state.mutedHighlightUpdatesInFlight, action.slug ),
 
 
 
82
  };
83
  default:
84
  return state;
29
  case RECEIVE_NOTICES:
30
  return {
31
  ...state,
32
+ notices: [ ...action.notices ],
 
 
33
  };
34
  case START_NOTICE_ACTION:
35
  return {
48
  ...state,
49
  doingActions: {
50
  ...state.doingActions,
51
+ [ action.noticeId ]: (
52
+ state.doingActions[ action.noticeId ] || []
53
+ ).filter( ( actionId ) => actionId !== action.actionId ),
54
  },
55
  };
56
  case RECEIVE_MUTED_HIGHLIGHTS:
69
  case FINISH_UPDATE_MUTED_HIGHLIGHT:
70
  return {
71
  ...state,
72
+ mutedHighlightUpdatesInFlight: omit(
73
+ state.mutedHighlightUpdatesInFlight,
74
+ action.slug
75
+ ),
76
  mutedHighlights: {
77
  ...state.mutedHighlights,
78
  [ action.slug ]: action.mute,
81
  case FAILED_UPDATE_MUTED_HIGHLIGHT:
82
  return {
83
  ...state,
84
+ mutedHighlightUpdatesInFlight: omit(
85
+ state.mutedHighlightUpdatesInFlight,
86
+ action.slug
87
+ ),
88
  };
89
  default:
90
  return state;
core/modules/core/entries/admin-notices/store/resolvers.js CHANGED
@@ -7,7 +7,12 @@ import { isEmpty } from 'lodash';
7
  * Internal dependencies
8
  */
9
  import { apiFetch } from './controls';
10
- import { FINISH_NOTICE_ACTION, FINISH_UPDATE_MUTED_HIGHLIGHT, receiveMutedHighlights, receiveNotices } from './actions';
 
 
 
 
 
11
 
12
  const getNotices = {
13
  *fulfill() {
@@ -18,16 +23,21 @@ const getNotices = {
18
  yield receiveNotices( notices );
19
  },
20
  shouldInvalidate( action ) {
21
- return action.type === FINISH_NOTICE_ACTION || action.type === FINISH_UPDATE_MUTED_HIGHLIGHT;
 
 
 
22
  },
23
  };
24
 
25
  export { getNotices as getNotices };
26
 
27
- export function *getMutedHighlights() {
28
  const settings = yield apiFetch( {
29
  path: '/ithemes-security/v1/admin-notices/settings',
30
  } );
31
 
32
- yield receiveMutedHighlights( isEmpty( settings.muted_highlights ) ? {} : settings.muted_highlights );
 
 
33
  }
7
  * Internal dependencies
8
  */
9
  import { apiFetch } from './controls';
10
+ import {
11
+ FINISH_NOTICE_ACTION,
12
+ FINISH_UPDATE_MUTED_HIGHLIGHT,
13
+ receiveMutedHighlights,
14
+ receiveNotices,
15
+ } from './actions';
16
 
17
  const getNotices = {
18
  *fulfill() {
23
  yield receiveNotices( notices );
24
  },
25
  shouldInvalidate( action ) {
26
+ return (
27
+ action.type === FINISH_NOTICE_ACTION ||
28
+ action.type === FINISH_UPDATE_MUTED_HIGHLIGHT
29
+ );
30
  },
31
  };
32
 
33
  export { getNotices as getNotices };
34
 
35
+ export function* getMutedHighlights() {
36
  const settings = yield apiFetch( {
37
  path: '/ithemes-security/v1/admin-notices/settings',
38
  } );
39
 
40
+ yield receiveMutedHighlights(
41
+ isEmpty( settings.muted_highlights ) ? {} : settings.muted_highlights
42
+ );
43
  }
core/modules/core/entries/admin-notices/store/selectors.js CHANGED
@@ -13,11 +13,19 @@ import { select } from '@wordpress/data';
13
  * @return {boolean} Whether resolution is in progress.
14
  */
15
  export function isResolving( selectorName, ...args ) {
16
- return select( 'core/data' ).isResolving( 'ithemes-security/admin-notices', selectorName, args );
 
 
 
 
17
  }
18
 
19
  export function isResolved( selectorName, ...args ) {
20
- return select( 'core/data' ).hasFinishedResolution( 'ithemes-security/admin-notices', selectorName, args );
 
 
 
 
21
  }
22
 
23
  export function getNotices( state ) {
13
  * @return {boolean} Whether resolution is in progress.
14
  */
15
  export function isResolving( selectorName, ...args ) {
16
+ return select( 'core/data' ).isResolving(
17
+ 'ithemes-security/admin-notices',
18
+ selectorName,
19
+ args
20
+ );
21
  }
22
 
23
  export function isResolved( selectorName, ...args ) {
24
+ return select( 'core/data' ).hasFinishedResolution(
25
+ 'ithemes-security/admin-notices',
26
+ selectorName,
27
+ args
28
+ );
29
  }
30
 
31
  export function getNotices( state ) {
core/modules/core/entries/admin-notices/utils.js DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * Check if the given element belongs to the notices panel even if it is outside of the DOM tree.
3
- *
4
- * @param {HTMLElement} element
5
- * @return {boolean} If the element is protected.
6
- */
7
- export function doesElementBelongToPanel( element ) {
8
- let node = element.parentNode;
9
-
10
- while ( node !== null ) {
11
- if ( node.classList && node.classList.contains( 'itsec-admin-notice-list-actions__more-menu-items' ) ) {
12
- return true;
13
- }
14
-
15
- node = node.parentNode;
16
- }
17
-
18
- return false;
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/entries/settings.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { registerPlugin } from '@wordpress/plugins';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import '@ithemes/security.core.admin-notices-api';
10
+ import { ToolbarFill } from '@ithemes/security.pages.settings';
11
+ import ToolbarButton from './admin-notices/components/toolbar-button';
12
+
13
+ registerPlugin( 'itsec-admin-notices-settings-toolbar', {
14
+ render() {
15
+ return (
16
+ <ToolbarFill>
17
+ <ToolbarButton />
18
+ </ToolbarFill>
19
+ );
20
+ },
21
+ } );
core/modules/core/module.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "core",
3
+ "type": "recommended",
4
+ "status": "always-active",
5
+ "title": "Core",
6
+ "scheduling": {
7
+ "clear-locks": {
8
+ "schedule": "daily",
9
+ "type": "recurring"
10
+ },
11
+ "clear-tokens": {
12
+ "schedule": "daily",
13
+ "type": "recurring"
14
+ },
15
+ "purge-lockouts": {
16
+ "schedule": "daily",
17
+ "type": "recurring"
18
+ }
19
+ }
20
+ }
core/modules/core/notices.php CHANGED
@@ -3,7 +3,7 @@
3
  class ITSEC_Admin_Notice_New_Feature_Core implements ITSEC_Admin_Notice {
4
 
5
  public function get_id() {
6
- return 'release-rcp';
7
  }
8
 
9
  public function get_title() {
@@ -11,7 +11,10 @@ class ITSEC_Admin_Notice_New_Feature_Core implements ITSEC_Admin_Notice {
11
  }
12
 
13
  public function get_message() {
14
- return esc_html__( 'iThemes Security Pro Now Integrates with Restrict Content Pro.', 'better-wp-security' );
 
 
 
15
  }
16
 
17
  public function get_meta() {
@@ -29,13 +32,14 @@ class ITSEC_Admin_Notice_New_Feature_Core implements ITSEC_Admin_Notice {
29
  public function get_actions() {
30
  return array(
31
  'blog' => new ITSEC_Admin_Notice_Action_Link(
32
- add_query_arg( 'itsec_view_release_post', 'release-ban-users', admin_url( 'index.php' ) ),
33
- esc_html__( 'See what’s new', 'better-wp-security' ),
34
  ITSEC_Admin_Notice_Action::S_PRIMARY,
35
  function () {
36
  $this->handle_dismiss();
 
37
 
38
- wp_redirect( 'https://ithemes.com/?p=59484' );
39
  die;
40
  }
41
  )
@@ -65,7 +69,5 @@ class ITSEC_Admin_Notice_New_Feature_Core implements ITSEC_Admin_Notice {
65
  }
66
  }
67
 
68
- if ( time() > 1603206000 ) {
69
- ITSEC_Lib_Admin_Notices::register( new ITSEC_Admin_Notice_Globally_Dismissible( new ITSEC_Admin_Notice_Managers_Only( new ITSEC_Admin_Notice_New_Feature_Core() ) ) );
70
- }
71
 
3
  class ITSEC_Admin_Notice_New_Feature_Core implements ITSEC_Admin_Notice {
4
 
5
  public function get_id() {
6
+ return 'release-new-ui-launch';
7
  }
8
 
9
  public function get_title() {
11
  }
12
 
13
  public function get_message() {
14
+ return sprintf(
15
+ esc_html__( 'iThemes Security %s is here!', 'better-wp-security' ),
16
+ ITSEC_Core::is_pro() ? '7.0' : '8.0'
17
+ );
18
  }
19
 
20
  public function get_meta() {
32
  public function get_actions() {
33
  return array(
34
  'blog' => new ITSEC_Admin_Notice_Action_Link(
35
+ add_query_arg( 'itsec_view_release_post', 'release-new-ui', admin_url( 'index.php' ) ),
36
+ esc_html__( 'See What’s New', 'better-wp-security' ),
37
  ITSEC_Admin_Notice_Action::S_PRIMARY,
38
  function () {
39
  $this->handle_dismiss();
40
+ $url = ITSEC_Core::is_pro() ? 'https://ithemes.com/?p=64448' : 'https://ithemes.com/?p=65086';
41
 
42
+ wp_redirect( $url );
43
  die;
44
  }
45
  )
69
  }
70
  }
71
 
72
+ ITSEC_Lib_Admin_Notices::register( new ITSEC_Admin_Notice_Globally_Dismissible( new ITSEC_Admin_Notice_Managers_Only( new ITSEC_Admin_Notice_New_Feature_Core() ) ) );
 
 
73
 
core/modules/core/sidebar-widget-backupbuddy-cross-promo.php DELETED
@@ -1,20 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Settings_Page_Sidebar_Widget_BackupBuddy_Cross_Promo extends ITSEC_Settings_Page_Sidebar_Widget {
4
- public function __construct() {
5
- $this->id = 'backupbuddy-cross-promo';
6
- $this->title = __( 'Complete Your Security Strategy With BackupBuddy', 'better-wp-security' );
7
- $this->priority = ITSEC_Core::is_pro() ? 11 : 7;
8
-
9
- parent::__construct();
10
- }
11
-
12
- public function render( $form ) {
13
- echo '<p style="text-align: center;"><img src="' . plugins_url( 'img/backupbuddy-logo.png', __FILE__ ) . '" alt="BackupBuddy"></p>';
14
- echo '<p>' . __( 'BackupBuddy is the complete backup, restore and migration solution for your WordPress site. Schedule automated backups, store your backups safely off-site and restore your site quickly & easily.', 'better-wp-security' ) . '</p>';
15
- echo sprintf( '<p style="font-weight: bold; font-size: 1em;">%s<span style="display: block; text-align: center; font-size: 1.2em; background: #ebebeb; padding: .5em;">%s</span></p>', __( '25% off BackupBuddy with coupon code', 'better-wp-security' ), __( 'BACKUPPROTECT', 'better-wp-security' ) );
16
- echo '<a href="http://ithemes.com/better-backups" class="button-secondary" target="_blank" rel="noopener noreferrer">' . __( 'Get BackupBuddy', 'better-wp-security' ) . '</a>';
17
- }
18
-
19
- }
20
- new ITSEC_Settings_Page_Sidebar_Widget_BackupBuddy_Cross_Promo();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/sidebar-widget-mail-list-signup.php DELETED
@@ -1,61 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Settings_Page_Sidebar_Widget_Mail_List_Signup extends ITSEC_Settings_Page_Sidebar_Widget {
4
-
5
- private $version = 1;
6
-
7
- public function __construct() {
8
- $this->id = 'mail-list-signup';
9
- $this->title = __( 'Download Our WordPress Security Pocket Guide', 'better-wp-security' );
10
- $this->priority = 6;
11
- $this->settings_form = false;
12
-
13
- parent::__construct();
14
- }
15
-
16
- public function render( $form ) {
17
- wp_enqueue_script( 'itsec-mc-validate', plugins_url( '/js/mc-validate.js', __FILE__ ), array( 'jquery' ), $this->version, true );
18
- $this->inline_js = "(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[0]='EMAIL';ftypes[0]='email';fnames[1]='FNAME';ftypes[1]='text';fnames[2]='LNAME';ftypes[2]='text';}(jQuery));";
19
- if ( function_exists( 'wp_add_inline_script' ) ) {
20
- wp_add_inline_script( 'itsec-mc-validate', $this->inline_js );
21
- } else {
22
- add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), null, 2 );
23
- }
24
- ?>
25
-
26
- <div id="mc_embed_signup">
27
- <form
28
- action="https://ithemes.us2.list-manage.com/subscribe/post?u=7acf83c7a47b32c740ad94a4e&amp;id=5176bfed9e"
29
- method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate"
30
- target="_blank" novalidate>
31
- <div style="text-align: center;">
32
- <img src="<?php echo plugins_url( 'img/security-ebook.png', __FILE__ ) ?>" style="max-width: 100%" alt="WordPress Security - A Pocket Guide">
33
- </div>
34
- <p><?php _e( 'Get tips for securing your site + the latest WordPress security updates, news and releases from iThemes.', 'better-wp-security' ); ?></p>
35
-
36
- <div id="mce-responses" class="clear">
37
- <div class="response" id="mce-error-response" style="display:none"></div>
38
- <div class="response" id="mce-success-response" style="display:none"></div>
39
- </div>
40
- <label for="mce-EMAIL" style="display: block;margin-bottom: 3px;"><?php _e( 'Email Address', 'better-wp-security' ); ?></label>
41
- <input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL" placeholder="email@domain.com"> <br/><br/>
42
- <input type="submit" value="<?php _e( 'Subscribe', 'better-wp-security' ); ?>" name="subscribe" id="mc-embedded-subscribe" class="button button-secondary">
43
- </form>
44
- </div>
45
- <?php
46
- }
47
-
48
- /**
49
- * Used to replicate the functionality of wp_add_inline_script
50
- *
51
- * @todo remove when we only support WordPress 4.5+
52
- */
53
- public function script_loader_tag( $tag, $handle ) {
54
- if ( 'itsec-mc-validate' === $handle ) {
55
- $tag .= sprintf( "<script type='text/javascript'>\n%s\n</script>\n", $this->inline_js );
56
- }
57
- return $tag;
58
- }
59
-
60
- }
61
- new ITSEC_Settings_Page_Sidebar_Widget_Mail_List_Signup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/sidebar-widget-pro-upsell.php DELETED
@@ -1,25 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Settings_Page_Sidebar_Widget_Pro_Upsell extends ITSEC_Settings_Page_Sidebar_Widget {
4
- public function __construct() {
5
- $this->id = 'pro-upsell';
6
- $this->title = __( 'Get iThemes Security Pro', 'better-wp-security' );
7
- $this->priority = 5;
8
-
9
- parent::__construct();
10
- }
11
-
12
- public function render( $form ) {
13
- echo '<p>' . sprintf( __( 'Add an extra layer of protection to your WordPress site with <a href="%s">iThemes Security Pro</a>, including:', 'better-wp-security' ), 'https://ithemes.com/security/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta' ) . '</p>';
14
- echo '<ul>';
15
- echo '<li>' . __( 'Two-factor authentication', 'better-wp-security' ) . '</li>';
16
- echo '<li>' . __( 'Scheduled malware scanning', 'better-wp-security' ) . '</li>';
17
- echo '<li>' . __( 'Google reCAPTCHA integration', 'better-wp-security' ) . '</li>';
18
- echo '<li>' . __( 'Private, ticketed support', 'better-wp-security' ) . '</li>';
19
- echo '<li>' . __( '+ more Pro-only features', 'better-wp-security' ) . '</li>';
20
- echo '</ul>';
21
- echo '<a href="https://ithemes.com/security/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta" class="button-primary" target="_blank" rel="noopener noreferrer">' . __( 'Get iThemes Security Pro', 'better-wp-security' ) . '</a>';
22
- }
23
-
24
- }
25
- new ITSEC_Settings_Page_Sidebar_Widget_Pro_Upsell();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/sidebar-widget-support.php DELETED
@@ -1,20 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Settings_Page_Sidebar_Widget_Free_Support extends ITSEC_Settings_Page_Sidebar_Widget {
4
- public function __construct() {
5
- $this->id = 'support';
6
- $this->title = __( 'Need Help Securing Your Site?', 'better-wp-security' );
7
- $this->priority = 11;
8
-
9
- parent::__construct();
10
- }
11
-
12
- public function render( $form ) {
13
- echo '<p>' . __( 'Since you are using the free version of iThemes Security from WordPress.org, you can get free support from the WordPress community.', 'better-wp-security' ) . '</p>';
14
- echo '<p><a class="button-secondary" href="http://wordpress.org/support/plugin/better-wp-security" target="_blank" rel="noopener noreferrer">' . __( 'Get Free Support', 'better-wp-security' ) . '</a></p>';
15
- echo '<p>' . __( 'Get added peace of mind with professional support from our expert team and pro features with iThemes Security Pro.', 'better-wp-security' ) . '</p>';
16
- echo '<p><a class="button-secondary" href="https://ithemes.com/security/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta" target="_blank" rel="noopener noreferrer">' . __( 'Get iThemes Security Pro', 'better-wp-security' ) . '</a></p>';
17
- }
18
-
19
- }
20
- new ITSEC_Settings_Page_Sidebar_Widget_Free_Support();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/core/sidebar-widget-sync-cross-promo.php DELETED
@@ -1,27 +0,0 @@
1
- <?php
2
-
3
- class ITSEC_Settings_Page_Sidebar_Widget_Sync_Cross_Promo extends ITSEC_Settings_Page_Sidebar_Widget {
4
- public function __construct() {
5
- $this->id = 'sync-cross-promo';
6
- $this->title = __( 'Manage Your Sites Remotely', 'better-wp-security' );
7
- $this->priority = 11;
8
-
9
- parent::__construct();
10
- }
11
-
12
- public function render( $form ) {
13
- ?>
14
- <div style="text-align: center;">
15
- <img src="<?php echo plugins_url( 'img/sync-logo.png', __FILE__ ) ?>" style="max-width: 100%" alt="Manage Your Sites Remotely">
16
- </div>
17
- <?php
18
-
19
- echo '<p>' . __( 'Manage updates (and much more!) for your WordPress websites all in one place. Save time logging in to multiple websites to perform WordPress admin tasks.', 'better-wp-security' ) . '</p>';
20
- echo '<p>' . __( 'Integrated with iThemes Security, so you can release lockouts, authorize IPs, and turn Away Mode on or off right from your Sync dashboard.', 'better-wp-security' ) . '</p>';
21
- echo '<div style="text-align: center;">';
22
- echo '<p><a class="button-primary" href="https://ithemes.com/member/cart.php?action=add&id=523" target="_blank" rel="noopener noreferrer">' . __( 'Free 30 Day Trial', 'better-wp-security' ) . '</a></p>';
23
- echo '</div>';
24
- }
25
-
26
- }
27
- new ITSEC_Settings_Page_Sidebar_Widget_Sync_Cross_Promo();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
core/modules/dashboard/active.php ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ <?php
2
+
3
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
4
+
5
+ return 'ITSEC_Dashboard';
core/modules/dashboard/cards/abstract-class-itsec-dashboard-card.php ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Dashboard_Card
5
+ */
6
+ abstract class ITSEC_Dashboard_Card {
7
+
8
+ /**
9
+ * Get the slug for this card.
10
+ *
11
+ * @return string
12
+ */
13
+ abstract public function get_slug();
14
+
15
+ /**
16
+ * Get the label for this card.
17
+ *
18
+ * @return string
19
+ */
20
+ abstract public function get_label();
21
+
22
+ /**
23
+ * Get the size constraints.
24
+ *
25
+ * - minW
26
+ * - minH
27
+ * - maxW
28
+ * - maxH
29
+ * - defaultW
30
+ * - defaultH
31
+ *
32
+ * @return array
33
+ */
34
+ abstract public function get_size();
35
+
36
+ /**
37
+ * Respond to the query for this card.
38
+ *
39
+ * @param array $query_args Any query arguments. Will need to be registered with {@see get_query_args()}
40
+ * @param array $settings Settings for the instance. Will need to be registered with {@see get_settings_schema()}
41
+ *
42
+ * @return array
43
+ */
44
+ abstract public function query_for_data( array $query_args, array $settings );
45
+
46
+ /**
47
+ * Get the card type.
48
+ *
49
+ * @return string
50
+ */
51
+ public function get_type() {
52
+ return 'custom';
53
+ }
54
+
55
+ /**
56
+ * Get the maximum instances of this card allowed in the dashboard.
57
+ *
58
+ * @return int|null Number of instances, or null for unlimited.
59
+ */
60
+ public function get_max() {
61
+ return 1;
62
+ }
63
+
64
+ /**
65
+ * Get supported query args.
66
+ *
67
+ * Format of query parameter names => schema configurations.
68
+ *
69
+ * @return array
70
+ */
71
+ public function get_query_args() {
72
+ return array();
73
+ }
74
+
75
+ /**
76
+ * Get the schema for the settings.
77
+ *
78
+ * @return array
79
+ */
80
+ public function get_settings_schema() {
81
+ return array();
82
+ }
83
+
84
+ /**
85
+ * Get links to include on the response.
86
+ *
87
+ * @return array
88
+ */
89
+ public function get_links() {
90
+ return array();
91
+ }
92
+ }
core/modules/dashboard/cards/class-itsec-dashboard-card-active-lockouts.php ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Dashboard_Card_Active_Lockouts extends ITSEC_Dashboard_Card {
4
+ /**
5
+ * @inheritDoc
6
+ */
7
+ public function get_slug() {
8
+ return 'active-lockouts';
9
+ }
10
+
11
+ /**
12
+ * @inheritDoc
13
+ */
14
+ public function get_label() {
15
+ return __( 'Active Lockouts', 'better-wp-security' );
16
+ }
17
+
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ public function get_size() {
22
+ return array(
23
+ 'minW' => 1,
24
+ 'minH' => 2,
25
+ 'maxW' => 2,
26
+ 'maxH' => 3,
27
+ 'defaultW' => 1,
28
+ 'defaultH' => 2,
29
+ );
30
+ }
31
+
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ public function query_for_data( array $query_args, array $settings ) {
36
+
37
+ /** @var ITSEC_Lockout $itsec_lockout */
38
+ global $itsec_lockout;
39
+
40
+ $lockout_query = array(
41
+ 'limit' => 100,
42
+ 'current' => true,
43
+ 'order' => 'DESC',
44
+ 'orderby' => 'lockout_start',
45
+ );
46
+
47
+ if ( ! empty( $query_args['search'] ) ) {
48
+ $lockout_query['search'] = $query_args['search'];
49
+ }
50
+
51
+ $lockouts = $itsec_lockout->get_lockouts( 'all', $lockout_query );
52
+
53
+ $data = array( 'lockouts' => array() );
54
+
55
+ foreach ( $lockouts as $lockout ) {
56
+ $data['lockouts'][] = $this->prepare_lockout( $lockout );
57
+ }
58
+
59
+ return $data;
60
+ }
61
+
62
+ /**
63
+ * @inheritDoc
64
+ */
65
+ public function get_query_args() {
66
+ $args = parent::get_query_args();
67
+
68
+ $args['search'] = array(
69
+ 'type' => 'string',
70
+ 'minLength' => 1,
71
+ );
72
+
73
+ return $args;
74
+ }
75
+
76
+ public function get_links() {
77
+ return array(
78
+ array(
79
+ 'rel' => 'item',
80
+ 'route' => 'lockout/(?P<lockout_id>[\d]+)',
81
+ 'title' => __( 'Lockout Details', 'better-wp-security' ),
82
+ 'methods' => WP_REST_Server::READABLE,
83
+ 'callback' => array( $this, 'get_lockout_details' ),
84
+ ),
85
+ array(
86
+ 'rel' => ITSEC_Lib_REST::LINK_REL . 'release-lockout',
87
+ 'route' => 'lockout/(?P<lockout_id>[\d]+)',
88
+ 'title' => __( 'Release Lockout', 'better-wp-security' ),
89
+ 'methods' => WP_REST_Server::DELETABLE,
90
+ 'cap' => ITSEC_Core::get_required_cap(),
91
+ 'callback' => array( $this, 'release_lockout' ),
92
+ ),
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Get the lockout details.
98
+ *
99
+ * @param WP_REST_Request $request
100
+ *
101
+ * @return array|WP_Error
102
+ */
103
+ public function get_lockout_details( $request ) {
104
+ /** @var ITSEC_Lockout $itsec_lockout */
105
+ global $itsec_lockout;
106
+
107
+ $lockout_id = (int) $request['lockout_id'];
108
+
109
+ if ( ! $lockout_id || ! $lockout = $itsec_lockout->get_lockout( $lockout_id ) ) {
110
+ return new WP_Error( 'not_found', __( 'Lockout Not Found', 'better-wp-security' ) );
111
+ }
112
+
113
+ return $this->prepare_lockout( $lockout, true );
114
+ }
115
+
116
+ /**
117
+ * Release the lockout.
118
+ *
119
+ * @param WP_REST_Request $request
120
+ *
121
+ * @return null|WP_Error
122
+ */
123
+ public function release_lockout( $request ) {
124
+ /** @var ITSEC_Lockout $itsec_lockout */
125
+ global $itsec_lockout;
126
+
127
+ $lockout_id = (int) $request['lockout_id'];
128
+
129
+ if ( ! $lockout_id || ! $lockout = $itsec_lockout->get_lockout( $lockout_id ) ) {
130
+ return new WP_Error( 'not_found', __( 'Lockout Not Found', 'better-wp-security' ) );
131
+ }
132
+
133
+ if ( ! $itsec_lockout->release_lockout( $lockout_id ) ) {
134
+ return new WP_Error( 'release_lockout_failed', __( 'Failed to release lockout.', 'better-wp-security' ) );
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ private function prepare_lockout( $lockout, $detail = false ) {
141
+ /** @var ITSEC_Lockout $itsec_lockout */
142
+ global $itsec_lockout;
143
+
144
+ $modules = $itsec_lockout->get_lockout_modules();
145
+
146
+ $data = array();
147
+
148
+ foreach ( $lockout as $key => $value ) {
149
+ $data[ str_replace( 'lockout_', '', $key ) ] = $value;
150
+ }
151
+
152
+ $data['active'] = (bool) $data['active'];
153
+
154
+ foreach ( array( 'start', 'start_gmt', 'expire', 'expire_gmt' ) as $date_prop ) {
155
+ $data[ $date_prop ] = ITSEC_Lib::to_rest_date( $data[ $date_prop ] );
156
+
157
+ $data["{$date_prop}_relative"] = human_time_diff( strtotime( $data[ $date_prop ] ) );
158
+ }
159
+
160
+ if ( ! empty( $data['host'] ) ) {
161
+ $data['label'] = $data['host'];
162
+ } elseif ( ! empty( $data['username'] ) ) {
163
+ $data['label'] = $data['username'];
164
+ } elseif ( ! empty( $data['user'] ) ) {
165
+ $user = get_userdata( $data['user'] );
166
+
167
+ $data['label'] = $user ? $user->display_name : sprintf( __( 'Deleted User %d', 'better-wp-security' ), $data['user'] );
168
+ } else {
169
+ $data['label'] = __( 'Unknown', 'better-wp-security' );
170
+ }
171
+
172
+ $data['description'] = isset( $modules[ $data['type'] ] ) ? $modules[ $data['type'] ]['reason'] : __( 'unknown reason.', 'better-wp-security' );
173
+
174
+ if ( $detail ) {
175
+ if ( ! empty( $data['host'] ) ) {
176
+ $entries = ITSEC_Log::get_entries( array(
177
+ 'init_timestamp' => $data['start_gmt'],
178
+ 'module' => 'lockout',
179
+ 'code' => "host-lockout::{$data['host']}",
180
+ ), 1, 1, 'timestamp', 'DESC', 'all' );
181
+ } elseif ( ! empty( $data['user'] ) ) {
182
+ $entries = ITSEC_Log::get_entries( array(
183
+ 'init_timestamp' => $data['start_gmt'],
184
+ 'module' => 'lockout',
185
+ 'code' => "user-lockout::{$data['user']}",
186
+ ), 1, 1, 'timestamp', 'DESC', 'all' );
187
+ } elseif ( ! empty( $data['username'] ) ) {
188
+ $entries = ITSEC_Log::get_entries( array(
189
+ 'init_timestamp' => $data['start_gmt'],
190
+ 'module' => 'lockout',
191
+ 'code' => "username-lockout::{$data['username']}",
192
+ ), 1, 1, 'timestamp', 'DESC', 'all' );
193
+ } else {
194
+ $entries = array();
195
+ }
196
+
197
+ if ( ! empty( $entries[0] ) ) {
198
+ $lockout_log = array(
199
+ 'id' => (int) $entries[0]['id'],
200
+ 'time' => ITSEC_Lib::to_rest_date( $entries[0]['init_timestamp'] ),
201
+ 'time_relative' => human_time_diff( strtotime( $entries[0]['init_timestamp'] ) ),
202
+ 'remote_ip' => $entries[0]['remote_ip'],
203
+ 'url' => $entries[0]['url'],
204
+ 'data' => $entries[0]['data'],
205
+ );
206
+ } else {
207
+ $lockout_log = array();
208
+ }
209
+
210
+ $data['detail'] = array(
211
+ 'log' => $lockout_log,
212
+ 'history' => array(),
213
+ );
214
+
215
+ switch ( $data['type'] ) {
216
+ case 'four_oh_four':
217
+ $logs = ITSEC_Log::get_entries( array(
218
+ 'module' => 'four_oh_four',
219
+ 'code' => 'found_404',
220
+ 'remote_ip' => $data['host'],
221
+ '__max_timestamp' => strtotime( $data['start_gmt'] ),
222
+ ), 100, 1, 'timestamp' );
223
+
224
+ if ( is_array( $logs ) ) {
225
+ foreach ( $logs as $log ) {
226
+ $data['detail']['history'][] = array(
227
+ 'id' => (int) $log['id'],
228
+ 'time' => ITSEC_Lib::to_rest_date( $log['init_timestamp'] ),
229
+ 'time_relative' => human_time_diff( strtotime( $log['init_timestamp'] ) ),
230
+ 'url' => $log['url'],
231
+ 'label' => $log['url'],
232
+ );
233
+ }
234
+ }
235
+ break;
236
+ case 'recaptcha':
237
+ $logs = ITSEC_Log::get_entries( array(
238
+ 'module' => 'recaptcha',
239
+ 'code' => 'failed-validation',
240
+ 'remote_ip' => $data['host'],
241
+ '__max_timestamp' => strtotime( $data['start_gmt'] ),
242
+ ), 100, 1, 'timestamp', 'DESC', 'all' );
243
+
244
+ if ( is_array( $logs ) ) {
245
+ foreach ( $logs as $log ) {
246
+ if ( is_wp_error( $log['data'] ) ) {
247
+ $label = $log['data']->get_error_code() === 'itsec-recaptcha-incorrect' ? __( 'Invalid Recaptcha', 'better-wp-security' ) : __( 'Skipped Recaptcha', 'better-wp-security' );
248
+ } else {
249
+ $label = __( 'Unknown', 'better-wp-security' );
250
+ }
251
+
252
+ $data['detail']['history'][] = array(
253
+ 'id' => (int) $log['id'],
254
+ 'time' => ITSEC_Lib::to_rest_date( $log['init_timestamp'] ),
255
+ 'time_relative' => human_time_diff( strtotime( $log['init_timestamp'] ) ),
256
+ 'url' => $log['url'],
257
+ 'label' => $label,
258
+ 'error' => is_wp_error( $log['data'] ) ? array(
259
+ 'code' => $log['data']->get_error_code(),
260
+ 'message' => $log['data']->get_error_message(),
261
+ ) : null,
262
+ );
263
+ }
264
+ }
265
+ break;
266
+ case 'brute_force':
267
+ $log_query = array(
268
+ 'module' => 'brute_force',
269
+ '__max_timestamp' => strtotime( $data['start_gmt'] ),
270
+ );
271
+
272
+ if ( ! empty( $data['host'] ) ) {
273
+ $log_query['remote_ip'] = $data['host'];
274
+ } elseif ( ! empty( $data['user'] ) ) {
275
+ $log_query['code'] = "invalid-login::user-{$data['user']}";
276
+ } elseif ( ! empty( $data['username'] ) ) {
277
+ $log_query['code'] = "invalid-login::username-{$data['username']}";
278
+ } else {
279
+ break;
280
+ }
281
+
282
+ $logs = ITSEC_Log::get_entries( $log_query, 100, 1, 'timestamp', 'DESC', 'all' );
283
+
284
+ if ( is_array( $logs ) ) {
285
+ foreach ( $logs as $log ) {
286
+ if ( ! empty( $data['host'] ) ) {
287
+ $label = $log['data']['username'];
288
+ } elseif ( ! empty( $data['username'] ) || ! empty( $data['user'] ) ) {
289
+ $label = $log['remote_ip'];
290
+ } else {
291
+ $label = '';
292
+ }
293
+
294
+ $data['detail']['history'][] = array(
295
+ 'id' => (int) $log['id'],
296
+ 'time' => ITSEC_Lib::to_rest_date( $log['init_timestamp'] ),
297
+ 'time_relative' => human_time_diff( strtotime( $log['init_timestamp'] ) ),
298
+ 'url' => $log['url'],
299
+ 'remote_ip' => $log['remote_ip'],
300
+ 'data' => array(
301
+ 'details' => $log['data']['details'],
302
+ 'user' => $log['data']['user'],
303
+ 'username' => $log['data']['username'],
304
+ 'user_id' => $log['data']['user_id'],
305
+ ),
306
+ 'label' => $label,
307
+ );
308
+ }
309
+ }
310
+ break;
311
+ }
312
+ }
313
+
314
+ return $data;
315
+ }
316
+ }
core/modules/dashboard/cards/class-itsec-dashboard-card-banned-users.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Dashboard_Card_Banned_Users extends ITSEC_Dashboard_Card {
4
+ public function get_slug() {
5
+ return 'banned-users-list';
6
+ }
7
+
8
+ public function get_label() {
9
+ return __( 'Banned Users' );
10
+ }
11
+
12
+ public function get_size() {
13
+ return [
14
+ 'minW' => 2,
15
+ 'minH' => 2,
16
+ 'maxW' => 3,
17
+ 'maxH' => 4,
18
+ 'defaultW' => 2,
19
+ 'defaultH' => 3,
20
+ ];
21
+ }
22
+
23
+ public function query_for_data( array $query_args, array $settings ) {
24
+ return [];
25
+ }
26
+ }
core/modules/dashboard/cards/class-itsec-dashboard-card-line-graph.php ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Dashboard_Card_Line_Graph
5
+ */
6
+ class ITSEC_Dashboard_Card_Line_Graph extends ITSEC_Dashboard_Card {
7
+
8
+ /** @var string */
9
+ private $slug;
10
+
11
+ /** @var string */
12
+ private $label;
13
+
14
+ /** @var array */
15
+ private $size;
16
+
17
+ /** @var array */
18
+ private $data_config;
19
+
20
+ /**
21
+ * ITSEC_Dashboard_Card_Line_Graph constructor.
22
+ *
23
+ * @param string $slug
24
+ * @param string $label
25
+ * @param array $size
26
+ * @param array $data_config
27
+ */
28
+ public function __construct( $slug, $label, array $data_config, array $size = array() ) {
29
+ $this->slug = $slug;
30
+ $this->label = $label;
31
+ $this->data_config = $data_config;
32
+ $this->size = wp_parse_args( $size, array(
33
+ 'minW' => 2,
34
+ 'minH' => 2,
35
+ 'maxW' => 3,
36
+ 'maxH' => 3,
37
+ 'defaultW' => 2,
38
+ 'defaultH' => 2,
39
+ ) );
40
+ }
41
+
42
+ /**
43
+ * @inheritDoc
44
+ */
45
+ public function query_for_data( array $query_args, array $settings ) {
46
+
47
+ if ( isset( $query_args['period'] ) ) {
48
+ $period = $query_args['period'];
49
+ } else {
50
+ $qa_schema = $this->get_query_args();
51
+ $period = $qa_schema['period']['default'];
52
+ }
53
+
54
+ $events = ITSEC_Dashboard_Util::query_events( ITSEC_Lib::flatten( wp_list_pluck( $this->data_config, 'events' ) ), $period );
55
+
56
+ if ( is_wp_error( $events ) ) {
57
+ return $events;
58
+ }
59
+
60
+ $data = array();
61
+
62
+ foreach ( $this->data_config as $config ) {
63
+ $key = implode( '--', (array) $config['events'] );
64
+
65
+ $data[ $key ] = array(
66
+ 'data' => array(),
67
+ 'label' => $config['label'],
68
+ );
69
+
70
+ foreach ( (array) $config['events'] as $event_name ) {
71
+ foreach ( $events[ $event_name ] as $event ) {
72
+ $date = ITSEC_Lib::to_rest_date( $event['time'] );
73
+
74
+ if ( isset( $data[ $key ]['data'][ $date ] ) ) {
75
+ $data[ $key ]['data'][ $date ]['y'] += $event['count'];
76
+ } else {
77
+ $data[ $key ]['data'][ $date ] = array(
78
+ 't' => $date,
79
+ 'y' => $event['count'],
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ $data[ $key ]['data'] = array_values( $data[ $key ]['data'] );
86
+ }
87
+
88
+ return $data;
89
+ }
90
+
91
+ public function get_query_args() {
92
+ $args = parent::get_query_args();
93
+
94
+ $args['period'] = ITSEC_Dashboard_REST::get_period_arg();
95
+
96
+ return $args;
97
+ }
98
+
99
+ /**
100
+ * @inheritDoc
101
+ */
102
+ public function get_type() {
103
+ return 'line';
104
+ }
105
+
106
+ /**
107
+ * @inheritDoc
108
+ */
109
+ public function get_slug() {
110
+ return $this->slug;
111
+ }
112
+
113
+ /**
114
+ * @inheritDoc
115
+ */
116
+ public function get_label() {
117
+ return $this->label;
118
+ }
119
+
120
+ /**
121
+ * @inheritDoc
122
+ */
123
+ public function get_size() {
124
+ return $this->size;
125
+ }
126
+ }
core/modules/dashboard/cards/class-itsec-dashboard-card-pie-chart.php ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Dashboard_Card_Pie_Chart
5
+ */
6
+ class ITSEC_Dashboard_Card_Pie_Chart extends ITSEC_Dashboard_Card {
7
+
8
+ /** @var string */
9
+ private $slug;
10
+
11
+ /** @var string */
12
+ private $label;
13
+
14
+ /** @var array */
15
+ private $data_config;
16
+
17
+ /** @var array */
18
+ private $size;
19
+
20
+ /** @var array */
21
+ private $options;
22
+
23
+ /**
24
+ * ITSEC_Dashboard_Card_Line_Graph constructor.
25
+ *
26
+ * @param string $slug
27
+ * @param string $label
28
+ * @param array $data_config
29
+ * @param array $options
30
+ */
31
+ public function __construct( $slug, $label, array $data_config, array $options = array() ) {
32
+ $this->slug = $slug;
33
+ $this->label = $label;
34
+ $this->data_config = $data_config;
35
+
36
+ $options = $this->options = wp_parse_args( $options, array(
37
+ 'size' => array(),
38
+ 'dated' => false,
39
+ 'circle_callback' => null,
40
+ 'circle_label' => __( 'Total', 'better-wp-security' ),
41
+ ) );
42
+
43
+ $this->size = wp_parse_args( $options['size'], array(
44
+ 'minW' => 1,
45
+ 'minH' => 2,
46
+ 'maxW' => 2,
47
+ 'maxH' => 2,
48
+ 'defaultW' => 1,
49
+ 'defaultH' => 2,
50
+ ) );
51
+ }
52
+
53
+ /**
54
+ * @inheritDoc
55
+ */
56
+ public function query_for_data( array $query_args, array $settings ) {
57
+
58
+ if ( ! $this->options['dated'] ) {
59
+ $period = false;
60
+ } elseif ( isset( $query_args['period'] ) ) {
61
+ $period = $query_args['period'];
62
+ } else {
63
+ $qa_schema = $this->get_query_args();
64
+ $period = $qa_schema['period']['default'];
65
+ }
66
+
67
+ $events = ITSEC_Dashboard_Util::query_events( ITSEC_Lib::flatten( wp_list_pluck( $this->data_config, 'events' ) ), $period );
68
+
69
+ if ( is_wp_error( $events ) ) {
70
+ return $events;
71
+ }
72
+
73
+ $data = array(
74
+ 'data' => array(),
75
+ 'circle_sum' => 0,
76
+ 'circle_label' => $this->options['circle_label'],
77
+ );
78
+
79
+ foreach ( $this->data_config as $config ) {
80
+ $key = implode( '--', (array) $config['events'] );
81
+
82
+ $data['data'][ $key ] = array(
83
+ 'data' => array(),
84
+ 'sum' => 0,
85
+ 'label' => $config['label'],
86
+ );
87
+
88
+ foreach ( (array) $config['events'] as $event_name ) {
89
+ foreach ( $events[ $event_name ] as $event ) {
90
+ $date = ITSEC_Lib::to_rest_date( $event['time'] );
91
+
92
+ $data['circle_sum'] += $event['count'];
93
+ $data['data'][ $key ]['sum'] += $event['count'];
94
+
95
+ if ( isset( $data[ $key ]['data'][ $date ] ) ) {
96
+ $data[ $key ]['data'][ $date ]['y'] += $event['count'];
97
+ } else {
98
+ $data[ $key ]['data'][ $date ] = array(
99
+ 'x' => $date,
100
+ 'y' => $event['count'],
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ $data[ $key ]['data'] = array_values( $data[ $key ]['data'] );
107
+ }
108
+
109
+ if ( $this->options['circle_callback'] ) {
110
+ $data['circle_sum'] = call_user_func( $this->options['circle_callback'], $query_args, $settings );
111
+ }
112
+
113
+ return $data;
114
+ }
115
+
116
+ public function get_query_args() {
117
+ $args = parent::get_query_args();
118
+
119
+ if ( $this->options['dated'] ) {
120
+ $args['period'] = ITSEC_Dashboard_REST::get_period_arg();
121
+ }
122
+
123
+ return $args;
124
+ }
125
+
126
+ /**
127
+ * @inheritDoc
128
+ */
129
+ public function get_type() {
130
+ return 'pie';
131
+ }
132
+
133
+ /**
134
+ * @inheritDoc
135
+ */
136
+ public function get_slug() {
137
+ return $this->slug;
138
+ }
139
+
140
+ /**
141
+ * @inheritDoc
142
+ */
143
+ public function get_label() {
144
+ return $this->label;
145
+ }
146
+
147
+ /**
148
+ * @inheritDoc
149
+ */
150
+ public function get_size() {
151
+ return $this->size;
152
+ }
153
+ }
core/modules/dashboard/cards/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/class-itsec-dashboard-rest.php ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Dashboard_REST
5
+ */
6
+ class ITSEC_Dashboard_REST {
7
+
8
+ public function run() {
9
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
10
+ add_filter( 'rest_route_data', array( $this, 'filter_route_data' ) );
11
+ add_filter( 'rest_pre_dispatch', array( $this, 'handle_options_request' ), 100, 3 );
12
+ add_filter( 'rest_request_before_callbacks', array( $this, 'return_permission_errors_before_validation' ), 10, 3 );
13
+ }
14
+
15
+ /**
16
+ * Register dashboard REST routes.
17
+ */
18
+ public function register_routes() {
19
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-util.php' );
20
+ require_once( dirname( __FILE__ ) . '/rest/abstract-itsec-rest-dashboard-controller.php' );
21
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-dashboards-controller.php' );
22
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-cards-controller.php' );
23
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-card-controller.php' );
24
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-unknown-card-controller.php' );
25
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-available-cards-controller.php' );
26
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-layout-controller.php' );
27
+ require_once( dirname( __FILE__ ) . '/rest/class-itsec-rest-dashboard-static-controller.php' );
28
+
29
+ $available = new ITSEC_REST_Dashboard_Available_Cards_Controller();
30
+ $available->register_routes();
31
+
32
+ $dashboards = new ITSEC_REST_Dashboard_Dashboards_Controller();
33
+ $dashboards->register_routes();
34
+
35
+ $layout = new ITSEC_REST_Dashboard_Layout_Controller();
36
+ $layout->register_routes();
37
+
38
+ $cards = new ITSEC_REST_Dashboard_Cards_Controller();
39
+ $cards->register_routes();
40
+
41
+ $static = new ITSEC_REST_Dashboard_Static_Controller();
42
+ $static->register_routes();
43
+ }
44
+
45
+ /**
46
+ * Remove any dashboard routes from the index.
47
+ *
48
+ * @param array $available
49
+ *
50
+ * @return array
51
+ */
52
+ public function filter_route_data( $available ) {
53
+
54
+ foreach ( $available as $route => $data ) {
55
+ if ( strpos( $route, '/ithemes-security/v1/dashboards' ) === 0 ) {
56
+ unset( $available[ $route ] );
57
+ }
58
+ }
59
+
60
+ return $available;
61
+ }
62
+
63
+ /**
64
+ * Return an empty response for any options requests to the dashboard.
65
+ *
66
+ * @param WP_REST_Response $response
67
+ * @param WP_REST_Server $server
68
+ * @param WP_REST_Request $request
69
+ *
70
+ * @return WP_REST_Response
71
+ */
72
+ public function handle_options_request( $response, $server, $request ) {
73
+
74
+ if ( 'OPTIONS' !== $request->get_method() ) {
75
+ return $response;
76
+ }
77
+
78
+ if ( strpos( $request->get_route(), '/ithemes-security/v1/dashboards' ) === false ) {
79
+ return $response;
80
+ }
81
+
82
+ $response->set_data( array() );
83
+
84
+ return $response;
85
+ }
86
+
87
+ /**
88
+ * Return permission errors before a validation error.
89
+ *
90
+ * @param WP_REST_Response|WP_Error $response
91
+ * @param array $handler
92
+ * @param WP_REST_Request $request
93
+ *
94
+ * @return WP_REST_Response|WP_Error
95
+ */
96
+ public function return_permission_errors_before_validation( $response, $handler, $request ) {
97
+
98
+ if ( ! is_wp_error( $response ) || strpos( $request->get_route(), '/ithemes-security/v1/dashboards' ) === false ) {
99
+ return $response;
100
+ }
101
+
102
+ if ( ! empty( $handler['permission_callback'] ) ) {
103
+ $permission = call_user_func( $handler['permission_callback'], $request );
104
+
105
+ if ( is_wp_error( $permission ) ) {
106
+ $response = $permission;
107
+ } elseif ( false === $permission || null === $permission ) {
108
+ $response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => rest_authorization_required_code() ) );
109
+ }
110
+ }
111
+
112
+ return $response;
113
+ }
114
+
115
+ /**
116
+ * Return either the masked not found error, or the given error if the user can manage ITSEC or debug mode is enabled.
117
+ *
118
+ * @param WP_Error|null $error
119
+ *
120
+ * @return WP_Error
121
+ */
122
+ public static function not_found_error( $error = null ) {
123
+ if ( $error && ( ( defined( 'ITSEC_DEBUG' ) && ITSEC_DEBUG ) || ITSEC_Core::current_user_can_manage() ) ) {
124
+ return $error;
125
+ }
126
+
127
+ return new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) );
128
+ }
129
+
130
+ /**
131
+ * Get the definition for the period collection param.
132
+ *
133
+ * @return array
134
+ */
135
+ public static function get_period_arg() {
136
+ return array(
137
+ 'default' => ITSEC_Dashboard_Util::P_30_DAYS,
138
+ 'oneOf' => array(
139
+ array(
140
+ 'type' => 'object',
141
+ 'additionalProperties' => false,
142
+ 'properties' => array(
143
+ 'start' => array(
144
+ 'type' => 'string',
145
+ 'format' => 'date-time',
146
+ 'required' => true,
147
+ ),
148
+ 'end' => array(
149
+ 'type' => 'string',
150
+ 'format' => 'date-time',
151
+ 'required' => true,
152
+ ),
153
+ ),
154
+ ),
155
+ array(
156
+ 'type' => 'string',
157
+ 'enum' => array( ITSEC_Dashboard_Util::P_24_HOURS, ITSEC_Dashboard_Util::P_WEEK, ITSEC_Dashboard_Util::P_30_DAYS )
158
+ ),
159
+ ),
160
+ );
161
+ }
162
+ }
core/modules/dashboard/class-itsec-dashboard-util.php ADDED
@@ -0,0 +1,917 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Dashboard_Util
5
+ */
6
+ class ITSEC_Dashboard_Util {
7
+
8
+ const P_24_HOURS = '24-hours';
9
+ const P_WEEK = 'week';
10
+ const P_30_DAYS = '30-days';
11
+
12
+ /** @var string[] WordPress breakpoints. */
13
+ public static $breakpoints = array( 'huge', 'wide', 'large', 'medium', 'mobile' );
14
+
15
+ /** @var ITSEC_Dashboard_Card[] */
16
+ private static $registered_cards;
17
+
18
+ /** @var array */
19
+ private static $_query_cache = array();
20
+
21
+ /**
22
+ * Get all registered cards.
23
+ *
24
+ * @return ITSEC_Dashboard_Card[]
25
+ */
26
+ public static function get_registered_cards() {
27
+ if ( ! isset( self::$registered_cards ) ) {
28
+ $cards = ITSEC_Modules::get_container()->get( 'dashboard.cards' );
29
+
30
+ self::$registered_cards = apply_filters( 'itsec_dashboards_cards', $cards );
31
+ }
32
+
33
+ return self::$registered_cards;
34
+ }
35
+
36
+ /**
37
+ * Get the card definition.
38
+ *
39
+ * @param string $slug
40
+ *
41
+ * @return ITSEC_Dashboard_Card|null
42
+ */
43
+ public static function get_card( $slug ) {
44
+
45
+ foreach ( self::get_registered_cards() as $card ) {
46
+ if ( $card->get_slug() === $slug ) {
47
+ return $card;
48
+ }
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Get the dashboards that are owned by the given user.
56
+ *
57
+ * @param WP_User|int|string|false $user
58
+ * @param string $return
59
+ *
60
+ * @return WP_Post[]|int[]
61
+ */
62
+ public static function get_owned_dashboards( $user = false, $return = 'posts' ) {
63
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
64
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
65
+ }
66
+
67
+ $user = ITSEC_Lib::get_user( $user );
68
+
69
+ if ( isset( self::$_query_cache['owned'][ $user->ID ] ) ) {
70
+ $ids = self::$_query_cache['owned'][ $user->ID ];
71
+
72
+ if ( 'ids' === $return ) {
73
+ return $ids;
74
+ }
75
+
76
+ return array_map( 'get_post', $ids );
77
+ }
78
+
79
+ $query = new WP_Query( array(
80
+ 'fields' => $return === 'ids' ? 'ids' : '',
81
+ 'post_type' => ITSEC_Dashboard::CPT_DASHBOARD,
82
+ 'no_found_rows' => true,
83
+ 'posts_per_page' => - 1,
84
+ 'author' => $user->ID,
85
+ ) );
86
+
87
+ self::$_query_cache['owned'][ $user->ID ] = array();
88
+
89
+ foreach ( $query->posts as $post ) {
90
+ self::$_query_cache['owned'][ $user->ID ][] = is_numeric( $post ) ? $post : $post->ID;
91
+ }
92
+
93
+ return $query->posts;
94
+ }
95
+
96
+ /**
97
+ * Get the dashboards that have been shared with the given user.
98
+ *
99
+ * @param WP_User|int|string|false $user
100
+ * @param string $return
101
+ *
102
+ * @return WP_Post[]|int[]
103
+ */
104
+ public static function get_shared_dashboards( $user = false, $return = 'posts' ) {
105
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
106
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
107
+ }
108
+
109
+ $user = ITSEC_Lib::get_user( $user );
110
+
111
+ if ( isset( self::$_query_cache['shared'][ $user->ID ] ) ) {
112
+ $ids = self::$_query_cache['shared'][ $user->ID ];
113
+
114
+ if ( 'ids' === $return ) {
115
+ return $ids;
116
+ }
117
+
118
+ return array_map( 'get_post', $ids );
119
+ }
120
+
121
+ $query = new WP_Query( array(
122
+ 'fields' => $return === 'ids' ? 'ids' : '',
123
+ 'post_type' => ITSEC_Dashboard::CPT_DASHBOARD,
124
+ 'no_found_rows' => true,
125
+ 'posts_per_page' => - 1,
126
+ 'author__not_in' => $user->ID,
127
+ 'meta_query' => array(
128
+ 'relation' => 'OR',
129
+ array(
130
+ 'key' => ITSEC_Dashboard::META_SHARE_USER,
131
+ 'value' => $user->ID,
132
+ ),
133
+ array(
134
+ 'key' => ITSEC_Dashboard::META_SHARE_ROLE,
135
+ 'value' => $user->roles,
136
+ ),
137
+ ),
138
+ ) );
139
+
140
+ self::$_query_cache['shared'][ $user->ID ] = array();
141
+
142
+ foreach ( $query->posts as $post ) {
143
+ self::$_query_cache['shared'][ $user->ID ][] = is_numeric( $post ) ? $post : $post->ID;
144
+ }
145
+
146
+ return $query->posts;
147
+ }
148
+
149
+ /**
150
+ * Can the given user access the configuration for the given card.
151
+ *
152
+ * This checks that they have a dashbaord
153
+ *
154
+ * @param string $card
155
+ * @param WP_User|int|string|false $user
156
+ *
157
+ * @return bool
158
+ */
159
+ public static function can_access_card( $card, $user = false ) {
160
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
161
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
162
+ }
163
+
164
+ $user = ITSEC_Lib::get_user( $user );
165
+
166
+ if ( user_can( $user, 'itsec_create_dashboards' ) ) {
167
+ return true;
168
+ }
169
+
170
+ if ( ! isset( self::$_query_cache['allowed_cards'][ $user->ID ] ) ) {
171
+ $ids = self::get_shared_dashboards( $user, 'ids' );
172
+ $id_where = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
173
+
174
+ global $wpdb;
175
+
176
+ if ( $ids ) {
177
+ $cards = $wpdb->get_col( $wpdb->prepare(
178
+ "SELECT DISTINCT `meta_value` FROM {$wpdb->postmeta} AS t1 JOIN {$wpdb->posts} AS t2 ON (t1.`post_id` = t2.`ID`) WHERE t1.`meta_key` = %s AND t2.`post_parent` IN ({$id_where})",
179
+ ITSEC_Dashboard::META_CARD,
180
+ ...$ids
181
+ ) );
182
+ } else {
183
+ $cards = false;
184
+ }
185
+
186
+ if ( false === $cards ) {
187
+ self::$_query_cache['allowed_cards'][ $user->ID ] = array();
188
+ } else {
189
+ self::$_query_cache['allowed_cards'][ $user->ID ] = array_flip( $cards );
190
+ }
191
+ }
192
+
193
+ return isset( self::$_query_cache['allowed_cards'][ $user->ID ][ $card ] );
194
+ }
195
+
196
+ /**
197
+ * Get cards for a dashboard.
198
+ *
199
+ * @param int $dashboard_id
200
+ *
201
+ * @return WP_Post[]
202
+ */
203
+ public static function get_dashboard_cards( $dashboard_id ) {
204
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
205
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
206
+ }
207
+
208
+ if ( isset( self::$_query_cache['cards'][ $dashboard_id ] ) ) {
209
+ return array_map( 'get_post', self::$_query_cache['cards'][ $dashboard_id ] );
210
+ }
211
+
212
+ $query = new WP_Query( array(
213
+ 'post_type' => ITSEC_Dashboard::CPT_CARD,
214
+ 'post_parent' => $dashboard_id,
215
+ 'posts_per_page' => - 1,
216
+ ) );
217
+
218
+ self::$_query_cache['cards'][ $dashboard_id ] = array();
219
+
220
+ foreach ( $query->posts as $post ) {
221
+ self::$_query_cache['cards'][ $dashboard_id ][] = $post->ID;
222
+ }
223
+
224
+ return $query->posts;
225
+ }
226
+
227
+ /**
228
+ * Get the primary dashboard for the given user.
229
+ *
230
+ * Will set if none specified and user has available dashboards.
231
+ *
232
+ * @param WP_User|string|int|false $user
233
+ *
234
+ * @return int
235
+ */
236
+ public static function get_primary_dashboard_id( $user = false ) {
237
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
238
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
239
+ }
240
+
241
+ $user = ITSEC_Lib::get_user( $user );
242
+ $primary_id = (int) get_user_meta( $user->ID, ITSEC_Dashboard::META_PRIMARY, true );
243
+
244
+ if ( ! $primary_id || ITSEC_Dashboard::CPT_DASHBOARD !== get_post_type( $primary_id ) ) {
245
+ if ( $owned = self::get_owned_dashboards() ) {
246
+ $primary_id = (int) $owned[0]->ID;
247
+ } elseif ( $shared = self::get_shared_dashboards() ) {
248
+ $primary_id = (int) $shared[0]->ID;
249
+ } else {
250
+ $primary_id = 0;
251
+ }
252
+
253
+ if ( $primary_id ) {
254
+ update_user_meta( $user->ID, ITSEC_Dashboard::META_PRIMARY, $primary_id );
255
+ }
256
+ }
257
+
258
+ return $primary_id;
259
+ }
260
+
261
+ /**
262
+ * Record an occurrence of an event.
263
+ *
264
+ * @param string $slug
265
+ *
266
+ * @return bool
267
+ */
268
+ public static function record_event( $slug ) {
269
+
270
+ $hour_time = date( 'Y-m-d H:00:00', ITSEC_Core::get_current_time_gmt() );
271
+
272
+ global $wpdb;
273
+ $r = $wpdb->query( $wpdb->prepare(
274
+ "INSERT INTO {$wpdb->base_prefix}itsec_dashboard_events (`event_slug`,`event_time`) VALUES (%s, %s) ON DUPLICATE KEY UPDATE `event_count` = `event_count` + 1",
275
+ $slug, $hour_time
276
+ ) );
277
+
278
+ return false !== $r;
279
+ }
280
+
281
+ /**
282
+ * Consolidate events.
283
+ *
284
+ * We initially track events hourly for 24 hours, and then consolidate the events into a single day entry.
285
+ */
286
+ public static function consolidate_events() {
287
+
288
+ // We want to ensure we can show the past 24 hours of events.
289
+
290
+ $now = ITSEC_Core::get_current_time_gmt(); // 2018-10-05 6:30:00
291
+ $max = $now - 2 * DAY_IN_SECONDS; // 2018-10-03 6:30:00
292
+
293
+ $consolidate_before = date( 'Y-m-d 23:59:59', $max ); // 2018-10-03 23:59:59
294
+
295
+ global $wpdb;
296
+
297
+ $r = $wpdb->query( $wpdb->prepare(
298
+ "INSERT INTO {$wpdb->base_prefix}itsec_dashboard_events ( `event_slug`, `event_time`, `event_count`, `event_consolidated`)
299
+ SELECT
300
+ `event_slug`,
301
+ str_to_date(concat(year(`event_time`), '-', month(`event_time`), '-', day(`event_time`),'-'), '%%Y-%%m-%%d') as `event_time`,
302
+ sum(`event_count`) as `event_count`,
303
+ 1 as `event_consolidated`
304
+ FROM {$wpdb->base_prefix}itsec_dashboard_events
305
+ WHERE
306
+ `event_consolidated` = 0 AND
307
+ `event_time` < %s
308
+ GROUP BY `event_slug`, year(`event_time`), month(`event_time`), day(`event_time`)
309
+ ON DUPLICATE KEY UPDATE `event_slug` = `event_slug`",
310
+ $consolidate_before
311
+ ) );
312
+
313
+ if ( false !== $r ) {
314
+ $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->base_prefix}itsec_dashboard_events WHERE `event_consolidated` = 0 AND `event_time` < %s", $consolidate_before ) );
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Count events.
320
+ *
321
+ * @param array|string $slug_or_slugs
322
+ * @param array|string|false $period
323
+ *
324
+ * @return array|int[]|WP_Error
325
+ */
326
+ public static function count_events( $slug_or_slugs, $period = false ) {
327
+
328
+ if ( false === $period ) {
329
+ $period = array(
330
+ 'start' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() - 2 * MONTH_IN_SECONDS ),
331
+ 'end' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() ),
332
+ );
333
+ }
334
+
335
+ $slugs = (array) $slug_or_slugs;
336
+
337
+ if ( is_wp_error( $range = self::_get_range( $period ) ) ) {
338
+ return $range;
339
+ }
340
+
341
+ list( $start, $end ) = $range;
342
+
343
+ $prepare = array(
344
+ date( 'Y-m-d H:i:s', $start ),
345
+ date( 'Y-m-d H:i:s', $end ),
346
+ );
347
+
348
+ $slug_where = implode( ', ', array_fill( 0, count( $slugs ), '%s' ) );
349
+ $prepare = array_merge( $prepare, $slugs );
350
+
351
+ global $wpdb;
352
+ $r = $wpdb->get_results( $wpdb->prepare(
353
+ "SELECT sum(`event_count`) as `c`, `event_slug` as `s` FROM {$wpdb->base_prefix}itsec_dashboard_events WHERE `event_time` BETWEEN %s AND %s AND `event_slug` IN ({$slug_where}) GROUP BY `event_slug` ORDER BY `event_time` DESC",
354
+ $prepare
355
+ ) );
356
+
357
+ if ( false === $r ) {
358
+ return new WP_Error( 'itsec-dashboard-query-count-events-db-error', __( 'Error when querying the database for counting events.', 'better-wp-security' ) );
359
+ }
360
+
361
+ $events = array();
362
+
363
+ foreach ( $r as $row ) {
364
+ $events[ $row->s ] = (int) $row->c;
365
+ }
366
+
367
+ foreach ( $slugs as $slug ) {
368
+ if ( ! isset( $events[ $slug ] ) ) {
369
+ $events[ $slug ] = 0;
370
+ }
371
+ }
372
+
373
+ return $events;
374
+ }
375
+
376
+ /**
377
+ * Retrieve events.
378
+ *
379
+ * @param array|string $slug_or_slugs
380
+ * @param array|string|false $period
381
+ *
382
+ * @return array|int[]|WP_Error
383
+ */
384
+ public static function query_events( $slug_or_slugs, $period = false ) {
385
+
386
+ if ( false === $period ) {
387
+ $period = array(
388
+ 'start' => date( 'Y-m-d 00:00:00', ITSEC_Core::get_current_time_gmt() - 2 * MONTH_IN_SECONDS ),
389
+ 'end' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
390
+ );
391
+ }
392
+
393
+ $slugs = (array) $slug_or_slugs;
394
+
395
+ if ( is_wp_error( $range = self::_get_range( $period ) ) ) {
396
+ return $range;
397
+ }
398
+
399
+ list( $start, $end ) = $range;
400
+
401
+ $prepare = array(
402
+ date( 'Y-m-d H:i:s', $start ),
403
+ date( 'Y-m-d H:i:s', $end ),
404
+ );
405
+
406
+ $slug_where = implode( ', ', array_fill( 0, count( $slugs ), '%s' ) );
407
+ $prepare = array_merge( $prepare, $slugs );
408
+
409
+ global $wpdb;
410
+ $r = $wpdb->get_results( $wpdb->prepare(
411
+ "SELECT `event_time` as `t`, `event_count` as `c`, `event_slug` as `s` FROM {$wpdb->base_prefix}itsec_dashboard_events WHERE `event_time` BETWEEN %s AND %s AND `event_slug` IN ({$slug_where}) ORDER BY `event_time` DESC",
412
+ $prepare
413
+ ) );
414
+
415
+ if ( false === $r ) {
416
+ return new WP_Error( 'itsec-dashboard-query-events-db-error', __( 'Error when querying the database for events.', 'better-wp-security' ) );
417
+ }
418
+
419
+ if ( self::P_24_HOURS === $period ) {
420
+ $format = 'Y-m-d H:00:00';
421
+ $increment = '+1 hour';
422
+ } else {
423
+ $format = 'Y-m-d';
424
+ $increment = '+1 day';
425
+ }
426
+
427
+ $events = array_combine( $slugs, array_pad( array(), count( $slugs ), array() ) );
428
+
429
+ foreach ( $r as $row ) {
430
+ $key = date( $format, strtotime( $row->t ) );
431
+
432
+ if ( isset( $events[ $row->s ][ $key ] ) ) {
433
+ $events[ $row->s ][ $key ] += $row->c; // Handle unconsolidated rows.
434
+ } else {
435
+ $events[ $row->s ][ $key ] = (int) $row->c;
436
+ }
437
+ }
438
+
439
+ $retval = array();
440
+
441
+ foreach ( $events as $slug => $slug_events ) {
442
+ $slug_events = self::fill_gaps( $slug_events, $start, $end, $format, $increment );
443
+
444
+ foreach ( $slug_events as $time => $count ) {
445
+ $retval[ $slug ][] = array(
446
+ 'time' => $time,
447
+ 'count' => $count,
448
+ );
449
+ }
450
+ }
451
+
452
+ return $retval;
453
+ }
454
+
455
+ /**
456
+ * Retrieve the total number of events.
457
+ *
458
+ * @param array|string|false $period
459
+ *
460
+ * @return int|WP_Error
461
+ */
462
+ public static function total_events( $period = false ) {
463
+
464
+ if ( false === $period ) {
465
+ $period = array(
466
+ 'start' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() - 2 * MONTH_IN_SECONDS ),
467
+ 'end' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() ),
468
+ );
469
+ }
470
+
471
+ if ( is_wp_error( $range = self::_get_range( $period ) ) ) {
472
+ return $range;
473
+ }
474
+
475
+ list( $start, $end ) = $range;
476
+
477
+ $prepare = array(
478
+ date( 'Y-m-d H:i:s', $start ),
479
+ date( 'Y-m-d H:i:s', $end ),
480
+ );
481
+
482
+ global $wpdb;
483
+ $count = $wpdb->get_var( $wpdb->prepare(
484
+ "SELECT sum(`event_count`) as `c` FROM {$wpdb->base_prefix}itsec_dashboard_events WHERE `event_time` BETWEEN %s AND %s",
485
+ $prepare
486
+ ) );
487
+
488
+ if ( false === $count ) {
489
+ return new WP_Error( 'itsec-dashboard-total-events-db-error', __( 'Error when querying the database for total events.', 'better-wp-security' ) );
490
+ }
491
+
492
+ return (int) $count;
493
+ }
494
+
495
+ /**
496
+ * Count the total IPs we are monitoring in the log.
497
+ *
498
+ * @param array|string|false $period
499
+ *
500
+ * @return int|WP_Error
501
+ */
502
+ public static function total_ips( $period = false ) {
503
+
504
+ if ( false === $period ) {
505
+ $period = array(
506
+ 'start' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() - 2 * MONTH_IN_SECONDS ),
507
+ 'end' => date( 'Y-m-d', ITSEC_Core::get_current_time_gmt() ),
508
+ );
509
+ }
510
+
511
+ if ( is_wp_error( $range = self::_get_range( $period ) ) ) {
512
+ return $range;
513
+ }
514
+
515
+ list( $start, $end ) = $range;
516
+
517
+ $prepare = array(
518
+ date( 'Y-m-d H:i:s', $start ),
519
+ date( 'Y-m-d H:i:s', $end ),
520
+ );
521
+
522
+ global $wpdb;
523
+ $count = $wpdb->get_var( $wpdb->prepare(
524
+ "SELECT COUNT( DISTINCT( `remote_ip` ) ) as c FROM {$wpdb->base_prefix}itsec_logs WHERE `timestamp` BETWEEN %s and %s",
525
+ $prepare )
526
+ );
527
+
528
+ if ( false === $count ) {
529
+ return new WP_Error( 'itsec-dashboard-total-ips-db-error', __( 'Error when querying the database for total IPs tracked.', 'better-wp-security' ) );
530
+ }
531
+
532
+ return (int) $count;
533
+ }
534
+
535
+ /**
536
+ * Fill the gaps in a range of days
537
+ *
538
+ * @param array $events
539
+ * @param int $start
540
+ * @param int $end
541
+ * @param string $format
542
+ * @param string $increment
543
+ *
544
+ * @return array
545
+ */
546
+ private static function fill_gaps( $events, $start, $end, $format = 'Y-m-d', $increment = '+1 day' ) {
547
+
548
+ $now = date( $format, $start );
549
+ $end_d = date( $format, $end );
550
+
551
+ while ( $now < $end_d ) {
552
+ if ( ! isset( $events[ $now ] ) ) {
553
+ $events[ $now ] = 0;
554
+ }
555
+
556
+ $now = date( $format, strtotime( "{$now} {$increment}" ) );
557
+ }
558
+
559
+ ksort( $events );
560
+
561
+ return $events;
562
+ }
563
+
564
+ /**
565
+ * Get the date range for the report query.
566
+ *
567
+ * @param string|array $period
568
+ *
569
+ * @return int[]|WP_Error
570
+ */
571
+ public static function _get_range( $period ) {
572
+ if ( is_array( $period ) ) {
573
+ if ( ! isset( $period['start'], $period['end'] ) ) {
574
+ return new WP_Error( 'itsec-dashboard-events-invalid-period', __( 'Invalid Period', 'better-wp-security' ) );
575
+ }
576
+
577
+ if ( false === ( $s = strtotime( $period['start'] ) ) || false === ( $e = strtotime( $period['end'] ) ) ) {
578
+ return new WP_Error( 'itsec-dashboard-events-invalid-period', __( 'Invalid Period', 'better-wp-security' ) );
579
+ }
580
+
581
+ return array( $s, $e );
582
+ }
583
+
584
+ switch ( $period ) {
585
+ case self::P_24_HOURS:
586
+ return array(
587
+ ( ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS ) - ( ( ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS ) % HOUR_IN_SECONDS ),
588
+ ITSEC_Core::get_current_time_gmt(),
589
+ );
590
+ case self::P_WEEK:
591
+ return array(
592
+ strtotime( '-1 week', ITSEC_Core::get_current_time_gmt() ),
593
+ ITSEC_Core::get_current_time_gmt(),
594
+ );
595
+ case self::P_30_DAYS:
596
+ return array(
597
+ strtotime( '-30 days', ITSEC_Core::get_current_time_gmt() ),
598
+ ITSEC_Core::get_current_time_gmt(),
599
+ );
600
+ }
601
+
602
+ return new WP_Error( 'itsec-dashboard-events-invalid-period', __( 'Invalid Period', 'better-wp-security' ) );
603
+ }
604
+
605
+ /**
606
+ * Bulk insert events.
607
+ *
608
+ * @param string $slug
609
+ * @param array $day_to_count
610
+ *
611
+ * @return int|WP_Error
612
+ */
613
+ public static function bulk_insert( $slug, $day_to_count ) {
614
+
615
+ global $wpdb;
616
+
617
+ $prepare = array();
618
+ $query = "INSERT INTO {$wpdb->base_prefix}itsec_dashboard_events ( `event_slug`, `event_time`, `event_count`, `event_consolidated` ) VALUES ";
619
+
620
+ foreach ( $day_to_count as $time => $count ) {
621
+ $query .= '(%s,%s,%d,1),';
622
+ $prepare[] = array( $slug, $time, $count );
623
+ }
624
+
625
+ $query = substr( $query, 0, - 1 );
626
+
627
+ if ( $prepare ) {
628
+ $prepare = call_user_func_array( 'array_merge', $prepare );
629
+ }
630
+
631
+ $r = $wpdb->query( $wpdb->prepare( $query, $prepare ) );
632
+
633
+ if ( false === $r ) {
634
+ return new WP_Error( 'itsec-dashboard-bulk-insert-query-failed', __( 'Failed to bulk insert events.', 'better-wp-security' ) );
635
+ }
636
+
637
+ return $r;
638
+ }
639
+
640
+ /**
641
+ * Backfill dashboard events from log entries.
642
+ *
643
+ * @param string $event Event slug.
644
+ * @param array $filters
645
+ *
646
+ * @return int|WP_Error
647
+ */
648
+ public static function backfill_events_from_logs( $event, $filters ) {
649
+ if ( ITSEC_Modules::get_setting( 'global', 'log_type' ) === 'file' ) {
650
+ return new WP_Error( 'itsec-dashboard-backfill-logs-invalid-log-type', __( 'Cannot backfill logs if DB logs are not available.', 'better-wp-security' ) );
651
+ }
652
+
653
+ $wheres = $prepare = array();
654
+
655
+ global $wpdb;
656
+
657
+ $prepare[] = date( 'Y-m-d H:i:s', strtotime( '-60 days' ) );
658
+ $wheres[] = '`timestamp` > %s';
659
+
660
+ if ( isset( $filters['module'] ) ) {
661
+ $prepare[] = $filters['module'];
662
+ $wheres[] = '`module` = %s';
663
+ }
664
+
665
+ if ( isset( $filters['code'] ) ) {
666
+ if ( empty( $filters['code_like'] ) ) {
667
+ if ( is_array( $filters['code'] ) ) {
668
+ $wheres[] = '`code` IN (' . implode( ', ', array_fill( 0, count( $filters['code'] ), '%s' ) ) . ')';
669
+ $prepare = array_merge( $prepare, $filters['code'] );
670
+ } else {
671
+ $prepare[] = $filters['code'];
672
+ $wheres[] = '`code` = %s';
673
+ }
674
+ } else {
675
+ $prepare[] = $wpdb->esc_like( $filters['code'] ) . '%';
676
+ $wheres[] = '`code` LIKE %s';
677
+ }
678
+ }
679
+
680
+ if ( isset( $filters['where'] ) ) {
681
+ $wheres = array_merge( $wheres, $filters['where'] );
682
+ }
683
+
684
+ $where = 'WHERE ' . implode( ' AND ', $wheres );
685
+
686
+ $r = $wpdb->get_results( $wpdb->prepare(
687
+ "SELECT count(`id`) as c, `timestamp` FROM {$wpdb->base_prefix}itsec_logs {$where} GROUP BY MONTH(`timestamp`), DAY(`timestamp`)",
688
+ $prepare
689
+ ) );
690
+
691
+ if ( false === $r ) {
692
+ return new WP_Error( 'itsec-dashboard-backfill-logs-query-error', __( 'Database error when backfilling logs.', 'better-wp-security' ) );
693
+ }
694
+
695
+ $to_insert = array();
696
+
697
+ foreach ( $r as $row ) {
698
+ $to_insert[ date( 'Y-m-d', strtotime( $row->timestamp ) ) . ' 00:00:00' ] = $row->c;
699
+ }
700
+
701
+ if ( ! $to_insert ) {
702
+ return 0;
703
+ }
704
+
705
+ return self::bulk_insert( $event, $to_insert );
706
+ }
707
+
708
+ /**
709
+ * Migrate log table data to the events table.
710
+ *
711
+ * @return int[]|WP_Error[]|WP_Error
712
+ */
713
+ public static function migrate() {
714
+
715
+ if ( ITSEC_Modules::get_setting( 'global', 'log_type' ) === 'file' ) {
716
+ return new WP_Error( 'itsec-dashboard-backfill-logs-invalid-log-type', __( 'Cannot backfill logs if DB logs are not available.', 'better-wp-security' ) );
717
+ }
718
+
719
+ $list = array(
720
+ 'local-brute-force' => array(
721
+ 'module' => 'brute_force',
722
+ 'code' => array( 'auto-ban-admin-username', 'invalid-login' ),
723
+ ),
724
+ 'network-brute-force' => array(
725
+ 'module' => 'ipcheck',
726
+ 'code' => array( 'failed-login-by-blocked-ip', 'successful-login-by-blocked-ip' ),
727
+ ),
728
+ 'blacklist-brute_force' => array(
729
+ 'module' => 'lockout',
730
+ 'code' => 'host-triggered-blacklist',
731
+ 'where' => array(
732
+ '`data` LIKE \'":brute_force";\'',
733
+ ),
734
+ ),
735
+ 'blacklist-brute_force_admin_user' => array(
736
+ 'module' => 'lockout',
737
+ 'code' => 'host-triggered-blacklist',
738
+ 'where' => array(
739
+ '`data` LIKE \'":brute_force_admin_user";\'',
740
+ ),
741
+ ),
742
+ 'blacklist-recaptcha' => array(
743
+ 'module' => 'lockout',
744
+ 'code' => 'host-triggered-blacklist',
745
+ 'where' => array(
746
+ '`data` LIKE \'":recaptcha";\'',
747
+ ),
748
+ ),
749
+ 'lockout-host' => array(
750
+ 'module' => 'lockout',
751
+ 'code' => 'host-lockout::',
752
+ 'code_like' => true,
753
+ ),
754
+ 'lockout-user' => array(
755
+ 'module' => 'lockout',
756
+ 'code' => 'user-lockout::',
757
+ 'code_like' => true,
758
+ ),
759
+ 'lockout-username' => array(
760
+ 'module' => 'lockout',
761
+ 'code' => 'username-lockout::',
762
+ 'code_like' => true,
763
+ ),
764
+ 'vm-update-plugin' => array(
765
+ 'module' => 'version_management',
766
+ 'code' => 'update::plugin,',
767
+ 'code_like' => true,
768
+ ),
769
+ 'vm-update-theme' => array(
770
+ 'module' => 'version_management',
771
+ 'code' => 'update::theme,',
772
+ 'code_like' => true,
773
+ ),
774
+ 'vm-update-core' => array(
775
+ 'module' => 'version_management',
776
+ 'code' => 'update-core',
777
+ ),
778
+ 'fingerprint-login-blocked' => array(
779
+ 'module' => 'fingerprinting',
780
+ 'code' => 'denied_fingerprint_blocked',
781
+ ),
782
+ 'fingerprint-status-approved' => array(
783
+ 'module' => 'fingerprinting',
784
+ 'code' => 'status::approved',
785
+ 'code_like' => true,
786
+ ),
787
+ 'fingerprint-status-auto-approved' => array(
788
+ 'module' => 'fingerprinting',
789
+ 'code' => 'status::auto-approved',
790
+ 'code_like' => true,
791
+ ),
792
+ 'fingerprint-status-denied' => array(
793
+ 'module' => 'fingerprinting',
794
+ 'code' => 'status::denied',
795
+ 'code_like' => true,
796
+ ),
797
+ 'fingerprint-session-destroyed' => array(
798
+ 'module' => 'fingerprinting',
799
+ 'code' => 'session_destroyed',
800
+ ),
801
+ 'fingerprint-session-switched-unknown' => array(
802
+ 'module' => 'fingerprinting',
803
+ 'code' => 'session_switched_unknown',
804
+ ),
805
+ 'fingerprint-session-switched-known' => array(
806
+ 'module' => 'fingerprinting',
807
+ 'code' => 'session_switched_known',
808
+ ),
809
+ );
810
+
811
+ $results = array();
812
+
813
+ foreach ( $list as $event => $filters ) {
814
+ $results[ $event ] = self::backfill_events_from_logs( $event, $filters );
815
+ }
816
+
817
+ return $results;
818
+ }
819
+
820
+ /**
821
+ * Export dashboard cards.
822
+ *
823
+ * @param int $dashboard_id
824
+ * @param array $args
825
+ *
826
+ * @return array
827
+ */
828
+ public static function export_cards( $dashboard_id, $args = array() ) {
829
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
830
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
831
+ }
832
+
833
+ $args = wp_parse_args( $args, array(
834
+ 'include' => array( 'size', 'position' )
835
+ ) );
836
+
837
+ $include = wp_parse_slug_list( $args['include'] );
838
+
839
+ $exported = array();
840
+
841
+ foreach ( self::get_dashboard_cards( $dashboard_id ) as $card ) {
842
+ $export = array(
843
+ 'type' => get_post_meta( $card->ID, ITSEC_Dashboard::META_CARD, true ),
844
+ );
845
+
846
+ if ( in_array( 'size', $include, true ) ) {
847
+ $export['size'] = get_post_meta( $card->ID, ITSEC_Dashboard::META_CARD_SIZE, true );
848
+ }
849
+
850
+ if ( in_array( 'position', $include, true ) ) {
851
+ $export['position'] = get_post_meta( $card->ID, ITSEC_Dashboard::META_CARD_POSITION, true );
852
+ }
853
+
854
+ if ( in_array( 'settings', $include, true ) ) {
855
+ $export['settings'] = get_post_meta( $card->ID, ITSEC_Dashboard::META_CARD_SETTINGS, true );
856
+ }
857
+
858
+ $exported[] = $export;
859
+ }
860
+
861
+ return $exported;
862
+ }
863
+
864
+ /**
865
+ * Import cards into a dashboard.
866
+ *
867
+ * @param int $dashboard_id
868
+ * @param array $cards
869
+ * @param array $args
870
+ */
871
+ public static function import_cards( $dashboard_id, $cards, $args = array() ) {
872
+ if ( ! class_exists( 'ITSEC_Dashboard' ) ) {
873
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard.php' );
874
+ }
875
+
876
+ $args = wp_parse_args( $args, array(
877
+ 'clear' => true,
878
+ 'skip_unknown' => false,
879
+ ) );
880
+
881
+ if ( $args['clear'] ) {
882
+ foreach ( self::get_dashboard_cards( $dashboard_id ) as $post ) {
883
+ wp_delete_post( $post->ID );
884
+ }
885
+ }
886
+
887
+ $author_id = get_post( $dashboard_id )->post_author;
888
+
889
+ foreach ( $cards as $card ) {
890
+ if ( $args['skip_unknown'] && ! self::get_card( $card['type'] ) ) {
891
+ continue;
892
+ }
893
+
894
+ $post = array(
895
+ 'post_type' => ITSEC_Dashboard::CPT_CARD,
896
+ 'post_parent' => $dashboard_id,
897
+ 'post_status' => 'publish',
898
+ 'post_author' => $author_id,
899
+ 'meta_input' => array(
900
+ ITSEC_Dashboard::META_CARD => $card['type'],
901
+ ITSEC_Dashboard::META_CARD_SIZE => isset( $card['size'] ) ? $card['size'] : array(),
902
+ ITSEC_Dashboard::META_CARD_POSITION => isset( $card['position'] ) ? $card['position'] : array(),
903
+ ITSEC_Dashboard::META_CARD_SETTINGS => isset( $card['settings'] ) ? $card['settings'] : array(),
904
+ ),
905
+ );
906
+
907
+ wp_insert_post( $post );
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Flushes the internal query cache.
913
+ */
914
+ public static function flush_cache() {
915
+ self::$_query_cache = [];
916
+ }
917
+ }
core/modules/dashboard/class-itsec-dashboard.php ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use iThemesSecurity\User_Groups;
4
+
5
+ /**
6
+ * Class ITSEC_Dashboard
7
+ */
8
+ class ITSEC_Dashboard implements \iThemesSecurity\Contracts\Runnable {
9
+
10
+ const CPT_DASHBOARD = 'itsec-dashboard';
11
+ const META_SHARE_USER = '_itsec_dashboard_share_user';
12
+ const META_SHARE_ROLE = '_itsec_dashboard_share_role';
13
+
14
+ const CPT_CARD = 'itsec-dash-card';
15
+ const META_CARD = '_itsec_dashboard_card';
16
+ const META_CARD_SETTINGS = '_itsec_dashboard_card_settings';
17
+ const META_CARD_POSITION = '_itsec_dashboard_card_position';
18
+ const META_CARD_SIZE = '_itsec_dashboard_card_size';
19
+
20
+ const META_PRIMARY = '_itsec_primary_dashboard';
21
+
22
+ /** @var User_Groups\Matcher */
23
+ private $matcher;
24
+
25
+ /**
26
+ * ITSEC_Dashboard constructor.
27
+ *
28
+ * @param User_Groups\Matcher $matcher
29
+ */
30
+ public function __construct( User_Groups\Matcher $matcher ) { $this->matcher = $matcher; }
31
+
32
+ /**
33
+ * Run the dashboard module.
34
+ */
35
+ public function run() {
36
+ add_action( 'init', array( $this, 'register_data_storage' ) );
37
+ add_action( 'itsec_scheduled_dashboard-consolidate-events', array( $this, 'run_consolidate_events' ) );
38
+ add_action( 'after_delete_post', array( $this, 'after_delete_post' ), 10, 2 );
39
+ add_filter( 'map_meta_cap', array( $this, 'map_meta_cap' ), 10, 4 );
40
+ add_action( 'itsec_log_add', array( $this, 'log_add' ) );
41
+
42
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-rest.php' );
43
+ $rest = new ITSEC_Dashboard_REST();
44
+ $rest->run();
45
+ }
46
+
47
+ /**
48
+ * Register the Custom Post Types and Metadata.
49
+ */
50
+ public function register_data_storage() {
51
+ register_post_type( self::CPT_DASHBOARD, array(
52
+ 'public' => false,
53
+ 'hierarchical' => true,
54
+ 'supports' => array( 'title' ),
55
+ ) );
56
+
57
+ register_post_meta( self::CPT_DASHBOARD, self::META_SHARE_USER, array(
58
+ 'type' => 'integer',
59
+ 'single' => false,
60
+ 'sanitize_callback' => 'absint'
61
+ ) );
62
+
63
+ register_post_meta( self::CPT_DASHBOARD, self::META_SHARE_ROLE, array(
64
+ 'type' => 'string',
65
+ 'single' => false,
66
+ 'sanitize_callback' => array( __CLASS__, '_sanitize_role' )
67
+ ) );
68
+
69
+ register_post_type( self::CPT_CARD, array(
70
+ 'public' => false,
71
+ 'supports' => array(),
72
+ ) );
73
+
74
+ register_post_meta( self::CPT_CARD, self::META_CARD, array(
75
+ 'type' => 'string',
76
+ 'single' => true,
77
+ 'sanitize_callback' => array( __CLASS__, '_sanitize_card' ),
78
+ ) );
79
+
80
+ register_post_meta( self::CPT_CARD, self::META_CARD_SETTINGS, array(
81
+ 'type' => 'object',
82
+ 'single' => true,
83
+ 'sanitize_callback' => array( __CLASS__, '_sanitize_settings' ),
84
+ ) );
85
+
86
+ register_post_meta( self::CPT_CARD, self::META_CARD_POSITION, array(
87
+ 'type' => 'object',
88
+ 'single' => true,
89
+ 'sanitize_callback' => array( __CLASS__, '_sanitize_position' ),
90
+ ) );
91
+
92
+ register_post_meta( self::CPT_CARD, self::META_CARD_SIZE, array(
93
+ 'type' => 'object',
94
+ 'single' => true,
95
+ 'sanitize_callback' => array( __CLASS__, '_sanitize_size' ),
96
+ ) );
97
+
98
+ register_meta( 'user', self::META_PRIMARY, array(
99
+ 'type' => 'integer',
100
+ 'single' => true,
101
+ 'sanitize_callback' => 'absint',
102
+ 'auth_callback' => array( __CLASS__, '_auth_primary' ),
103
+ 'show_in_rest' => array(
104
+ 'schema' => array(
105
+ 'type' => 'integer',
106
+ 'context' => array( 'edit' ),
107
+ )
108
+ ),
109
+ ) );
110
+ }
111
+
112
+ /**
113
+ * Delete all cards when a dashboard is deleted.
114
+ *
115
+ * @param int $post_id
116
+ * @param WP_Post $post
117
+ */
118
+ public function after_delete_post( $post_id, $post ) {
119
+ if ( $post->post_type !== self::CPT_DASHBOARD ) {
120
+ return;
121
+ }
122
+
123
+ delete_metadata( 'user', 0, self::META_PRIMARY, $post_id, true );
124
+
125
+ foreach ( ITSEC_Dashboard_Util::get_dashboard_cards( $post_id ) as $post ) {
126
+ wp_delete_post( $post->ID );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Sanitize the "role" metadata.
132
+ *
133
+ * @param string $role
134
+ *
135
+ * @return string
136
+ */
137
+ public static function _sanitize_role( $role ) {
138
+ return array_key_exists( $role, wp_roles()->roles ) ? $role : '';
139
+ }
140
+
141
+ /**
142
+ * Sanitize the "card" metadata.
143
+ *
144
+ * @param string $card
145
+ *
146
+ * @return string
147
+ */
148
+ public static function _sanitize_card( $card ) {
149
+ return (string) preg_replace( '/[^\w_-]/', '', $card );
150
+ }
151
+
152
+ /**
153
+ * Sanitize the "settings" metadata.
154
+ *
155
+ * @param mixed $settings
156
+ *
157
+ * @return array
158
+ */
159
+ public static function _sanitize_settings( $settings ) {
160
+ return is_array( $settings ) ? $settings : array();
161
+ }
162
+
163
+ /**
164
+ * Sanitize the "position" metadata.
165
+ *
166
+ * @param mixed $position
167
+ *
168
+ * @return array
169
+ */
170
+ public static function _sanitize_position( $position ) {
171
+
172
+ $sanitized = array();
173
+
174
+ if ( ! is_array( $position ) ) {
175
+ return $sanitized;
176
+ }
177
+
178
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-util.php' );
179
+
180
+ foreach ( $position as $breakpoint => $entry ) {
181
+ if ( ! in_array( $breakpoint, ITSEC_Dashboard_Util::$breakpoints, true ) ) {
182
+ continue;
183
+ }
184
+
185
+ $sanitized[ $breakpoint ] = self::_sanitize_position_entry( $entry );
186
+ }
187
+
188
+ return $sanitized;
189
+ }
190
+
191
+ /**
192
+ * Sanitize a single position value for a breakpoint.
193
+ *
194
+ * @param array|mixed $position
195
+ *
196
+ * @return array
197
+ */
198
+ private static function _sanitize_position_entry( $position ) {
199
+ if ( ! is_array( $position ) || ! isset( $position['x'], $position['y'] ) ) {
200
+ return array();
201
+ }
202
+
203
+ return array(
204
+ 'x' => absint( $position['x'] ),
205
+ 'y' => absint( $position['y'] ),
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Sanitize the "size" metadata.
211
+ *
212
+ * @param mixed $size
213
+ *
214
+ * @return array
215
+ */
216
+ public static function _sanitize_size( $size ) {
217
+
218
+ $sanitized = array();
219
+
220
+ if ( ! is_array( $size ) ) {
221
+ return $sanitized;
222
+ }
223
+
224
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-util.php' );
225
+
226
+ foreach ( $size as $breakpoint => $entry ) {
227
+ if ( ! in_array( $breakpoint, ITSEC_Dashboard_Util::$breakpoints, true ) ) {
228
+ continue;
229
+ }
230
+
231
+ $sanitized[ $breakpoint ] = self::_sanitize_size_entry( $entry );
232
+ }
233
+
234
+ return $sanitized;
235
+ }
236
+
237
+ /**
238
+ * Sanitize a single size value for a breakpoint.
239
+ *
240
+ * @param array|mixed $size
241
+ *
242
+ * @return array
243
+ */
244
+ private static function _sanitize_size_entry( $size ) {
245
+ if ( ! is_array( $size ) || ! isset( $size['w'], $size['h'] ) ) {
246
+ return array();
247
+ }
248
+
249
+ return array(
250
+ 'w' => absint( $size['w'] ),
251
+ 'h' => absint( $size['h'] ),
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Authorization callback to check if a user can set the primary dashboard meta key.
257
+ *
258
+ * @param bool $allowed
259
+ * @param string $meta_key
260
+ * @param int $user_id
261
+ *
262
+ * @return bool
263
+ */
264
+ public static function _auth_primary( $allowed, $meta_key, $user_id ) {
265
+ return current_user_can( 'edit_user', $user_id );
266
+ }
267
+
268
+ /**
269
+ * Consolidate events on a daily schedule.
270
+ */
271
+ public function run_consolidate_events() {
272
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-util.php' );
273
+ ITSEC_Dashboard_Util::consolidate_events();
274
+ }
275
+
276
+ /**
277
+ * Handle custom capabilities for the dashboard.
278
+ *
279
+ * @param array $caps
280
+ * @param string $cap
281
+ * @param int $user_id
282
+ * @param array $args
283
+ *
284
+ * @return array
285
+ */
286
+ public function map_meta_cap( $caps, $cap, $user_id, $args ) {
287
+
288
+ $user_id = (int) $user_id;
289
+
290
+ switch ( $cap ) {
291
+ case 'itsec_dashboard_access':
292
+ case 'itsec_dashboard_menu':
293
+ if ( user_can( $user_id, 'itsec_create_dashboards' ) ) {
294
+ return array();
295
+ }
296
+
297
+ require_once( dirname( __FILE__ ) . '/class-itsec-dashboard-util.php' );
298
+
299
+ if ( ITSEC_Dashboard_Util::get_shared_dashboards( $user_id, 'ids' ) ) {
300
+ return array();
301
+ }
302
+
303
+ return array( 'do_not_allow' );
304
+ case 'itsec_view_dashboard':
305
+ if ( empty( $args[0] ) || ! ( $post = get_post( $args[0] ) ) || self::CPT_DASHBOARD !== $post->post_type ) {
306
+ return array( 'do_not_allow' );
307
+ }
308
+
309
+ if ( $user_id === (int) $post->post_author && user_can( $user_id, 'itsec_create_dashboards' ) ) {
310
+ return array();
311
+ }
312
+
313
+ $uids = get_post_meta( $post->ID, self::META_SHARE_USER );
314
+
315
+ if ( in_array( $user_id, $uids, false ) ) {
316
+ return array();
317
+ }
318
+
319
+ $user = get_userdata( $user_id );
320
+
321
+ foreach ( get_post_meta( $post->ID, self::META_SHARE_ROLE ) as $role ) {
322
+ if ( in_array( $role, $user->roles, true ) ) {
323
+ return array();
324
+ }
325
+ }
326
+
327
+ return array( 'do_not_allow' );
328
+ case 'itsec_edit_dashboard':
329
+ if ( empty( $args[0] ) || ! ( $post = get_post( $args[0] ) ) || self::CPT_DASHBOARD !== $post->post_type ) {
330
+ return array( 'do_not_allow' );
331
+ }
332
+
333
+ if ( $user_id === (int) $post->post_author && user_can( $user_id, 'itsec_create_dashboards' ) ) {
334
+ return array();
335
+ }
336
+
337
+ return array( 'do_not_allow' );
338
+ case 'itsec_create_dashboards':
339
+ if ( ! $user = get_userdata( $user_id ) ) {
340
+ return array( 'do_not_allow' );
341
+ }
342
+
343
+ if ( user_can( $user_id, ITSEC_Core::get_required_cap() ) ) {
344
+ return array();
345
+ }
346
+
347
+ $group = ITSEC_Modules::get_setting( 'dashboard', 'group' );
348
+
349
+ if ( ! $this->matcher->matches( User_Groups\Match_Target::for_user( $user ), $group ) ) {
350
+ return array( 'do_not_allow' );
351
+ }
352
+
353
+ return array();
354
+ }
355
+
356
+ return $caps;
357
+ }
358
+
359
+ /**
360
+ * Create an event for certain log items.
361
+ *
362
+ * @param array $data
363
+ */
364
+ public function log_add( $data ) {
365
+ list( $code ) = array_pad( explode( '::', $data['code'] ), 2, '' );
366
+
367
+ switch ( $data['module'] ) {
368
+ case 'brute_force':
369
+ switch ( $code ) {
370
+ case 'auto-ban-admin-username':
371
+ case 'invalid-login':
372
+ ITSEC_Dashboard_Util::record_event( 'local-brute-force' );
373
+ break;
374
+ }
375
+ break;
376
+ case 'ipcheck':
377
+ switch ( $code ) {
378
+ case 'failed-login-by-blocked-ip':
379
+ case 'successful-login-by-blocked-ip':
380
+ ITSEC_Dashboard_Util::record_event( 'network-brute-force' );
381
+ break;
382
+ }
383
+ break;
384
+ case 'lockout':
385
+ switch ( $code ) {
386
+ case 'host-triggered-blacklist':
387
+ // blacklist-four_oh_four, blacklist-brute_force, blacklist-brute_force_admin_user, blacklist-recaptcha
388
+ ITSEC_Dashboard_Util::record_event( 'blacklist-' . $data['data']['module'] );
389
+ break;
390
+ case 'host-lockout':
391
+ ITSEC_Dashboard_Util::record_event( 'lockout-host' );
392
+ break;
393
+ case 'user-lockout':
394
+ ITSEC_Dashboard_Util::record_event( 'lockout-user' );
395
+ break;
396
+ case 'username-lockout':
397
+ ITSEC_Dashboard_Util::record_event( 'lockout-username' );
398
+ break;
399
+ }
400
+ break;
401
+ }
402
+ }
403
+ }
core/modules/dashboard/container.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use iThemesSecurity\User_Groups\Matcher;
4
+ use Pimple\Container;
5
+
6
+ return static function ( Container $c ) {
7
+ $c['ITSEC_Dashboard'] = static function ( Container $c ) {
8
+ return new ITSEC_Dashboard( $c[ Matcher::class ] );
9
+ };
10
+
11
+ $c->extend( 'dashboard.cards', function ( $cards ) use ( $c ) {
12
+ $cards[] = new ITSEC_Dashboard_Card_Active_Lockouts();
13
+ $cards[] = new ITSEC_Dashboard_Card_Line_Graph( 'brute-force', __( 'Brute Force Attacks', 'better-wp-security' ), [
14
+ [
15
+ 'events' => [ 'local-brute-force', 'network-brute-force' ],
16
+ 'label' => __( 'Attacks', 'better-wp-security' ),
17
+ ],
18
+ ] );
19
+ $cards[] = new ITSEC_Dashboard_Card_Pie_Chart( 'lockout', __( 'Lockouts', 'better-wp-security' ), [
20
+ [
21
+ 'events' => 'lockout-host',
22
+ 'label' => __( 'Hosts', 'better-wp-security' ),
23
+ ],
24
+ [
25
+ 'events' => 'lockout-user',
26
+ 'label' => __( 'Users', 'better-wp-security' ),
27
+ ],
28
+ [
29
+ 'events' => 'lockout-username',
30
+ 'label' => __( 'Usernames', 'better-wp-security' ),
31
+ ],
32
+ ], [
33
+ 'circle_label' => _x( 'Total', 'Total Lockouts', 'better-wp-security' ),
34
+ 'circle_callback' => function () {
35
+ /** @var ITSEC_Lockout $itsec_lockout */
36
+ global $itsec_lockout;
37
+
38
+ return $itsec_lockout->get_lockouts( 'all', array( 'return' => 'count', 'current' => false ) );
39
+ },
40
+ ] );
41
+
42
+ if ( $c['ban-hosts.repositories'] ) {
43
+ $cards[] = new ITSEC_Dashboard_Card_Banned_Users();
44
+ }
45
+
46
+ return $cards;
47
+ } );
48
+ };
core/modules/dashboard/deactivate.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <?php
2
+
3
+ ITSEC_Core::get_scheduler()->unschedule( 'dashboard-consolidate-events' );
core/modules/dashboard/entries/api.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { capitalize } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Slot, Fill } from '@wordpress/components';
10
+
11
+ import './dashboard/store';
12
+
13
+ export function AdminBarFill( { type = 'secondary', ...props } ) {
14
+ return <Fill name={ `AdminBar${ capitalize( type ) }` } { ...props } />;
15
+ }
16
+
17
+ export function AdminBarSlot( { type = 'secondary', ...props } ) {
18
+ return <Slot name={ `AdminBar${ capitalize( type ) }` } { ...props } />;
19
+ }
20
+
21
+ export function BelowToolbarFill( props ) {
22
+ return <Fill name="BelowToolbar" { ...props } />;
23
+ }
24
+
25
+ export function BelowToolbarSlot( props ) {
26
+ return <Slot name="BelowToolbar" { ...props } />;
27
+ }
28
+
29
+ export function EditCardsFill( props ) {
30
+ return <Fill name="EditCards" { ...props } />;
31
+ }
32
+
33
+ export function EditCardsSlot( props ) {
34
+ return <Slot name="EditCards" { ...props } />;
35
+ }
core/modules/dashboard/entries/dashboard.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { setLocaleData } from '@wordpress/i18n';
5
+ import { render } from '@wordpress/element';
6
+ import domReady from '@wordpress/dom-ready';
7
+
8
+ setLocaleData( { '': {} }, 'better-wp-security' );
9
+
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import App from './dashboard/app.js';
14
+
15
+ domReady( () => {
16
+ const el = document.getElementById( 'itsec-dashboard-root' );
17
+
18
+ if ( el ) {
19
+ const canManage = el.dataset.canManage === '1';
20
+ const installType = el.dataset.installType;
21
+
22
+ render( <App context={ { canManage, installType } } />, el );
23
+ }
24
+ } );
25
+
26
+ export * from './dashboard/utils';
27
+ export { useRegisterCards } from './dashboard/cards';
28
+ export Card from './dashboard/components/card';
29
+ export CardHeader, {
30
+ Date as CardHeaderDate,
31
+ Status as CardHeaderStatus,
32
+ Title as CardHeaderTitle,
33
+ } from './dashboard/components/card/header';
34
+ export CardFooter, {
35
+ FooterSchemaActions as CardFooterSchemaActions,
36
+ } from './dashboard/components/card/footer';
37
+ export PromoCard from './dashboard/components/edit-cards/promo-card';
38
+ export MasterDetail, {
39
+ Back as MasterDetailBack,
40
+ } from './dashboard/components/master-detail';
core/modules/dashboard/entries/dashboard/app.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { NoticeList, SlotFillProvider, Popover } from '@wordpress/components';
5
+ import { pure, usePrevious } from '@wordpress/compose';
6
+ import { useSelect, useDispatch } from '@wordpress/data';
7
+ import { useEffect } from '@wordpress/element';
8
+ import { PluginArea } from '@wordpress/plugins';
9
+ import '@wordpress/notices';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { BelowToolbarSlot } from '@ithemes/security.dashboard.api';
15
+ import '@ithemes/security-data';
16
+ import { useEventListener } from '@ithemes/security-hocs';
17
+ import { useRegisterCards } from './cards';
18
+ import AdminBar from './components/admin-bar';
19
+ import CardGrid from './components/card-grid';
20
+ import CreateDashboard from './components/create-dashboard';
21
+ import Toolbar from './components/toolbar';
22
+ import Help from './components/help';
23
+ import { ConfigContext } from './utils';
24
+ import './style.scss';
25
+
26
+ const Page = pure( ( { page, dashboardId } ) => {
27
+ useRegisterCards();
28
+
29
+ switch ( page ) {
30
+ case 'view-dashboard':
31
+ return <CardGrid dashboardId={ dashboardId } />;
32
+ case 'create-dashboard':
33
+ return <CreateDashboard />;
34
+ case 'help':
35
+ return <Help />;
36
+ default:
37
+ return null;
38
+ }
39
+ } );
40
+
41
+ export default function App( { context } ) {
42
+ const {
43
+ page,
44
+ primaryDashboard,
45
+ dashboardId,
46
+ isUsingTouch,
47
+ notices,
48
+ } = useSelect(
49
+ ( select ) => ( {
50
+ page: select( 'ithemes-security/dashboard' ).getCurrentPage(),
51
+ primaryDashboard: select(
52
+ 'ithemes-security/dashboard'
53
+ ).getPrimaryDashboard(),
54
+ dashboardId: select(
55
+ 'ithemes-security/dashboard'
56
+ ).getViewingDashboardId(),
57
+ isUsingTouch: select( 'ithemes-security/dashboard' ).isUsingTouch(),
58
+ notices: select( 'core/notices' ).getNotices( 'ithemes-security' ),
59
+ } ),
60
+ []
61
+ );
62
+ const { usingTouch, viewDashboard, viewCreateDashboard } = useDispatch(
63
+ 'ithemes-security/dashboard'
64
+ );
65
+ const { removeNotice } = useDispatch( 'core/notices' );
66
+ useEventListener( 'touchstart', () => isUsingTouch || usingTouch() );
67
+
68
+ const prevPrimaryDashboard = usePrevious( primaryDashboard );
69
+ useEffect( () => {
70
+ if ( prevPrimaryDashboard !== undefined ) {
71
+ return;
72
+ }
73
+
74
+ if ( primaryDashboard ) {
75
+ viewDashboard( primaryDashboard );
76
+ } else {
77
+ viewCreateDashboard();
78
+ }
79
+ }, [ primaryDashboard ] );
80
+
81
+ if ( primaryDashboard === undefined ) {
82
+ return null;
83
+ }
84
+
85
+ return (
86
+ <SlotFillProvider>
87
+ <ConfigContext.Provider value={ context }>
88
+ <div className={ `itsec-dashboard itsec-app-page--${ page }` }>
89
+ <Popover.Slot />
90
+ <NoticeList
91
+ notices={ notices }
92
+ onRemove={ ( noticeId ) =>
93
+ removeNotice( noticeId, 'ithemes-security' )
94
+ }
95
+ />
96
+ <Toolbar dashboardId={ dashboardId } />
97
+ <BelowToolbarSlot fillProps={ { page, dashboardId } } />
98
+ <AdminBar dashboardId={ dashboardId } />
99
+ <Page page={ page } dashboardId={ dashboardId } />
100
+ </div>
101
+ <PluginArea />
102
+ </ConfigContext.Provider>
103
+ </SlotFillProvider>
104
+ );
105
+ }
core/modules/dashboard/entries/dashboard/cards/active-lockouts/Detail.js ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __, sprintf } from '@wordpress/i18n';
5
+ import { Component, Fragment } from '@wordpress/element';
6
+ import { dateI18n } from '@wordpress/date';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import lockoutController from './lockout-controller';
12
+
13
+ class Detail extends Component {
14
+ static defaultProps = {
15
+ master: {},
16
+ isVisible: false,
17
+ };
18
+
19
+ state = {
20
+ details: null,
21
+ viewLog: 0,
22
+ };
23
+
24
+ componentDidMount() {
25
+ if ( this.props.isVisible ) {
26
+ this.fetchDetails( this.props.master.id );
27
+ }
28
+ }
29
+
30
+ componentDidUpdate( prevProps ) {
31
+ const fetch =
32
+ ( this.props.isVisible && ! prevProps.isVisible ) ||
33
+ this.props.master.id !== prevProps.master.id;
34
+
35
+ if ( fetch ) {
36
+ this.fetchDetails( this.props.master.id );
37
+ }
38
+ }
39
+
40
+ shouldComponentUpdate( nextProps, nextState ) {
41
+ if ( this.props.master.id !== nextProps.master.id ) {
42
+ return true;
43
+ }
44
+
45
+ if ( this.props.isVisible !== nextProps.isVisible ) {
46
+ return true;
47
+ }
48
+
49
+ if ( ! this.state.details && nextState.details ) {
50
+ return true;
51
+ }
52
+
53
+ if ( this.state.viewLog !== nextState.viewLog ) {
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ fetchDetails = ( id ) => {
61
+ if ( ! this.props.master.links.item ) {
62
+ return;
63
+ }
64
+
65
+ const url = this.props.master.links.item[ 0 ].href.replace(
66
+ '{lockout_id}',
67
+ id
68
+ );
69
+
70
+ lockoutController.getDetails( url ).then( ( details ) => {
71
+ if ( this.unmounted || this.props.master.id !== id ) {
72
+ return;
73
+ }
74
+
75
+ this.setState( { details } );
76
+ } );
77
+ };
78
+
79
+ componentWillUnmount() {
80
+ this.unmounted = true;
81
+ }
82
+
83
+ render() {
84
+ const { master } = this.props;
85
+ const { details } = this.state;
86
+
87
+ return (
88
+ <div className="itsec-card-active-lockouts__detail-container">
89
+ <time
90
+ className="itsec-card-active-lockouts__start-time"
91
+ dateTime={ master.start_gmt }
92
+ >
93
+ { sprintf(
94
+ /* translators: 1. Relative time from human_time_diff(). */
95
+ __( '%s ago', 'better-wp-security' ),
96
+ master.start_gmt_relative
97
+ ) }
98
+ </time>
99
+ <h3 className="itsec-card-active-lockouts__label">
100
+ { master.label }
101
+ </h3>
102
+ <p className="itsec-card-active-lockouts__description">
103
+ { master.description }
104
+ </p>
105
+
106
+ { details && details.history.length > 0 && (
107
+ <Fragment>
108
+ <hr />
109
+
110
+ <div className="itsec-card-active-lockouts__history">
111
+ <h4 className="itsec-card-active-lockouts__history-title">
112
+ { __( 'History', 'better-wp-security' ) }
113
+ </h4>
114
+ <ul>
115
+ { details.history.map( this.renderHistory ) }
116
+ </ul>
117
+ </div>
118
+ </Fragment>
119
+ ) }
120
+ </div>
121
+ );
122
+ }
123
+
124
+ renderHistory = ( history ) => {
125
+ if ( ! history.label ) {
126
+ return;
127
+ }
128
+
129
+ const time = (
130
+ <time
131
+ dateTime={ history.time }
132
+ title={ dateI18n( 'M d, Y g:s A', history.time ) }
133
+ >
134
+ { sprintf(
135
+ /* translators: 1. Relative time from human_time_diff(). */
136
+ __( '%s ago', 'better-wp-security' ),
137
+ history.time_relative
138
+ ) }
139
+ </time>
140
+ );
141
+
142
+ return (
143
+ <li key={ history.id }>
144
+ <code>{ history.label }</code>
145
+ { ' – ' }
146
+ { time }
147
+ </li>
148
+ );
149
+ };
150
+ }
151
+
152
+ export default Detail;
core/modules/dashboard/entries/dashboard/cards/active-lockouts/index.js ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { isEmpty } from 'lodash';
5
+ import memize from 'memize';
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __, sprintf } from '@wordpress/i18n';
10
+ import { compose, withState, pure } from '@wordpress/compose';
11
+ import { Fragment } from '@wordpress/element';
12
+ import { withSelect, withDispatch, dispatch } from '@wordpress/data';
13
+ import { dateI18n } from '@wordpress/date';
14
+ import { Button, Spinner, Dashicon } from '@wordpress/components';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import Header, { Title } from '../../components/card/header';
20
+ import Footer from '../../components/card/footer';
21
+ import MasterDetail, { Back } from '../../components/master-detail';
22
+ import { CardHappy } from '../../components/empty-states';
23
+ import Detail from './Detail';
24
+ import lockoutController from './lockout-controller';
25
+ import { withDebounceHandler } from '@ithemes/security-hocs';
26
+ import './style.scss';
27
+
28
+ function MasterRender( { master } ) {
29
+ return (
30
+ <Fragment>
31
+ <time
32
+ className="itsec-card-active-lockouts__start-time"
33
+ dateTime={ master.start_gmt }
34
+ title={ dateI18n( 'M d, Y g:s A', master.start_gmt ) }
35
+ >
36
+ { sprintf(
37
+ /* translators: 1. Relative time from human_time_diff(). */
38
+ __( '%s ago', 'better-wp-security' ),
39
+ master.start_gmt_relative
40
+ ) }
41
+ </time>
42
+ <h3 className="itsec-card-active-lockouts__label">
43
+ { master.label }
44
+ </h3>
45
+ <p className="itsec-card-active-lockouts__description">
46
+ { master.description }
47
+ </p>
48
+ </Fragment>
49
+ );
50
+ }
51
+
52
+ const withLinks = memize( function ( lockouts, links ) {
53
+ return lockouts.map( ( lockout ) => ( {
54
+ ...lockout,
55
+ links,
56
+ } ) );
57
+ } );
58
+
59
+ function ActiveLockouts( {
60
+ card,
61
+ config,
62
+ isQuerying,
63
+ query,
64
+ selectedId,
65
+ releasingIds,
66
+ setState,
67
+ } ) {
68
+ const select = ( id ) => {
69
+ return setState( { selectedId: id } );
70
+ };
71
+
72
+ const onRelease = async ( e ) => {
73
+ e.preventDefault();
74
+
75
+ setState( { releasingIds: [ ...releasingIds, selectedId ] } );
76
+
77
+ try {
78
+ await lockoutController.release(
79
+ card._links[
80
+ 'ithemes-security:release-lockout'
81
+ ][ 0 ].href.replace( '{lockout_id}', selectedId )
82
+ );
83
+ } catch ( error ) {
84
+ // eslint-disable-next-line no-console
85
+ console.warn( error );
86
+ }
87
+
88
+ await dispatch( 'ithemes-security/dashboard' ).refreshDashboardCard(
89
+ card.id
90
+ );
91
+ setState( {
92
+ selectedId: 0,
93
+ releasingIds: releasingIds.filter( ( id ) => id !== selectedId ),
94
+ } );
95
+ };
96
+
97
+ const isSmall = true;
98
+
99
+ return (
100
+ <div className="itsec-card--type-active-lockouts">
101
+ <Header>
102
+ <Back
103
+ isSmall={ isSmall }
104
+ select={ select }
105
+ selectedId={ selectedId }
106
+ />
107
+ <Title card={ card } config={ config } />
108
+ </Header>
109
+ { selectedId === 0 && (
110
+ <div className="itsec-card-active-lockouts__search-container">
111
+ <input
112
+ type="search"
113
+ onChange={ ( e ) =>
114
+ query( { search: e.target.value } )
115
+ }
116
+ placeholder={ __( 'Search Lockouts', 'better-wp-security' ) }
117
+ />
118
+ { isQuerying ? <Spinner /> : <Dashicon icon="search" /> }
119
+ </div>
120
+ ) }
121
+ { isEmpty( card.data.lockouts ) ? (
122
+ <CardHappy
123
+ title={ __( 'All Clear!', 'better-wp-security' ) }
124
+ text={ __(
125
+ 'No users are currently locked out of your site.',
126
+ 'better-wp-security'
127
+ ) }
128
+ />
129
+ ) : (
130
+ <MasterDetail
131
+ masters={ withLinks( card.data.lockouts, card._links ) }
132
+ detailRender={ Detail }
133
+ masterRender={ MasterRender }
134
+ mode="list"
135
+ selectedId={ selectedId }
136
+ select={ select }
137
+ isSmall={ isSmall }
138
+ />
139
+ ) }
140
+ { selectedId > 0 &&
141
+ card._links[ 'ithemes-security:release-lockout' ] && (
142
+ <Footer>
143
+ <span className="itsec-card-footer__action">
144
+ <Button
145
+ isPrimary
146
+ isSmall
147
+ aria-disabled={ releasingIds.includes(
148
+ selectedId
149
+ ) }
150
+ isBusy={ releasingIds.includes( selectedId ) }
151
+ onClick={ onRelease }
152
+ >
153
+ { __( 'Release Lockout', 'better-wp-security' ) }
154
+ </Button>
155
+ </span>
156
+ </Footer>
157
+ ) }
158
+ </div>
159
+ );
160
+ }
161
+
162
+ export const slug = 'active-lockouts';
163
+ export const settings = {
164
+ render: compose( [
165
+ withState( { selectedId: 0, releasingIds: [] } ),
166
+ withSelect( ( select, ownProps ) => ( {
167
+ isQuerying: select(
168
+ 'ithemes-security/dashboard'
169
+ ).isQueryingDashboardCard( ownProps.card.id ),
170
+ } ) ),
171
+ withDispatch( ( d, ownProps ) => ( {
172
+ query( queryArgs ) {
173
+ return d( 'ithemes-security/dashboard' ).queryDashboardCard(
174
+ ownProps.card.id,
175
+ queryArgs
176
+ );
177
+ },
178
+ } ) ),
179
+ withDebounceHandler( 'query', 500, { leading: true } ),
180
+ pure,
181
+ ] )( ActiveLockouts ),
182
+ elementQueries: [
183
+ {
184
+ type: 'width',
185
+ dir: 'max',
186
+ px: 500,
187
+ },
188
+ ],
189
+ };
core/modules/dashboard/entries/dashboard/cards/active-lockouts/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/active-lockouts/lockout-controller.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { dispatch } from '@wordpress/data';
5
+ import { __, sprintf } from '@wordpress/i18n';
6
+ import apiFetch from '@wordpress/api-fetch';
7
+
8
+ class LockoutController {
9
+ #details = {};
10
+ #fetching = {};
11
+ #releasing = {};
12
+
13
+ getDetails( url ) {
14
+ if ( this.#details[ url ] ) {
15
+ return Promise.resolve( this.#details[ url ] );
16
+ }
17
+
18
+ if ( ! this.#fetching[ url ] ) {
19
+ this.#fetching[ url ] = apiFetch( { url } ).then( ( response ) => {
20
+ this.#details[ url ] = response.detail;
21
+ delete this.#fetching[ url ];
22
+
23
+ return response.detail;
24
+ } );
25
+ }
26
+
27
+ return this.#fetching[ url ];
28
+ }
29
+
30
+ isFetching( url ) {
31
+ return !! this.#fetching[ url ];
32
+ }
33
+
34
+ release( url ) {
35
+ if ( ! this.#releasing[ url ] ) {
36
+ this.#releasing[ url ] = apiFetch( {
37
+ url,
38
+ method: 'DELETE',
39
+ } )
40
+ .then( ( response ) => {
41
+ delete this.#releasing[ url ];
42
+
43
+ const id = `release-lockout-${ url }`;
44
+
45
+ setTimeout(
46
+ () =>
47
+ dispatch( 'core/notices' ).removeNotice(
48
+ id,
49
+ 'ithemes-security'
50
+ ),
51
+ 5000
52
+ );
53
+ dispatch( 'core/notices' ).createNotice(
54
+ 'success',
55
+ __( 'Lockout Released', 'better-wp-security' ),
56
+ { id, context: 'ithemes-security' }
57
+ );
58
+
59
+ return response;
60
+ } )
61
+ .catch( ( e ) => {
62
+ delete this.#releasing[ url ];
63
+
64
+ dispatch( 'core/notices' ).createNotice(
65
+ 'error',
66
+ sprintf(
67
+ /* translators: 1. Error message */
68
+ __( 'Error when releasing lockout: %s', 'better-wp-security' ),
69
+ e.message
70
+ ),
71
+ { context: 'ithemes-security' }
72
+ );
73
+ } );
74
+ }
75
+
76
+ return this.#releasing[ url ];
77
+ }
78
+
79
+ isReleasing( url ) {
80
+ return !! this.#releasing[ url ];
81
+ }
82
+ }
83
+
84
+ const controller = new LockoutController();
85
+
86
+ export default controller;
core/modules/dashboard/entries/dashboard/cards/active-lockouts/style.scss ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .itsec-card--type-active-lockouts {
3
+ display: flex;
4
+ flex-direction: column;
5
+ overflow: hidden;
6
+ height: 100%;
7
+ position: relative;
8
+
9
+
10
+ & .itsec-component-master-detail {
11
+ border-bottom-left-radius: 10px;
12
+ }
13
+
14
+ & .itsec-card-header {
15
+ flex-grow: 0;
16
+ flex-shrink: 0;
17
+ border-bottom: 1px solid $light-gray-500;
18
+ }
19
+
20
+ & .itsec-component-master-detail {
21
+ }
22
+
23
+ & .itsec-component-master-detail__detail-container {
24
+ padding: 0 1em;
25
+ }
26
+
27
+ & .itsec-component-master-detail__master {
28
+ padding: 1em;
29
+ margin: 0;
30
+ border-top: 1px solid $light-gray-500;
31
+
32
+ &:last-child {
33
+ border-bottom: none;
34
+ }
35
+ }
36
+
37
+ & .itsec-component-master-detail__master-list {
38
+ margin: 0;
39
+ }
40
+
41
+ & .itsec-card-active-lockouts__label {
42
+ margin: 0 !important;
43
+ font-size: 1.15em;
44
+ }
45
+
46
+ & .itsec-card-active-lockouts__description {
47
+ margin: 0;
48
+ font-style: oblique;
49
+ line-height: 1.3;
50
+ }
51
+
52
+ & .itsec-card-active-lockouts__start-time {
53
+ color: #6c7781;
54
+ font-size: .8em;
55
+ text-transform: uppercase;
56
+ letter-spacing: -0.2px;
57
+ line-height: 1;
58
+ }
59
+
60
+ & .itsec-card-active-lockouts__detail-container {
61
+ padding: 1em 0;
62
+
63
+ & hr {
64
+ max-width: 5em;
65
+ border-bottom: none;
66
+ border-bottom-color: $light-gray-100;
67
+ margin: 1em auto;
68
+ }
69
+ }
70
+
71
+ & .itsec-card-active-lockouts__history {
72
+ & .itsec-card-active-lockouts__history-title {
73
+ margin: 1em 0;
74
+ }
75
+
76
+ & ul {
77
+ margin: 0 0 0 1em;
78
+ list-style: square;
79
+ }
80
+
81
+ & code {
82
+ background: #eaeaea;
83
+ word-break: break-all;
84
+ display: inline-block;
85
+ }
86
+ }
87
+
88
+ & .itsec-card-active-lockouts__search-container {
89
+ position: relative;
90
+
91
+ & input[type="search"] {
92
+ width: 100%;
93
+ margin: 0;
94
+ padding: 3px 1em;
95
+ border: none;
96
+ box-shadow: none;
97
+ transition: all 400ms ease;
98
+ background: $highlight-blue;
99
+ color: $dark-gray-500;
100
+ border-radius: 0;
101
+
102
+ &:focus {
103
+ padding: 1em;
104
+ }
105
+
106
+ &::placeholder {
107
+ color: $dark-gray-100;
108
+ }
109
+
110
+ &::-webkit-search-cancel-button {
111
+ display: none;
112
+ }
113
+ }
114
+
115
+ & .components-spinner,
116
+ & .dashicon {
117
+ position: absolute;
118
+ right: 1em;
119
+ top: calc(50% - 9px);
120
+ margin: 0;
121
+ }
122
+
123
+ & .dashicon {
124
+ color: $dark-gray-100;
125
+ top: calc(50% - 10px);
126
+ }
127
+ }
128
+ }
core/modules/dashboard/entries/dashboard/cards/banned-users-list/add-new.js ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { withTheme } from '@rjsf/core';
5
+ import { mapValues, isObject, isEmpty } from 'lodash';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useMemo, useState, useRef } from '@wordpress/element';
11
+ import { useDispatch } from '@wordpress/data';
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import Theme from '@ithemes/security-rjsf-theme';
17
+ import { modifySchemaByUiSchema } from '@ithemes/security-utils';
18
+
19
+ const SchemaForm = withTheme( Theme );
20
+
21
+ const formContext = {
22
+ disableInlineErrors: true,
23
+ };
24
+
25
+ export default function AddNew( {
26
+ id,
27
+ createForm,
28
+ save,
29
+ setSaving,
30
+ afterSave,
31
+ } ) {
32
+ const formElement = useRef( null );
33
+ const [ createData, setCreateData ] = useState( {} );
34
+ const [ extraErrors, setExtraErrors ] = useState( {} );
35
+ const { createNotice } = useDispatch( 'core/notices' );
36
+ const createFormSchema = useMemo( () => {
37
+ if ( ! createForm ) {
38
+ return;
39
+ }
40
+
41
+ return modifySchemaByUiSchema(
42
+ createForm.submissionSchema,
43
+ createForm.submissionSchema.uiSchema || {}
44
+ );
45
+ }, [ createForm ] );
46
+ const onSubmit = async ( e ) => {
47
+ setSaving( true );
48
+ setExtraErrors( {} );
49
+ const ban = await save( createForm.href, e.formData );
50
+ setSaving( false );
51
+
52
+ if ( ban instanceof Error ) {
53
+ if (
54
+ ban.code === 'rest_invalid_param' &&
55
+ isObject( ban.data.params )
56
+ ) {
57
+ const invalidParams = mapValues(
58
+ ban.data.params,
59
+ ( error ) => ( { __errors: [ error ] } )
60
+ );
61
+ setExtraErrors( invalidParams );
62
+ } else {
63
+ createNotice( 'error', ban.message, {
64
+ context: 'ithemes-security',
65
+ } );
66
+ }
67
+
68
+ return;
69
+ }
70
+
71
+ afterSave();
72
+ setCreateData( {} );
73
+ if ( formElement && formElement.current ) {
74
+ const firstInput = formElement.current.formElement.querySelector(
75
+ 'input'
76
+ );
77
+
78
+ if ( firstInput ) {
79
+ firstInput.focus();
80
+ }
81
+ }
82
+ };
83
+
84
+ return (
85
+ <section className="itsec-card-banned-users__create">
86
+ { createFormSchema && (
87
+ <SchemaForm
88
+ id={ id }
89
+ idPrefix={ `${ id }_part` }
90
+ formData={ createData }
91
+ onChange={ ( e ) => setCreateData( e.formData ) }
92
+ onSubmit={ onSubmit }
93
+ schema={ createFormSchema }
94
+ uiSchema={ createFormSchema.uiSchema || {} }
95
+ omitExtraData
96
+ liveValidate={ ! isEmpty( createData ) }
97
+ extraErrors={ extraErrors }
98
+ formContext={ formContext }
99
+ ref={ formElement }
100
+ >
101
+ <></>
102
+ </SchemaForm>
103
+ ) }
104
+ </section>
105
+ );
106
+ }
core/modules/dashboard/entries/dashboard/cards/banned-users-list/index.js ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useDebounceCallback } from '@react-hook/debounce';
5
+ import classnames from 'classnames';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { Button } from '@wordpress/components';
11
+ import { useSelect, useDispatch } from '@wordpress/data';
12
+ import { __ } from '@wordpress/i18n';
13
+ import { useState } from '@wordpress/element';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import Header, { Title } from '../../components/card/header';
19
+ import Footer from '../../components/card/footer';
20
+ import { Back } from '../../components/master-detail';
21
+ import Search from './search';
22
+ import List from './list';
23
+ import AddNew from './add-new';
24
+ import './style.scss';
25
+
26
+ function BannedUsers( { card, config, eqProps } ) {
27
+ const [ isCreating, setCreating ] = useState( false );
28
+ const [ isSaving, setSaving ] = useState( false );
29
+ const { schema } = useSelect( ( select ) => ( {
30
+ schema: select( 'ithemes-security/core' ).getSchema(
31
+ 'ithemes-security-ban'
32
+ ),
33
+ } ) );
34
+ const {
35
+ createBan,
36
+ query,
37
+ invalidateResolutionForStoreSelector: invalidateResolution,
38
+ } = useDispatch( 'ithemes-security/bans' );
39
+ const debouncedQuery = useDebounceCallback( query, 500 );
40
+ const [ selected, select ] = useState( 0 );
41
+ const isSmall =
42
+ eqProps[ 'max-height' ] && eqProps[ 'max-height' ].includes( '500px' );
43
+ const onSelect = ( selectedId ) => {
44
+ select( selectedId );
45
+ setCreating( false );
46
+ };
47
+ const formId = `itsec-ban-card-create-form__${ card.id }`;
48
+ return (
49
+ <div
50
+ className={ classnames( 'itsec-card--type-banned-users', {
51
+ 'itsec-card-banned-users--creating': isCreating,
52
+ } ) }
53
+ >
54
+ <Header>
55
+ <Back
56
+ isSmall={ isSmall }
57
+ select={ select }
58
+ selectedId={ selected }
59
+ />
60
+ <Title card={ card } config={ config } />
61
+ </Header>
62
+ <Search query={ debouncedQuery } />
63
+ <List
64
+ selected={ isCreating ? false : selected }
65
+ onSelect={ onSelect }
66
+ isSmall={ isSmall }
67
+ />
68
+ { isCreating && (
69
+ <AddNew
70
+ id={ formId }
71
+ createForm={ isCreating }
72
+ save={ createBan }
73
+ setSaving={ setSaving }
74
+ afterSave={ () => invalidateResolution( 'getBans' ) }
75
+ />
76
+ ) }
77
+ <Footer>
78
+ { isCreating && (
79
+ <>
80
+ <span className="itsec-card-footer__action">
81
+ <Button
82
+ isLink
83
+ isSmall
84
+ disabled={ isSaving }
85
+ onClick={ () => setCreating( false ) }
86
+ >
87
+ { __( 'Cancel', 'better-wp-security' ) }
88
+ </Button>
89
+ </span>
90
+ <span className="itsec-card-footer__action">
91
+ <Button
92
+ isPrimary
93
+ isSmall
94
+ form={ formId }
95
+ type="submit"
96
+ isBusy={ isSaving }
97
+ disabled={ isSaving }
98
+ >
99
+ { __( 'Save', 'better-wp-security' ) }
100
+ </Button>
101
+ </span>
102
+ </>
103
+ ) }
104
+ { ! isCreating && (
105
+ <>
106
+ { schema?.links
107
+ .filter( ( link ) => link.rel === 'create-form' )
108
+ .map( ( createForm ) => (
109
+ <span
110
+ key={ createForm.href }
111
+ className="itsec-card-footer__action"
112
+ >
113
+ <Button
114
+ isSmall
115
+ isPrimary
116
+ onClick={ () =>
117
+ setCreating(
118
+ isCreating ? false : createForm
119
+ )
120
+ }
121
+ >
122
+ { createForm.title }
123
+ </Button>
124
+ </span>
125
+ ) ) }
126
+ </>
127
+ ) }
128
+ </Footer>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ export const slug = 'banned-users-list';
134
+ export const settings = {
135
+ render: BannedUsers,
136
+ elementQueries: [
137
+ {
138
+ type: 'height',
139
+ dir: 'max',
140
+ px: 500,
141
+ },
142
+ ],
143
+ };
core/modules/dashboard/entries/dashboard/cards/banned-users-list/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/banned-users-list/list.js ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import { flatten, get } from 'lodash';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { Button, TextareaControl, Icon } from '@wordpress/components';
11
+ import { useSelect, useDispatch } from '@wordpress/data';
12
+ import { __ } from '@wordpress/i18n';
13
+ import { dateI18n } from '@wordpress/date';
14
+ import { Fragment, useState } from '@wordpress/element';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import { getSelf, getTargetHint } from '@ithemes/security-utils';
20
+ import MasterDetail from '../../components/master-detail';
21
+
22
+ function MasterRender( { master: ban } ) {
23
+ return (
24
+ <Fragment>
25
+ <th
26
+ scope="row"
27
+ className={ classnames(
28
+ 'itsec-card-banned-users__bans--column-label',
29
+ `itsec-card-banned-users__ban--actor-type-${
30
+ ban.created_by ? ban.created_by.type : 'none'
31
+ }`,
32
+ ban.created_by &&
33
+ `itsec-card-banned-users__ban--actor-id-${ ban.created_by.id }`
34
+ ) }
35
+ >
36
+ <span className="itsec-card-banned-users__bans-label">
37
+ { ban.label }
38
+ </span>
39
+ { ban.created_at && (
40
+ <span className="itsec-card-banned-users__bans-date">
41
+ { dateI18n( 'M d, Y g:i A', ban.created_at ) }
42
+ </span>
43
+ ) }
44
+ </th>
45
+ <td className="itsec-card-banned-users__bans--column-comment">
46
+ { ban.comment }
47
+ </td>
48
+ </Fragment>
49
+ );
50
+ }
51
+
52
+ function DetailRender( { master: ban } ) {
53
+ const { updateBan, deleteBan } = useDispatch( 'ithemes-security/bans' );
54
+ const { createNotice } = useDispatch( 'core/notices' );
55
+ const { isUpdating, isDeleting } = useSelect( ( select ) => ( {
56
+ isUpdating: select( 'ithemes-security/bans' ).isUpdating( ban ),
57
+ isDeleting: select( 'ithemes-security/bans' ).isDeleting( ban ),
58
+ } ) );
59
+ const [ comment, setComment ] = useState( ban.comment );
60
+ const canEdit = getTargetHint( ban, 'allow', false ).includes( 'PUT' );
61
+ const links = flatten( Object.values( get( ban, '_links', {} ) ) ).filter(
62
+ ( link ) => link.media === 'text/html'
63
+ );
64
+ const onSave = async () => {
65
+ const updated = await updateBan( ban, { comment } );
66
+
67
+ if ( updated instanceof Error ) {
68
+ createNotice( 'error', updated.message, {
69
+ context: 'ithemes-security',
70
+ } );
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div className="itsec-card-banned-users__ban">
76
+ <div className="itsec-card-banned-users__ban-top">
77
+ <dl>
78
+ <dt>{ __( 'Host', 'better-wp-security' ) }</dt>
79
+ <dd>{ ban.label }</dd>
80
+ <dt>{ __( 'Time', 'better-wp-security' ) }</dt>
81
+ <dd>
82
+ { ban.created_at &&
83
+ dateI18n( 'M d, Y g:i A', ban.created_at ) }
84
+ </dd>
85
+ <dt>{ __( 'Source', 'better-wp-security' ) }</dt>
86
+ <dd>{ ban.created_by && ban.created_by.label }</dd>
87
+ { ! canEdit && (
88
+ <>
89
+ <dt>{ __( 'Notes', 'better-wp-security' ) }</dt>
90
+ <dd>{ ban.comment }</dd>
91
+ </>
92
+ ) }
93
+ </dl>
94
+
95
+ <ul className="itsec-card-banned-users__ban-actions">
96
+ { getTargetHint( ban, 'allow', false ).includes(
97
+ 'DELETE'
98
+ ) && (
99
+ <li>
100
+ <Button
101
+ isLink
102
+ isBusy={ isDeleting }
103
+ onClick={ () => deleteBan( ban ) }
104
+ icon="dismiss"
105
+ >
106
+ { __( 'Remove Ban', 'better-wp-security' ) }
107
+ </Button>
108
+ </li>
109
+ ) }
110
+
111
+ { links.map( ( link ) => (
112
+ <li key={ link.href }>
113
+ <a href={ link.href }>
114
+ <Icon icon="arrow-right-alt" />
115
+ { link.title }
116
+ </a>
117
+ </li>
118
+ ) ) }
119
+ </ul>
120
+ </div>
121
+
122
+ { canEdit && (
123
+ <TextareaControl
124
+ className="itsec-card-banned-users__ban-notes"
125
+ label={ __( 'Notes', 'better-wp-security' ) }
126
+ value={ comment }
127
+ onChange={ setComment }
128
+ onBlur={ () => comment !== ban.comment && onSave() }
129
+ readOnly={ isUpdating }
130
+ maxLength={ 255 }
131
+ rows={ 4 }
132
+ />
133
+ ) }
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export default function List( { isSmall, onSelect, selected } ) {
139
+ const { fetchQueryNextPage } = useDispatch( 'ithemes-security/bans' );
140
+ const { bans, hasNext, isQuerying } = useSelect( ( select ) => ( {
141
+ bans: select( 'ithemes-security/bans' ).getBans(),
142
+ hasNext: !! select( 'ithemes-security/bans' ).getQueryHeaderLink(
143
+ 'main',
144
+ 'next'
145
+ ),
146
+ isQuerying: select( 'ithemes-security/bans' ).isQuerying( 'main' ),
147
+ } ) );
148
+
149
+ return (
150
+ <MasterDetail
151
+ masters={ bans }
152
+ detailRender={ DetailRender }
153
+ masterRender={ MasterRender }
154
+ selectedId={ selected }
155
+ select={ onSelect }
156
+ idProp={ getSelf }
157
+ direction="vertical"
158
+ borderless
159
+ isSmall={ isSmall }
160
+ hasNext={ hasNext }
161
+ loadNext={ () => fetchQueryNextPage( 'main' ) }
162
+ isQuerying={ isQuerying }
163
+ >
164
+ <thead>
165
+ <tr>
166
+ <th className="itsec-card-banned-users__bans--column-label">
167
+ { __( 'Host', 'better-wp-security' ) }
168
+ </th>
169
+ <th className="itsec-card-banned-users__bans--column-comment">
170
+ { __( 'Notes', 'better-wp-security' ) }
171
+ </th>
172
+ </tr>
173
+ </thead>
174
+ </MasterDetail>
175
+ );
176
+ }
core/modules/dashboard/entries/dashboard/cards/banned-users-list/search.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { omitBy } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __, sprintf } from '@wordpress/i18n';
10
+ import { TextControl } from '@wordpress/components';
11
+ import { useState } from '@wordpress/element';
12
+ import { useSelect } from '@wordpress/data';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import { SelectControl } from '@ithemes/security-components';
18
+
19
+ function useActorsSelect( emptyLabel = '' ) {
20
+ const { types, byType } = useSelect( ( select ) => {
21
+ const selectTypes =
22
+ select( 'ithemes-security/core' ).getActorTypes() || [];
23
+ const selectByType = {};
24
+
25
+ for ( const type of selectTypes ) {
26
+ selectByType[ type.slug ] = select(
27
+ 'ithemes-security/core'
28
+ ).getActors( type.slug );
29
+ }
30
+
31
+ return { types: selectTypes, byType: selectByType };
32
+ }, [] );
33
+
34
+ const options = [];
35
+ options.push( {
36
+ label: emptyLabel,
37
+ value: '',
38
+ } );
39
+
40
+ for ( const type of types ) {
41
+ options.push( {
42
+ label: sprintf(
43
+ /* translators: 1. Actor type label */
44
+ __( 'Any %s', 'better-wp-security' ),
45
+ type.label
46
+ ),
47
+ value: type.slug,
48
+ optgroup: type.label,
49
+ } );
50
+
51
+ for ( const actor of byType[ type.slug ] || [] ) {
52
+ options.push( {
53
+ label: actor.label,
54
+ value: type.slug + ':' + actor.id,
55
+ optgroup: type.label,
56
+ } );
57
+ }
58
+ }
59
+
60
+ return options;
61
+ }
62
+
63
+ export default function Search( { query } ) {
64
+ const actors = useActorsSelect( __( 'All', 'better-wp-security' ) );
65
+ const [ search, setSearch ] = useState( {
66
+ search: '',
67
+ actor_id: '',
68
+ actor_type: '',
69
+ } );
70
+ const onSearch = ( change ) => {
71
+ const newSearch = { ...search, ...change };
72
+ setSearch( newSearch );
73
+ query( 'main', {
74
+ ...omitBy( newSearch, ( value ) => value === '' ),
75
+ per_page: 100,
76
+ } );
77
+ };
78
+
79
+ return (
80
+ <section className="itsec-card-banned-users__search">
81
+ <SelectControl
82
+ options={ actors }
83
+ hideLabelFromVision
84
+ label={ __( 'Ban Reason', 'better-wp-security' ) }
85
+ value={
86
+ search.actor_type && search.actor_id
87
+ ? search.actor_type + ':' + search.actor_id
88
+ : search.actor_type
89
+ }
90
+ onChange={ ( change ) => {
91
+ if ( change === '' ) {
92
+ onSearch( { actor_type: '', actor_id: '' } );
93
+ } else {
94
+ const [ actorType, actorId = '' ] = change.split( ':' );
95
+ onSearch( {
96
+ actor_type: actorType,
97
+ actor_id: actorId,
98
+ } );
99
+ }
100
+ } }
101
+ />
102
+ <TextControl
103
+ value={ search.search }
104
+ onChange={ ( term ) => onSearch( { search: term } ) }
105
+ hideLabelFromVision
106
+ label={ __( 'Search', 'better-wp-security' ) }
107
+ type="search"
108
+ />
109
+ </section>
110
+ );
111
+ }
core/modules/dashboard/entries/dashboard/cards/banned-users-list/style.scss ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-card--type-banned-users {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: space-between;
5
+ height: 100%;
6
+
7
+ & .itsec-card-header {
8
+ flex-shrink: 0;
9
+ }
10
+
11
+ & .itsec-card-footer__actions {
12
+ margin-top: auto;
13
+ }
14
+
15
+ & .itsec-card-banned-users__search {
16
+ display: flex;
17
+ padding: .5em 1em;
18
+ border-top: 1px solid $light-gray-500;
19
+ align-items: center;
20
+
21
+ & > .components-base-control {
22
+ flex-grow: 1;
23
+ flex-basis: 0;
24
+ margin: 0 .25em;
25
+
26
+ &:first-child {
27
+ margin-left: 0;
28
+ }
29
+
30
+ &:last-child {
31
+ margin-right: 0;
32
+ }
33
+
34
+ & .components-base-control__field {
35
+ margin-bottom: 0;
36
+ }
37
+ }
38
+
39
+ & .components-select-control__input {
40
+ margin: 0;
41
+ }
42
+ }
43
+
44
+ & .itsec-card-banned-users__create {
45
+ background: $highlight-blue;
46
+ padding: 1em;
47
+ }
48
+
49
+ & .itsec-component-master-detail {
50
+ flex-shrink: 1;
51
+ }
52
+
53
+ & .itsec-component-master-detail__detail-container {
54
+ background: $highlight-blue;
55
+ padding: 1em;
56
+ }
57
+
58
+ & .itsec-card-banned-users__bans--column-comment {
59
+ width: 40%;
60
+ }
61
+
62
+ & tbody .itsec-card-banned-users__bans--column-label {
63
+ & span {
64
+ display: block;
65
+ }
66
+
67
+ & .itsec-card-banned-users__bans-label {
68
+ color: $main-blue;
69
+ border-left: solid 3px $dark-gray-150;
70
+ padding-left: 3px;
71
+ }
72
+
73
+ &.itsec-card-banned-users__ban--actor-type-lockout_module.itsec-card-banned-users__ban--actor-id-four_oh_four .itsec-card-banned-users__bans-label {
74
+ border-left-color: #e67e22;
75
+ }
76
+
77
+ &.itsec-card-banned-users__ban--actor-type-lockout_module.itsec-card-banned-users__ban--actor-id-brute_force .itsec-card-banned-users__bans-label {
78
+ border-left-color: #2ecc71;
79
+ }
80
+
81
+ &.itsec-card-banned-users__ban--actor-type-lockout_module.itsec-card-banned-users__ban--actor-id-brute_force_admin_user .itsec-card-banned-users__bans-label {
82
+ border-left-color: #3498db;
83
+ }
84
+
85
+ &.itsec-card-banned-users__ban--actor-type-lockout_module.itsec-card-banned-users__ban--actor-id-recaptcha .itsec-card-banned-users__bans-label {
86
+ border-left-color: #e74c3c;
87
+ }
88
+
89
+ &.itsec-card-banned-users__ban--actor-type-user .itsec-card-banned-users__bans-label {
90
+ border-left-color: #8e44ad;
91
+ }
92
+
93
+ & .itsec-card-banned-users__bans-date {
94
+ font-size: .8em;
95
+ padding-left: 6px;
96
+ }
97
+ }
98
+
99
+ & tbody .itsec-card-banned-users__bans--column-comment {
100
+ vertical-align: top;
101
+ font-size: .8em;
102
+ }
103
+
104
+ & .itsec-card-banned-users__bans {
105
+ border-spacing: 0;
106
+ width: 100%;
107
+
108
+ @include sticky-table();
109
+ }
110
+
111
+ & .itsec-card-banned-users__ban {
112
+
113
+ & .itsec-card-banned-users__ban-top {
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: flex-start;
117
+ flex-wrap: wrap;
118
+ margin-bottom: 1em;
119
+ }
120
+
121
+ & dl {
122
+ display: grid;
123
+ grid-template: min-content / min-content 1fr;
124
+ grid-gap: 1em 1.5em;
125
+ margin: 0 1em 1em 0;
126
+
127
+ & dt {
128
+ text-transform: uppercase;
129
+ font-weight: normal;
130
+ color: #6c7781;
131
+ }
132
+
133
+ & dd {
134
+ margin: 0;
135
+ display: inline;
136
+ }
137
+ }
138
+
139
+ & .itsec-card-banned-users__ban-actions {
140
+ margin: 0 0 1em 0;
141
+
142
+ & li:last-child {
143
+ margin-bottom: 0;
144
+ }
145
+
146
+ & a {
147
+ display: inline-flex;
148
+ padding: 6px;
149
+ min-width: 36px;
150
+ align-items: center;
151
+ justify-content: left;
152
+ text-decoration: none;
153
+
154
+ &:hover,
155
+ &:active {
156
+ color: #00a0d2;
157
+ }
158
+
159
+ & .dashicon {
160
+ margin-right: 8px;
161
+ display: inline-block;
162
+ flex: 0 0 auto;
163
+ }
164
+ }
165
+
166
+ .components-button.is-link {
167
+ text-decoration: none;
168
+ }
169
+ }
170
+
171
+ & .itsec-card-banned-users__ban-notes {
172
+ & .components-base-control__field {
173
+ margin-bottom: 0;
174
+ }
175
+
176
+ & label {
177
+ text-transform: uppercase;
178
+ font-weight: normal;
179
+ color: #6c7781;
180
+ }
181
+ }
182
+ }
183
+
184
+ &.itsec-card-banned-users--creating .itsec-card-footer__actions {
185
+ justify-content: flex-end;
186
+ }
187
+
188
+ & textarea[rows] {
189
+ resize: none;
190
+ }
191
+
192
+ // This ugly selector is to make sure we only style the first legend.
193
+ & .itsec-card-banned-users__create > .rjsf > .field-object > fieldset > legend {
194
+ color: $dark-gray-800;
195
+ display: block;
196
+ font-weight: 600;
197
+ margin-left: -2px;
198
+ }
199
+
200
+ & .itsec-rjsf-error-list {
201
+ background: #F7ABAB;
202
+ padding: .25em;
203
+ border-radius: 3px;
204
+ margin-bottom: 1em;
205
+
206
+ & h3 {
207
+ color: #551515;
208
+ font-size: 1em;
209
+ margin: 0;
210
+ }
211
+
212
+ & ul {
213
+ margin: 0;
214
+ }
215
+
216
+ & li {
217
+ margin: 0;
218
+ color: #551515;
219
+ }
220
+ }
221
+
222
+ .itsec-card-banned-users__create {
223
+ .itsec-rjsf-title-field {
224
+ display: block;
225
+ margin-bottom: .5em;
226
+ font-size: 1em;
227
+ color: $medium-text;
228
+ }
229
+
230
+ label {
231
+ text-transform: uppercase;
232
+ font-weight: normal;
233
+ color: $medium-text;
234
+ }
235
+
236
+ p {
237
+ margin: 0;
238
+ }
239
+ }
240
+ }
241
+
242
+ .itsec-card[max-height~="500px"] .itsec-card--type-banned-users {
243
+ &.itsec-card-banned-users--creating {
244
+ & .itsec-card-banned-users__search {
245
+ display: none;
246
+ }
247
+
248
+ & .itsec-component-master-detail {
249
+ display: none;
250
+ }
251
+
252
+ & .itsec-card-banned-users__create {
253
+ flex-grow: 1;
254
+ overflow: scroll;
255
+ }
256
+ }
257
+ }
core/modules/dashboard/entries/dashboard/cards/index.js ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { mapValues, toInteger } from 'lodash';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useDispatch, useSelect } from '@wordpress/data';
10
+ import { useMemo } from '@wordpress/element';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { useSingletonEffect } from '@ithemes/security-hocs';
16
+ import { LineGraph, PieChart } from './renderers';
17
+ import * as activeLockouts from './active-lockouts';
18
+ import * as bannedUsers from './banned-users-list';
19
+
20
+ export function useRegisterCards() {
21
+ const { registerCard } = useDispatch( 'ithemes-security/dashboard' );
22
+
23
+ useSingletonEffect( useRegisterCards, () =>
24
+ [ activeLockouts, bannedUsers ].forEach( ( { slug, settings } ) =>
25
+ registerCard( slug, settings )
26
+ )
27
+ );
28
+ }
29
+
30
+ export function useCardRenderer( config ) {
31
+ const settings = useSelect(
32
+ ( select ) =>
33
+ select( 'ithemes-security/dashboard' ).getRegisteredCard(
34
+ config.slug
35
+ ),
36
+ [ config.slug ]
37
+ );
38
+
39
+ if ( settings?.render ) {
40
+ return settings.render;
41
+ }
42
+
43
+ switch ( config.type ) {
44
+ case 'line':
45
+ return LineGraph;
46
+ case 'pie':
47
+ return PieChart;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ export function useCardElementQueries( config, style, gridWidth ) {
54
+ const settings = useSelect(
55
+ ( select ) =>
56
+ select( 'ithemes-security/dashboard' ).getRegisteredCard(
57
+ config.slug
58
+ ),
59
+ [ config.slug ]
60
+ );
61
+ const queries = settings?.elementQueries;
62
+
63
+ return useMemo( () => {
64
+ if ( ! queries ) {
65
+ return {};
66
+ }
67
+
68
+ const size = {
69
+ height: style.height
70
+ ? toInteger( style.height.replace( 'px', '' ) )
71
+ : 0,
72
+ };
73
+
74
+ if ( style.width && style.width.endsWith( '%' ) ) {
75
+ size.width =
76
+ ( toInteger( style.width.replace( '%', '' ) ) * gridWidth ) /
77
+ 100;
78
+ } else {
79
+ size.width = style.width
80
+ ? toInteger( style.width.replace( 'px', '' ) )
81
+ : 0;
82
+ }
83
+
84
+ const props = {};
85
+
86
+ for ( const query of queries ) {
87
+ if ( ! size[ query.type ] ) {
88
+ continue;
89
+ }
90
+
91
+ let pass = false;
92
+
93
+ switch ( query.dir ) {
94
+ case 'max':
95
+ pass = size[ query.type ] <= query.px;
96
+ break;
97
+ case 'min':
98
+ pass = size[ query.type ] >= query.px;
99
+ break;
100
+ }
101
+
102
+ if ( ! pass ) {
103
+ continue;
104
+ }
105
+
106
+ props[ `${ query.dir }-${ query.type }` ] =
107
+ ( props[ `${ query.dir }-${ query.type }` ] || '' ) +
108
+ query.px +
109
+ 'px ';
110
+ }
111
+
112
+ return mapValues( props, ( str ) => str.trim() );
113
+ }, [ queries, style, gridWidth ] );
114
+ }
core/modules/dashboard/entries/dashboard/cards/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/renderers/index.js ADDED
@@ -0,0 +1,2 @@
 
 
1
+ export { default as LineGraph } from './line-graph';
2
+ export { default as PieChart } from './pie-chart';
core/modules/dashboard/entries/dashboard/cards/renderers/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/index.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ ResponsiveContainer,
6
+ LineChart,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianAxis,
10
+ Tooltip,
11
+ Legend,
12
+ Line,
13
+ } from 'recharts';
14
+ import { isEmpty } from 'lodash';
15
+
16
+ /**
17
+ * WordPress dependencies
18
+ */
19
+ import { dateI18n } from '@wordpress/date';
20
+ import { compose } from '@wordpress/compose';
21
+ import { withSelect } from '@wordpress/data';
22
+
23
+ /**
24
+ * Internal dependencies
25
+ */
26
+ import { PRIMARYS } from '@ithemes/security-style-guide';
27
+ import Header, { Title, Date } from '../../../components/card/header';
28
+ import './style.scss';
29
+
30
+ function LineGraph( { card, config, period } ) {
31
+ period =
32
+ period ||
33
+ ( config.query_args.period && config.query_args.period.default );
34
+
35
+ const data = [],
36
+ lines = [];
37
+
38
+ if ( ! isEmpty( card.data ) ) {
39
+ for ( const key in card.data ) {
40
+ if ( ! card.data.hasOwnProperty( key ) ) {
41
+ continue;
42
+ }
43
+
44
+ for ( let i = 0; i < card.data[ key ].data.length; i++ ) {
45
+ const datum = card.data[ key ].data[ i ];
46
+
47
+ if ( data[ i ] ) {
48
+ data[ i ][ key ] = datum.y;
49
+ } else {
50
+ const format = period === '24-hours' ? 'g A' : 'M j';
51
+
52
+ data.push( {
53
+ name: datum.t ? dateI18n( format, datum.t ) : datum.x,
54
+ [ key ]: datum.y,
55
+ } );
56
+ }
57
+ }
58
+
59
+ lines.push( {
60
+ name: card.data[ key ].label,
61
+ dataKey: key,
62
+ } );
63
+ }
64
+ }
65
+
66
+ return (
67
+ <div className="itsec-card--type-line-graph">
68
+ <Header>
69
+ <Title card={ card } config={ config } />
70
+ <Date card={ card } config={ config } />
71
+ </Header>
72
+ <ResponsiveContainer width="100%" height="100%">
73
+ <LineChart
74
+ margin={ { top: 10, left: -15, right: 50, bottom: 10 } }
75
+ data={ data }
76
+ >
77
+ <XAxis dataKey="name" />
78
+ <YAxis allowDecimals={ false } />
79
+ <CartesianAxis strokeDasharray="3 3" />
80
+ <Tooltip />
81
+ <Legend />
82
+ { lines.map( ( line, i ) => (
83
+ <Line
84
+ type="monotone"
85
+ dataKey={ line.dataKey }
86
+ name={ line.name }
87
+ key={ line.dataKey }
88
+ stroke={ PRIMARYS[ i ] }
89
+ isAnimationActive={ false }
90
+ />
91
+ ) ) }
92
+ </LineChart>
93
+ </ResponsiveContainer>
94
+ </div>
95
+ );
96
+ }
97
+ export default compose( [
98
+ withSelect( ( select, { card } ) => ( {
99
+ period: (
100
+ select( 'ithemes-security/dashboard' ).getDashboardCardQueryArgs(
101
+ card.id
102
+ ) || {}
103
+ ).period,
104
+ } ) ),
105
+ ] )( LineGraph );
core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/renderers/line-graph/style.scss ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .itsec-card--type-line-graph {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ overflow: hidden;
6
+
7
+ & .itsec-card-header {
8
+ flex-shrink: 0;
9
+ }
10
+
11
+ & .recharts-responsive-container {
12
+ flex-grow: 1;
13
+ }
14
+ }
core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/index.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ ResponsiveContainer,
6
+ PieChart as Chart,
7
+ Pie,
8
+ Cell,
9
+ Sector,
10
+ } from 'recharts';
11
+ import classnames from 'classnames';
12
+
13
+ /**
14
+ * WordPress dependencies
15
+ */
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { shortenNumber } from '@ithemes/security-utils';
22
+ import { PRIMARYS } from '@ithemes/security-style-guide';
23
+ import Header, { Title, Date } from '../../../components/card/header';
24
+ import './style.scss';
25
+
26
+ export default function PieChart( { card, config } ) {
27
+ const data = [];
28
+ let total = 0;
29
+
30
+ if ( card.data && card.data.data ) {
31
+ for ( const key in card.data.data ) {
32
+ if ( ! card.data.data.hasOwnProperty( key ) ) {
33
+ continue;
34
+ }
35
+
36
+ total += card.data.data[ key ].sum;
37
+ data.push( {
38
+ name: card.data.data[ key ].label,
39
+ value: card.data.data[ key ].sum,
40
+ } );
41
+ }
42
+ }
43
+
44
+ const hasData = total > 0;
45
+ const fillOpacity = hasData ? 1 : 0.4;
46
+
47
+ if ( ! hasData ) {
48
+ data.forEach( ( datum ) => ( datum.value = 1 ) );
49
+ }
50
+
51
+ const renderActiveShape = ( props ) => {
52
+ const {
53
+ cx,
54
+ cy,
55
+ innerRadius,
56
+ outerRadius,
57
+ startAngle,
58
+ endAngle,
59
+ fill,
60
+ } = props;
61
+
62
+ return (
63
+ <g>
64
+ <text
65
+ x={ cx }
66
+ y={ cy + 10 }
67
+ dy={ 8 }
68
+ textAnchor="middle"
69
+ fill={ fill }
70
+ fillOpacity={ fillOpacity }
71
+ className="itsec-card-pie-chart__circle-sum"
72
+ >
73
+ { hasData ? shortenNumber( card.data.circle_sum ) : '—' }
74
+ </text>
75
+ <text
76
+ x={ cx }
77
+ y={ cy + 30 }
78
+ dy={ 8 }
79
+ textAnchor="middle"
80
+ fill={ fill }
81
+ fillOpacity={ fillOpacity }
82
+ className="itsec-card-pie-chart__circle-label"
83
+ >
84
+ { hasData
85
+ ? card.data.circle_label
86
+ : __( 'No Data', 'better-wp-security' ) }
87
+ </text>
88
+ <Sector
89
+ cx={ cx }
90
+ cy={ cy }
91
+ innerRadius={ innerRadius }
92
+ outerRadius={ outerRadius }
93
+ startAngle={ startAngle }
94
+ endAngle={ endAngle }
95
+ fill={ fill }
96
+ fillOpacity={ fillOpacity }
97
+ />
98
+ </g>
99
+ );
100
+ };
101
+
102
+ return (
103
+ <div
104
+ className={ classnames( 'itsec-card--type-pie-chart', {
105
+ 'itsec-card--type-pie-chart--no-data': ! hasData,
106
+ } ) }
107
+ >
108
+ <Header>
109
+ <Title card={ card } config={ config } />
110
+ { config.query_args.period && (
111
+ <Date card={ card } config={ config } />
112
+ ) }
113
+ </Header>
114
+ <ResponsiveContainer width="100%" height={ 200 }>
115
+ <Chart>
116
+ <Pie
117
+ data={ data }
118
+ dataKey="value"
119
+ innerRadius={ 60 }
120
+ outerRadius={ 80 }
121
+ fill="#8884d8"
122
+ fillOpacity={ fillOpacity }
123
+ paddingAngle={ 5 }
124
+ activeShape={ renderActiveShape }
125
+ activeIndex={ 0 }
126
+ isAnimationActive={ false }
127
+ >
128
+ { data.map( ( entry, index ) => (
129
+ <Cell
130
+ key={ index }
131
+ fill={ PRIMARYS[ index % PRIMARYS.length ] }
132
+ />
133
+ ) ) }
134
+ </Pie>
135
+ </Chart>
136
+ </ResponsiveContainer>
137
+ <table className="itsec-card-pie-chart__values">
138
+ <tbody>
139
+ { data.map( ( datum, i ) => (
140
+ <tr key={ datum.name }>
141
+ <th scope="row">{ datum.name }</th>
142
+ <td style={ { color: PRIMARYS[ i ] } }>
143
+ { hasData
144
+ ? ( ( datum.value / total ) * 100 ).toFixed(
145
+ 0
146
+ ) + '%'
147
+ : '—' }
148
+ </td>
149
+ </tr>
150
+ ) ) }
151
+ </tbody>
152
+ </table>
153
+ </div>
154
+ );
155
+ }
core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/dashboard/entries/dashboard/cards/renderers/pie-chart/style.scss ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .itsec-card--type-pie-chart {
3
+
4
+ & .itsec-card-pie-chart__circle-sum {
5
+ color: $dark-gray-900;
6
+ fill: $dark-gray-900;
7
+ font-size: 4.5em;
8
+ font-weight: bold;
9
+ letter-spacing: -3.8px;
10
+ }
11
+
12
+ & .itsec-card-pie-chart__circle-label {
13
+ color: $dark-gray-500;
14
+ fill: $dark-gray-500;
15
+ text-transform: uppercase;
16
+ }
17
+
18
+ & .itsec-card-pie-chart__values {
19
+ width: 100%;
20
+ padding: 1em;
21
+ border-spacing: 0;
22
+
23
+ & th,
24
+ & td {
25
+ padding: .25em;
26
+ border-bottom: 1px solid $light-gray-500;
27
+ }
28
+
29
+ & tr:last-child th,
30
+ & tr:last-child td {
31
+ border-bottom: none;
32
+ }
33
+
34
+ & th {
35
+ font-weight: normal;
36
+ text-align: left;
37
+ }
38
+
39
+ & td {
40
+ text-align: right;
41
+ }
42
+ }
43
+
44
+ &.itsec-card--type-pie-chart--no-data {
45
+ .itsec-card-pie-chart__values {
46
+ opacity: 0.4;
47
+ }
48
+ }
49
+ }
core/modules/dashboard/entries/dashboard/components/add-card/index.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import memize from 'memize';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { compose, withState } from '@wordpress/compose';
10
+ import { withSelect, withDispatch } from '@wordpress/data';
11
+ import { SelectControl, Button } from '@wordpress/components';
12
+ import { __ } from '@wordpress/i18n';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import './style.scss';
18
+
19
+ const buildCardOptions = memize( ( cards ) => [
20
+ { value: '', label: '' },
21
+ ...cards.map( ( card ) => ( { value: card.href, label: card.title } ) ),
22
+ ] );
23
+
24
+ function AddCard( { cards, addCard, isAdding, selected, setState } ) {
25
+ const onSubmit = ( e ) => {
26
+ e.preventDefault();
27
+
28
+ addCard( selected, {} );
29
+ };
30
+
31
+ const onChange = ( newSelected ) => setState( { sele