LLMS_Post_Relationships

Hooks and actions related to post relationships.


Source Source

File: includes/class.llms.post.relationships.php

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
class LLMS_Post_Relationships {
 
    /**
     * Configure relationships
     *
     * @var array
     */
    private $relationships = array(
        'course'          => array(
            array(
                'action'    => 'delete',
                'meta_key'  => '_llms_product_id',
                'post_type' => 'llms_access_plan',
            ),
        ),
 
        'llms_membership' => array(
            array(
                'action'    => 'delete',
                'meta_key'  => '_llms_product_id',
                'post_type' => 'llms_access_plan',
            ),
        ),
 
        'lesson'          => array(
            array(
                'action'               => 'unset',
                'meta_key'             => '_llms_prerequisite', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                'meta_keys_additional' => array( '_llms_has_prerequisite' ),
                'post_type'            => 'lesson',
            ),
            array(
                'action'    => 'unset',
                'meta_key'  => '_llms_lesson_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                'post_type' => 'llms_quiz',
            ),
        ),
 
        'llms_order'      => array(
            array(
                'action'    => 'delete',
                'meta_key'  => '_llms_order_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                'post_type' => 'llms_transaction',
            ),
        ),
 
        'llms_quiz'       => array(
            array(
                'action'    => 'delete', // Delete = force delete; trash = move to trash.
                'meta_key'  => '_llms_parent_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                'post_type' => 'llms_question',
            ),
            array(
                'action'               => 'unset',
                'meta_key'             => '_llms_quiz', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                'meta_keys_additional' => array( '_llms_quiz_enabled' ),
                'post_type'            => 'lesson',
            ),
            array(
                'action'     => 'delete',
                'table_name' => 'lifterlms_quiz_attempts',
                'table_key'  => 'quiz_id',
            ),
        ),
 
    );
 
    /**
     * Constructor.
     *
     * @since 3.16.12
     * @since 5.4.0 Prevent course/membership with active subscriptions deletion.
     * @since 6.0.0 Added hook to cleanup user post meta data when awarded certs and achievements are deleted.
     *
     * @return void
     */
    public function __construct() {
 
        add_action( 'delete_post', array( $this, 'maybe_update_relationships' ) );
        add_action( 'pre_delete_post', array( __CLASS__, 'maybe_prevent_product_deletion' ), 10, 2 );
 
        add_action( 'before_delete_post', array( __CLASS__, 'maybe_clean_earned_engagments_related_user_post_meta' ) );
 
    }
 
    /**
     * Maybe delete LifterLMS user post meta related to earned engagements.
     *
     * @since 6.0.0
     *
     * @param int $post_id Post ID.
     * @return void
     */
    public static function maybe_clean_earned_engagments_related_user_post_meta( $post_id ) {
 
        $post_types = array(
            'llms_my_certificate',
            'llms_my_achievement',
        );
        $post_type  = get_post_type( $post_id );
 
        if ( ! in_array( $post_type, $post_types, true ) ) {
            return;
        }
 
        $earned_engagement = 'llms_my_certificate' === $post_type ? new LLMS_User_Certificate( $post_id ) : new LLMS_User_Achievement( $post_id );
 
        do_action_deprecated(
            'llms_before_delete_' . str_replace( 'llms_my_', '', $post_type ),
            array(
                $earned_engagement,
            ),
            '6.0.0',
            '',
            __( 'Use WordPress core  `before_delete_post` action hook', 'lifterlms' )
        );
 
        global $wpdb;
        $wpdb->delete(
            "{$wpdb->prefix}lifterlms_user_postmeta",
            array(
                'user_id'    => $earned_engagement->get_user_id(),
                'meta_key'   => '_' . str_replace( 'llms_my_', '', $post_type ) . '_earned',
                'meta_value' => $post_id,
            ),
            array( '%d', '%s', '%d' )
        ); // no-cache ok.
 
        add_action(
            'after_delete_post',
            function( $post_id ) use ( $earned_engagement, $post_type ) {
 
                if ( $earned_engagement->get( 'id' ) === $post_id ) {
                    do_action_deprecated(
                        'llms_delete_' . str_replace( 'llms_my_', '', $post_type ),
                        array(
                            $earned_engagement,
                        ),
                        '6.0.0',
                        '',
                        __( 'Use WordPress core `deleted_post` action hook.', 'lifterlms' )
                    );
                }
 
            }
        );
    }
 
    /**
     * Determine whether a product deletion should take place.
     *
     * @since 5.4.0
     *
     * @param bool|null $delete Whether to go forward with deletion.
     * @param WP_Post   $post   Post object.
     * @return bool|null
     */
    public static function maybe_prevent_product_deletion( $delete, $post ) {
 
        if ( ! in_array( get_post_type( $post ), array( 'course', 'llms_membership' ), true ) ) {
            return $delete;
        }
 
        $product = llms_get_product( $post );
 
        if ( empty( $product ) || ! $product->has_active_subscriptions() ) {
            return $delete;
        }
 
        // If performing the deletion via REST API change the error message to reflect the reason for the prevention.
        if ( llms_is_rest() ) {
            // Filter the error message.
            add_filter( 'rest_request_after_callbacks', array( __CLASS__, 'rest_filter_products_with_active_subscriptions_error_message' ), 10, 3 );
        } else { // Deleting via wp-admin.
            wp_die(
                self::delete_product_with_active_subscriptions_error_message( $product->get( 'id' ) )
            );
        }
 
        return false;
 
    }
 
    /**
     * Filter the error message returned when trying to delete a product with active subscription via REST API.
     *
     * The original message is a standard permission denied message.
     *
     * @since 5.4.0
     *
     * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
     *                                                                   Usually a WP_REST_Response or WP_Error.
     * @param array                                            $handler  Route handler used for the request.
     * @param WP_REST_Request                                  $request  Request used to generate the response.
     * @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed
     */
    public static function rest_filter_products_with_active_subscriptions_error_message( $response, $handler, $request ) {
 
        if ( is_wp_error( $response ) ) {
            foreach ( $response->errors as $code => &$data ) {
                // Error code can be produced by our rest-api or by wp core.
                if ( in_array( $code, array( 'llms_rest_cannot_delete', 'rest_cannot_delete' ), true ) ) {
                    $data[0] = self::delete_product_with_active_subscriptions_error_message( $request['id'] );
                    break;
                }
            }
        }
 
        return $response;
 
    }
 
    /**
     * Returns the error message to display when deleting a product with active subscriptions.
     *
     * @since 5.4.0
     *
     * @param int $post_id The WP_Post ID of the product.
     * @return string
     */
    public static function delete_product_with_active_subscriptions_error_message( $post_id ) {
 
        $post_type = get_post_type( $post_id );
 
        if ( ! in_array( $post_type, array( 'course', 'llms_membership' ), true ) ) {
            return '';
        }
 
        $post_type_object = get_post_type_object( $post_type );
        $post_type_name   = $post_type_object->labels->name;
        return sprintf(
            // Translators: %s = The post type plural name.
            __( 'Sorry, you are not allowed to delete %s with active subscriptions.', 'lifterlms' ),
            $post_type_name
        );
 
    }
 
    /**
     * Delete / Trash posts related to the deleted post.
     *
     * @since 3.16.12
     * @since 3.37.8 Allow for deletion of related items outside the WP core posts table.
     *
     * @param WP_Post $post WP Post that's been deleted.
     * @param array   $data Relationship data array.
     * @return void
     */
    private function delete_relationships( $post, $data ) {
 
        if ( isset( $data['post_type'] ) && isset( $data['meta_key'] ) ) {
 
            $this->delete_wp_posts( $post, $data );
 
        } elseif ( isset( $data['table_name'] ) && isset( $data['table_key'] ) ) {
 
            $this->delete_table_records( $post, $data );
 
        }
 
    }
 
    /**
     * Delete records from a table that are related to the deleted post.
     *
     * @since 3.37.8
     *
     * @param WP_Post $post WP Post that's been deleted.
     * @param array   $data Relationship data array.
     * @return void
     */
    private function delete_table_records( $post, $data ) {
 
        global $wpdb;
        $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prefix . $data['table_name'],
            array(
                $data['table_key'] => $post->ID,
            ),
            '%d'
        );
 
    }
 
    /**
     * Delete or trash WP Posts related to the deleted post.
     *
     * @since 3.37.8
     *
     * @param WP_Post $post WP Post that's been deleted.
     * @param array   $data Relationship data array.
     * @return void
     */
    private function delete_wp_posts( $post, $data ) {
 
        $relationships = $this->get_related_posts( $post->ID, $data['post_type'], $data['meta_key'] );
 
        $force = ( 'delete' === $data['action'] );
 
        foreach ( $relationships as $id ) {
            wp_delete_post( $id, $force );
        }
 
    }
 
    /**
     * Get a list of post types with relationships that should be checked.
     *
     * @since 3.16.12
     *
     * @return array
     */
    private function get_post_types() {
        return array_keys( $this->get_relationships() );
    }
 
    /**
     * Retrieve filtered LifterLMS post relationships array.
     *
     * @since 3.16.12
     *
     * @return array
     */
    private function get_relationships() {
        return apply_filters( 'llms_get_post_relationships', $this->relationships );
    }
 
    /**
     * Retrieve an array of post ids related to the deleted post by post type and meta key.
     *
     * @since 3.16.12
     *
     * @param int    $post_id   WP Post ID of the deleted post.
     * @param string $post_type WP Post type of the related post(s).
     * @param string $meta_key  meta_key to check for relations by.
     * @return array
     */
    private function get_related_posts( $post_id, $post_type, $meta_key ) {
 
        global $wpdb;
        return $wpdb->get_col(
            $wpdb->prepare(
                "SELECT p.ID
             FROM {$wpdb->posts} AS p
             LEFT JOIN {$wpdb->postmeta} AS pm
                    ON p.ID = pm.post_id
                   AND pm.meta_key = %s
             WHERE p.post_type = %s
               AND pm.meta_value = %d",
                $meta_key,
                $post_type,
                $post_id
            )
        ); // db-call ok; no-cache ok.
 
    }
 
    /**
     * Check relationships and delete / update related posts when a post is deleted.
     *
     * Called on `delete_post` hook (before a post is deleted).
     *
     * @since 3.16.12
     * @since 3.24.0 Unknown.
     *
     * @param int $post_id WP Post ID of the deleted post.
     * @return void
     */
    public function maybe_update_relationships( $post_id ) {
 
        $post = get_post( $post_id );
        if ( ! in_array( $post->post_type, $this->get_post_types(), true ) ) {
            return;
        }
 
        foreach ( $this->get_relationships() as $post_type => $relationships ) {
 
            if ( $post->post_type !== $post_type ) {
                continue;
            }
 
            foreach ( $relationships as $data ) {
 
                if ( in_array( $data['action'], array( 'delete', 'trash' ), true ) ) {
 
                    $this->delete_relationships( $post, $data );
 
                } elseif ( 'unset' === $data['action'] ) {
 
                    $this->unset_relationships( $post, $data );
 
                }
            }
        }
 
    }
 
    /**
     * Unsets relationship data from post_meta when a post is deleted.
     *
     * @since 3.16.12
     * @since 3.24.0 Unknown.
     *
     * @param WP_Post $post WP Post that's been deleted.
     * @param array   $data Relationship data array.
     * @return void
     */
    private function unset_relationships( $post, $data ) {
 
        $relationships = $this->get_related_posts( $post->ID, $data['post_type'], $data['meta_key'] );
 
        foreach ( $relationships as $id ) {
 
            delete_post_meta( $id, $data['meta_key'], $post->ID );
 
            if ( isset( $data['meta_keys_additional'] ) ) {
                foreach ( $data['meta_keys_additional'] as $key ) {
                    delete_post_meta( $id, $key );
                }
            }
        }
 
    }
 
}

Top ↑

Methods Methods


Top ↑

Changelog Changelog

Changelog
Version Description
4.15.0 Delete access plans related to courses/memberships on their deletion.
3.37.8 Delete student quiz attempts when a quiz is deleted.
3.24.0 Unknown.
3.16.12 Introduced.

Top ↑

User Contributed Notes User Contributed Notes

You must log in before being able to contribute a note or feedback.