| @@ -1,13 +1,12 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace IvyForms\Services\Menu; |
| 4 | 4 | |
| 5 | -// phpcs:disable PSR1.Files.SideEffects | |
| 6 | -if (!defined('ABSPATH')) { | |
| 7 | - exit; // Exit if accessed directly | |
| 8 | -} | |
| 9 | - | |
| 5 | +use IvyForms\Services\API\IvyFormsAPI; | |
| 6 | +use IvyForms\Services\Payment\PaymentsFeature; | |
| 7 | +use IvyForms\Services\Permissions\IvyFormsAccess; | |
| 8 | +use IvyForms\Services\Permissions\PermissionResolver; | |
| 10 | 9 | use IvyForms\Services\Translations\BackendStrings; |
| 11 | 10 | |
| 12 | 11 | /** |
| 13 | 12 | * Class Menu |
| @@ -13,21 +12,28 @@ | ||
| 13 | 12 | * Class Menu |
| 14 | 13 | */ |
| 15 | 14 | class MenuMapper |
| 16 | 15 | { |
| 16 | + private ?PermissionResolver $permissionResolver; | |
| 17 | + | |
| 18 | + public function __construct( | |
| 19 | + ?PermissionResolver $permissionResolver = null | |
| 20 | + ) { | |
| 21 | + $this->permissionResolver = $permissionResolver; | |
| 22 | + } | |
| 23 | + | |
| 17 | 24 | /** |
| 18 | 25 | * @return mixed[] |
| 19 | 26 | */ |
| 20 | 27 | public function __invoke(): array |
| 21 | 28 | { |
| 22 | - return | |
| 29 | + $menus = | |
| 23 | 30 | [ |
| 24 | 31 | // [ |
| 25 | 32 | // 'parentSlug' => 'ivyforms', |
| 26 | 33 | // 'pageTitle' => 'Dashboard', |
| 27 | 34 | // 'menuTitle' => BackendStrings::getDashboardStrings()['dashboard'], |
| 28 | -// // TODO - Add IvyForms capability | |
| 29 | -// 'capability' => 'read', | |
| 35 | +// 'capability' => 'manage_options', | |
| 30 | 36 | // 'menuSlug' => 'ivyforms-dashboard', |
| 31 | 37 | // ], |
| 32 | 38 | [ |
| 33 | 39 | 'parentSlug' => 'ivyforms', |
| @@ -32,10 +38,10 @@ | ||
| 32 | 38 | [ |
| 33 | 39 | 'parentSlug' => 'ivyforms', |
| 34 | 40 | 'pageTitle' => 'All Forms', |
| 35 | 41 | 'menuTitle' => BackendStrings::getCommonStrings()['all_forms'], |
| 36 | - // TODO - Add IvyForms capability | |
| 37 | - 'capability' => 'read', | |
| 42 | + // WP checks this cap on the admin menu row; fine-grained access uses PermissionResolver below. | |
| 43 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 38 | 44 | 'menuSlug' => 'ivyforms-forms', |
| 39 | 45 | ], |
| 40 | 46 | [ |
| 41 | 47 | 'parentSlug' => 'ivyforms', |
| @@ -40,10 +46,9 @@ | ||
| 40 | 46 | [ |
| 41 | 47 | 'parentSlug' => 'ivyforms', |
| 42 | 48 | 'pageTitle' => 'New form', |
| 43 | 49 | 'menuTitle' => BackendStrings::getCommonStrings()['new_form'], |
| 44 | - // TODO - Add IvyForms capability | |
| 45 | - 'capability' => 'read', | |
| 50 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 46 | 51 | 'menuSlug' => 'ivyforms-builder', |
| 47 | 52 | ], |
| 48 | 53 | [ |
| 49 | 54 | 'parentSlug' => 'ivyforms', |
| @@ -48,36 +53,116 @@ | ||
| 48 | 53 | [ |
| 49 | 54 | 'parentSlug' => 'ivyforms', |
| 50 | 55 | 'pageTitle' => 'Entries', |
| 51 | 56 | 'menuTitle' => BackendStrings::getEntriesStrings()['entries'], |
| 52 | - // TODO - Add IvyForms capability | |
| 53 | - 'capability' => 'read', | |
| 57 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 54 | 58 | 'menuSlug' => 'ivyforms-entries', |
| 55 | 59 | ], |
| 56 | -// // TODO Add Settings page | |
| 57 | 60 | [ |
| 58 | 61 | 'parentSlug' => 'ivyforms', |
| 62 | + 'pageTitle' => 'Payments', | |
| 63 | + 'menuTitle' => BackendStrings::getPaymentStrings()['payments'], | |
| 64 | + 'capability' => 'manage_options', | |
| 65 | + 'menuSlug' => 'ivyforms-payments', | |
| 66 | + ], | |
| 67 | + [ | |
| 68 | + 'parentSlug' => 'ivyforms', | |
| 59 | 69 | 'pageTitle' => 'Settings', |
| 60 | 70 | 'menuTitle' => BackendStrings::getCommonStrings()['settings'], |
| 61 | - // TODO - Add IvyForms capability | |
| 62 | - 'capability' => 'read', | |
| 71 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 63 | 72 | 'menuSlug' => 'ivyforms-settings', |
| 64 | 73 | ], |
| 65 | -// [ | |
| 66 | -// 'parentSlug' => 'ivyforms', | |
| 67 | -// 'pageTitle' => 'Integrations', | |
| 68 | -// 'menuTitle' => BackendStrings::getIntegrationsStrings()['integrations'], | |
| 69 | -// // TODO - Add IvyForms capability | |
| 70 | -// 'capability' => 'read', | |
| 71 | -// 'menuSlug' => 'ivyforms-integrations', | |
| 72 | -// ], | |
| 74 | + [ | |
| 75 | + 'parentSlug' => 'ivyforms', | |
| 76 | + 'pageTitle' => 'Integrations', | |
| 77 | + 'menuTitle' => BackendStrings::getIntegrationsStrings()['integrations'], | |
| 78 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 79 | + 'menuSlug' => 'ivyforms-integrations', | |
| 80 | + ], | |
| 81 | + ]; | |
| 73 | 82 | |
| 74 | -// [ | |
| 75 | -// 'parentSlug' => 'ivyforms', | |
| 76 | -// 'pageTitle' => 'Testing', | |
| 77 | -// 'menuTitle' => 'Testing', | |
| 78 | -// 'capability' => 'read', | |
| 79 | -// 'menuSlug' => 'ivyforms-testing', | |
| 80 | -// ], | |
| 83 | + // Payments page stays hidden while Stripe and PayPal are turned off | |
| 84 | + if (!PaymentsFeature::isEnabled()) { | |
| 85 | + $menus = array_values(array_filter( | |
| 86 | + $menus, | |
| 87 | + static fn(array $menu): bool => $menu['menuSlug'] !== 'ivyforms-payments' | |
| 88 | + )); | |
| 89 | + } | |
| 90 | + | |
| 91 | + // Add "Upgrade to PRO" menu only if Pro plugin is not installed/activated | |
| 92 | + if (!IvyFormsAPI::isProPluginActive()) { | |
| 93 | + $menus[] = [ | |
| 94 | + 'parentSlug' => 'ivyforms', | |
| 95 | + 'pageTitle' => 'Upgrade to PRO', | |
| 96 | + // phpcs:ignore Generic.Files.LineLength.TooLong | |
| 97 | + 'menuTitle' => '<span class="ivyforms-upgrade-to-pro">' . __('Upgrade to PRO', 'ivyforms') . '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3.30739 5.78346L5.09363 3.36669C6.24826 1.80449 6.82558 1.02339 7.3642 1.18846C7.90283 1.35352 7.90283 2.31153 7.90283 4.22757V4.40823C7.90283 5.0993 7.90283 5.44483 8.12364 5.66157L8.13533 5.6728C8.36091 5.88496 8.72054 5.88496 9.4398 5.88496C10.7341 5.88496 11.3813 5.88496 11.6 6.27751C11.6037 6.28401 11.6072 6.29056 11.6106 6.29716C11.8171 6.69579 11.4424 7.20277 10.6929 8.21673L8.90667 10.6335C7.75202 12.1957 7.1747 12.9768 6.63607 12.8117C6.09744 12.6466 6.09746 11.6886 6.09748 9.77256L6.09749 9.59198C6.0975 8.9009 6.0975 8.55536 5.87669 8.33861L5.865 8.32739C5.63942 8.11522 5.27979 8.11522 4.56052 8.11522C3.26618 8.11522 2.61901 8.11522 2.40029 7.72267C2.39666 7.71617 2.39314 7.70962 2.38973 7.70302C2.18325 7.3044 2.55796 6.79742 3.30739 5.78346Z" fill="white"/></svg></span>', | |
| 98 | + 'capability' => IvyFormsAccess::CAP_ACCESS_PLUGIN, | |
| 99 | + // phpcs:ignore Generic.Files.LineLength.TooLong | |
| 100 | + 'menuSlug' => 'https://ivyforms.com/pricing?utm_source=ivy-lite&utm_medium=lite-upgrade&utm_content=ivy&utm_campaign=ivy', | |
| 101 | + 'isExternal' => true, | |
| 81 | 102 | ]; |
| 103 | + } | |
| 104 | + | |
| 105 | + $visibleMenus = []; | |
| 106 | + foreach ($menus as $menu) { | |
| 107 | + if (!$this->canAccessMenu($menu)) { | |
| 108 | + continue; | |
| 109 | + } | |
| 110 | + $visibleMenus[] = $menu; | |
| 111 | + } | |
| 112 | + | |
| 113 | + return $visibleMenus; | |
| 114 | + } | |
| 115 | + | |
| 116 | + /** | |
| 117 | + * @param mixed[] $menu | |
| 118 | + */ | |
| 119 | + private function canAccessMenu(array $menu): bool | |
| 120 | + { | |
| 121 | + if (IvyFormsAccess::hasImplicitElevatedAccess()) { | |
| 122 | + return true; | |
| 123 | + } | |
| 124 | + | |
| 125 | + $resolver = $this->permissionResolver; | |
| 126 | + $user = wp_get_current_user(); | |
| 127 | + if (!$resolver || !$user instanceof \WP_User || (int) $user->ID <= 0) { | |
| 128 | + return false; | |
| 129 | + } | |
| 130 | + | |
| 131 | + $slug = (string) ($menu['menuSlug'] ?? ''); | |
| 132 | + if ($slug === '' || strpos($slug, 'http') === 0) { | |
| 133 | + return true; | |
| 134 | + } | |
| 135 | + | |
| 136 | + return $this->canAccessBySlug($slug, $resolver, $user); | |
| 137 | + } | |
| 138 | + | |
| 139 | + /** | |
| 140 | + * @param list<string> $permissionKeys | |
| 141 | + */ | |
| 142 | + private function userCanAnyOf(PermissionResolver $resolver, \WP_User $user, array $permissionKeys): bool | |
| 143 | + { | |
| 144 | + return $resolver->userCanAnyOf($user, $permissionKeys); | |
| 145 | + } | |
| 146 | + | |
| 147 | + private function canAccessBySlug(string $slug, PermissionResolver $resolver, \WP_User $user): bool | |
| 148 | + { | |
| 149 | + if ($slug === 'ivyforms-forms') { | |
| 150 | + return $this->userCanAnyOf($resolver, $user, [ | |
| 151 | + 'view_forms_list', | |
| 152 | + 'add_edit_forms', | |
| 153 | + 'delete_forms', | |
| 154 | + ]); | |
| 155 | + } | |
| 156 | + if ($slug === 'ivyforms-builder') { | |
| 157 | + return $resolver->userCanIncludingFormScoped('add_edit_forms', $user, null); | |
| 158 | + } | |
| 159 | + if ($slug === 'ivyforms-entries') { | |
| 160 | + return $this->userCanAnyOf($resolver, $user, ['view_entries_admin', 'delete_entries_admin']); | |
| 161 | + } | |
| 162 | + if (in_array($slug, ['ivyforms-settings', 'ivyforms-integrations'], true)) { | |
| 163 | + return $resolver->userCanIncludingFormScoped('access_settings_page', $user, null); | |
| 164 | + } | |
| 165 | + | |
| 166 | + return true; | |
| 82 | 167 | } |
| 83 | 168 | } |