PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.16.7.2
GiveWP – Donation Plugin and Fundraising Platform v4.16.7.2
4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / Campaigns / Actions / AllowGiveRolesToEditCampaignPages.php
give / src / Campaigns / Actions Last commit date
AddCampaignFormFromRequest.php 1 year ago AddNewBadgeToAdminMenuItem.php 1 year ago AllowGiveRolesToEditCampaignPages.php 1 week ago ArchiveCampaignFormsAsDraftStatus.php 1 year ago ArchiveCampaignPagesAsDraftStatus.php 1 year ago AssignDuplicatedFormToCampaign.php 1 year ago AssociateCampaignPageWithCampaign.php 1 year ago CacheCampaignData.php 9 months ago ConvertQueryDataToCampaign.php 1 year ago CreateCampaignPage.php 1 year ago CreateDefaultCampaignForm.php 6 months ago CreateDefaultLayoutForCampaignPage.php 1 year ago DuplicateCampaign.php 9 months ago EnqueueCampaignPageEditorAssets.php 1 year ago FormInheritsCampaignGoal.php 1 year ago LoadCampaignAdminOptions.php 1 year ago LoadCampaignDetailsAssets.php 1 year ago LoadCampaignPublicOptions.php 1 year ago LoadCampaignsListTableAssets.php 1 year ago PreventDeleteDefaultForm.php 1 year ago RedirectLegacyCreateFormToCreateCampaign.php 6 months ago RegisterCampaignBlocks.php 8 months ago RegisterCampaignIdRestField.php 1 year ago RegisterCampaignShortcodes.php 1 year ago RenderDonateButton.php 11 months ago ReplaceGiveFormsCptLabels.php 1 year ago UnarchiveCampaignFormAsPublishStatus.php 1 year ago
AllowGiveRolesToEditCampaignPages.php
220 lines
1 <?php
2
3 namespace Give\Campaigns\Actions;
4
5 use Give\Campaigns\ValueObjects\CampaignPageMetaKeys;
6 use Give\Framework\Database\DB;
7 use WP_User;
8
9 /**
10 * Allow users with Give roles to edit and publish campaign landing pages.
11 *
12 * Campaign pages are standard WordPress pages (post_type = 'page') that have
13 * the give_campaign_id meta key. Give roles like give_worker and give_manager
14 * have edit_pages but not edit_others_pages or publish_pages, so this action
15 * maps the meta capabilities to allow full management of campaign pages
16 * for users with edit_give_forms capability.
17 *
18 * @since 4.14.0
19 */
20 class AllowGiveRolesToEditCampaignPages
21 {
22 /**
23 * Cache for campaign page checks to avoid repeated DB queries.
24 *
25 * @var array<int, bool>
26 */
27 private static array $campaignPageCache = [];
28
29 /**
30 * Filter meta capabilities for campaign pages.
31 *
32 * Hooked to 'map_meta_cap' filter.
33 *
34 * @since 4.15.3 Handle null $cap gracefully for better compatibility.
35 * @since 4.14.0
36 */
37 public function mapMetaCap(array $caps, ?string $cap, int $userId, array $args): array
38 {
39 // Fast check: only handle specific meta capabilities
40 static $pageMetaCaps = ['edit_post' => true, 'delete_post' => true, 'publish_post' => true, 'read_post' => true];
41 if (!is_string($cap) || !isset($pageMetaCaps[$cap])) {
42 return $caps;
43 }
44
45 // We need a post ID to check
46 if (empty($args[0])) {
47 return $caps;
48 }
49
50 // Check if user has Give capability first (cheaper than DB lookups)
51 if (!user_can($userId, 'edit_give_forms')) {
52 return $caps;
53 }
54
55 // Check if this is a campaign page (uses cache)
56 if (!$this->isCampaignPage((int)$args[0])) {
57 return $caps;
58 }
59
60 // Grant full access to campaign pages
61 return [];
62 }
63
64 /**
65 * Dynamically grant page capabilities when working with a campaign page.
66 *
67 * The block editor and REST API check primitive capabilities like 'publish_pages'
68 * directly (not through map_meta_cap), so we need to dynamically add them.
69 *
70 * Hooked to 'user_has_cap' filter.
71 *
72 * @since 4.14.0
73 */
74 public function grantPublishCapability(array $allcaps, array $caps, array $args, WP_User $user): array
75 {
76 // Fast check: skip if not in admin or REST context
77 if (!is_admin() && !wp_is_serving_rest_request()) {
78 return $allcaps;
79 }
80
81 // Fast check: only process if specific page caps are requested
82 static $pageCaps = ['publish_pages' => true, 'edit_others_pages' => true, 'edit_published_pages' => true, 'delete_others_pages' => true];
83 $requestedPageCaps = array_filter($caps, static function ($cap) use ($pageCaps) {
84 return is_string($cap) && isset($pageCaps[$cap]);
85 });
86 if (empty($requestedPageCaps)) {
87 return $allcaps;
88 }
89
90 // User must have edit_give_forms capability (check from allcaps, no DB query)
91 if (empty($allcaps['edit_give_forms'])) {
92 return $allcaps;
93 }
94
95 // Get the post being edited (cached)
96 $postId = $this->getCurrentEditingPostId();
97 if (!$postId) {
98 return $allcaps;
99 }
100
101 // Check if this is a campaign page (uses cache)
102 if (!$this->isCampaignPage($postId)) {
103 return $allcaps;
104 }
105
106 // Grant the requested page capabilities
107 foreach ($requestedPageCaps as $cap) {
108 $allcaps[$cap] = true;
109 }
110
111 return $allcaps;
112 }
113
114 /**
115 * Check if a post is a campaign page (with caching).
116 *
117 * @since 4.16.7 Read the campaign ID meta directly instead of through get_post_meta().
118 * @since 4.14.0
119 */
120 private function isCampaignPage(int $postId): bool
121 {
122 if (isset(self::$campaignPageCache[$postId])) {
123 return self::$campaignPageCache[$postId];
124 }
125
126 $post = get_post($postId);
127 if (!$post || $post->post_type !== 'page') {
128 self::$campaignPageCache[$postId] = false;
129
130 return false;
131 }
132
133 /*
134 * get_post_meta() fires the get_post_metadata filter, which third parties hook to run
135 * capability checks. Those re-enter this action through map_meta_cap and recurse until
136 * the call stack is exhausted, so read the meta without going through the filter.
137 */
138 $campaignPageMeta = DB::table('postmeta')
139 ->select('meta_value')
140 ->where('post_id', $postId)
141 ->where('meta_key', CampaignPageMetaKeys::CAMPAIGN_ID)
142 ->get();
143
144 self::$campaignPageCache[$postId] = !empty($campaignPageMeta->meta_value);
145
146 return self::$campaignPageCache[$postId];
147 }
148
149 /**
150 * Get the post ID currently being edited (with static caching).
151 *
152 * @since 4.14.0
153 */
154 private function getCurrentEditingPostId(): ?int
155 {
156 static $cachedPostId = null;
157 static $checked = false;
158
159 if ($checked) {
160 return $cachedPostId;
161 }
162 $checked = true;
163
164 // Check for post ID in query string (standard edit screen)
165 if (!empty($_GET['post'])) {
166 $cachedPostId = (int)$_GET['post'];
167 return $cachedPostId;
168 }
169
170 // Check REST API for post ID (query param or route)
171 if (wp_is_serving_rest_request()) {
172 if (!empty($_GET['post_id'])) {
173 $cachedPostId = (int)$_GET['post_id'];
174 return $cachedPostId;
175 }
176
177 $cachedPostId = $this->getPostIdFromRestRoute();
178 if ($cachedPostId) {
179 return $cachedPostId;
180 }
181 }
182
183 // Check global $post
184 global $post;
185 if ($post instanceof \WP_Post) {
186 $cachedPostId = $post->ID;
187 return $cachedPostId;
188 }
189
190 return null;
191 }
192
193 /**
194 * Extract post ID from REST API route.
195 *
196 * @since 4.14.0
197 */
198 private function getPostIdFromRestRoute(): ?int
199 {
200 static $cachedRestPostId = null;
201 static $restChecked = false;
202
203 if ($restChecked) {
204 return $cachedRestPostId;
205 }
206 $restChecked = true;
207
208 global $wp;
209 $restRoute = $wp->query_vars['rest_route'] ?? '';
210
211 // Match routes like /wp/v2/pages/123
212 if (preg_match('#/wp/v2/pages/(\d+)#', $restRoute, $matches)) {
213 $cachedRestPostId = (int)$matches[1];
214 }
215
216 return $cachedRestPostId;
217 }
218 }
219
220