PluginProbe
ActivityPub / 5.3.1
ActivityPub v5.3.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / class-activitypub.php

class-activitypub.php in ActivityPub 5.3.1, at includes/class-activitypub.php

743 lines 20.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ActivityPub Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Exception;
11 use Activitypub\Transformer\Factory;
12 use Activitypub\Collection\Outbox;
13 use Activitypub\Collection\Followers;
14 use Activitypub\Collection\Extra_Fields;
15
16 /**
17 * ActivityPub Class.
18 *
19 * @author Matthias Pfefferle
20 */
21 class Activitypub {
22 /**
23 * Initialize the class, registering WordPress hooks.
24 */
25 public static function init() {
26 \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
27 \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
28 \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
29 \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
30 \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
31
32 // Add support for ActivityPub to custom post types.
33 $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
34
35 foreach ( $post_types as $post_type ) {
36 \add_post_type_support( $post_type, 'activitypub' );
37 }
38
39 \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
40 \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
41
42 \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
43 \add_action( 'init', array( self::class, 'theme_compat' ), 11 );
44
45 \add_action( 'user_register', array( self::class, 'user_register' ) );
46
47 \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
48
49 if ( site_supports_blocks() ) {
50 \add_action( 'tool_box', array( self::class, 'tool_box' ) );
51 }
52
53 \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
54
55 \add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
56 \add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 );
57
58 // Register several post_types.
59 self::register_post_types();
60 }
61
62 /**
63 * Activation Hook.
64 */
65 public static function activate() {
66 self::flush_rewrite_rules();
67 Scheduler::register_schedules();
68
69 \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
70 Migration::update_comment_counts();
71 }
72
73 /**
74 * Deactivation Hook.
75 */
76 public static function deactivate() {
77 self::flush_rewrite_rules();
78 Scheduler::deregister_schedules();
79
80 \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
81 Migration::update_comment_counts( 2000 );
82 }
83
84 /**
85 * Uninstall Hook.
86 */
87 public static function uninstall() {
88 Scheduler::deregister_schedules();
89
90 \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
91 Migration::update_comment_counts( 2000 );
92
93 delete_option( 'activitypub_actor_mode' );
94 delete_option( 'activitypub_attribution_domains' );
95 delete_option( 'activitypub_authorized_fetch' );
96 delete_option( 'activitypub_application_user_private_key' );
97 delete_option( 'activitypub_application_user_public_key' );
98 delete_option( 'activitypub_blog_user_private_key' );
99 delete_option( 'activitypub_blog_user_public_key' );
100 delete_option( 'activitypub_blog_description' );
101 delete_option( 'activitypub_blog_identifier' );
102 delete_option( 'activitypub_custom_post_content' );
103 delete_option( 'activitypub_db_version' );
104 delete_option( 'activitypub_default_extra_fields' );
105 delete_option( 'activitypub_enable_blog_user' );
106 delete_option( 'activitypub_enable_users' );
107 delete_option( 'activitypub_header_image' );
108 delete_option( 'activitypub_last_post_with_permalink_as_id' );
109 delete_option( 'activitypub_mailer_new_follower' );
110 delete_option( 'activitypub_mailer_new_dm' );
111 delete_option( 'activitypub_max_image_attachments' );
112 delete_option( 'activitypub_migration_lock' );
113 delete_option( 'activitypub_object_type' );
114 delete_option( 'activitypub_outbox_purge_days' );
115 delete_option( 'activitypub_support_post_types' );
116 delete_option( 'activitypub_use_hashtags' );
117 delete_option( 'activitypub_use_opengraph' );
118 delete_option( 'activitypub_use_permalink_as_id_for_blog' );
119 }
120
121 /**
122 * Return a AS2 JSON version of an author, post or page.
123 *
124 * @param string $template The path to the template object.
125 *
126 * @return string The new path to the JSON template.
127 */
128 public static function render_activitypub_template( $template ) {
129 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
130 return $template;
131 }
132
133 self::add_headers();
134
135 if ( ! is_activitypub_request() ) {
136 return $template;
137 }
138
139 $activitypub_template = false;
140 $activitypub_object = Query::get_instance()->get_activitypub_object();
141
142 if ( $activitypub_object ) {
143 if ( \get_query_var( 'preview' ) ) {
144 \define( 'ACTIVITYPUB_PREVIEW', true );
145
146 /**
147 * Filter the template used for the ActivityPub preview.
148 *
149 * @param string $activitypub_template Absolute path to the template file.
150 */
151 $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
152 } else {
153 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
154 }
155 }
156
157 /*
158 * Check if the request is authorized.
159 *
160 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
161 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
162 */
163 if ( $activitypub_template && use_authorized_fetch() ) {
164 $verification = Signature::verify_http_signature( $_SERVER );
165 if ( \is_wp_error( $verification ) ) {
166 header( 'HTTP/1.1 401 Unauthorized' );
167
168 // Fallback as template_loader can't return http headers.
169 return $template;
170 }
171 }
172
173 if ( $activitypub_template ) {
174 \set_query_var( 'is_404', false );
175
176 // Check if header already sent.
177 if ( ! \headers_sent() ) {
178 // Send 200 status header.
179 \status_header( 200 );
180 }
181
182 return $activitypub_template;
183 }
184
185 return $template;
186 }
187
188 /**
189 * Add the 'self' link to the header.
190 */
191 public static function add_headers() {
192 $id = Query::get_instance()->get_activitypub_object_id();
193
194 if ( ! $id ) {
195 return;
196 }
197
198 if ( ! headers_sent() ) {
199 \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
200
201 if ( ACTIVITYPUB_SEND_VARY_HEADER ) {
202 // Send Vary header for Accept header.
203 \header( 'Vary: Accept', false );
204 }
205 }
206
207 add_action(
208 'wp_head',
209 function () use ( $id ) {
210 echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
211 }
212 );
213 }
214
215 /**
216 * Add support for `p` and `author` query vars.
217 *
218 * @param string $redirect_url The URL to redirect to.
219 * @param string $requested_url The requested URL.
220 *
221 * @return string $redirect_url
222 */
223 public static function redirect_canonical( $redirect_url, $requested_url ) {
224 if ( ! is_activitypub_request() ) {
225 return $redirect_url;
226 }
227
228 $query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
229
230 if ( ! $query ) {
231 return $redirect_url;
232 }
233
234 $query_params = \wp_parse_args( $query );
235 unset( $query_params['activitypub'] );
236
237 if ( 1 !== count( $query_params ) ) {
238 return $redirect_url;
239 }
240
241 if ( isset( $query_params['p'] ) ) {
242 return null;
243 }
244
245 if ( isset( $query_params['author'] ) ) {
246 return null;
247 }
248
249 return $requested_url;
250 }
251
252 /**
253 * Custom redirects for ActivityPub requests.
254 *
255 * @return void
256 */
257 public static function template_redirect() {
258 $comment_id = get_query_var( 'c', null );
259
260 // Check if it seems to be a comment.
261 if ( ! $comment_id ) {
262 return;
263 }
264
265 $comment = get_comment( $comment_id );
266
267 // Load a 404 page if `c` is set but not valid.
268 if ( ! $comment ) {
269 global $wp_query;
270 $wp_query->set_404();
271 return;
272 }
273
274 // Stop if it's not an ActivityPub comment.
275 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
276 return;
277 }
278
279 wp_safe_redirect( get_comment_link( $comment ) );
280 exit;
281 }
282
283 /**
284 * Add the 'activitypub' query variable so WordPress won't mangle it.
285 *
286 * @param array $vars The query variables.
287 *
288 * @return array The query variables.
289 */
290 public static function add_query_vars( $vars ) {
291 $vars[] = 'activitypub';
292 $vars[] = 'preview';
293 $vars[] = 'author';
294 $vars[] = 'c';
295 $vars[] = 'p';
296
297 return $vars;
298 }
299
300 /**
301 * Replaces the default avatar.
302 *
303 * @param array $args Arguments passed to get_avatar_data(), after processing.
304 * @param int|string|object $id_or_email A user ID, email address, or comment object.
305 *
306 * @return array $args
307 */
308 public static function pre_get_avatar_data( $args, $id_or_email ) {
309 if (
310 ! $id_or_email instanceof \WP_Comment ||
311 ! isset( $id_or_email->comment_type ) ||
312 $id_or_email->user_id
313 ) {
314 return $args;
315 }
316
317 $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
318 if (
319 ! empty( $id_or_email->comment_type ) &&
320 ! \in_array(
321 $id_or_email->comment_type,
322 (array) $allowed_comment_types,
323 true
324 )
325 ) {
326 $args['url'] = false;
327 /** This filter is documented in wp-includes/link-template.php */
328 return \apply_filters( 'get_avatar_data', $args, $id_or_email );
329 }
330
331 // Check if comment has an avatar.
332 $avatar = self::get_avatar_url( $id_or_email->comment_ID );
333
334 if ( $avatar ) {
335 if ( empty( $args['class'] ) ) {
336 $args['class'] = array();
337 } elseif ( \is_string( $args['class'] ) ) {
338 $args['class'] = \explode( ' ', $args['class'] );
339 }
340
341 $args['url'] = $avatar;
342 $args['class'][] = 'avatar-activitypub';
343 $args['class'][] = 'u-photo';
344 $args['class'] = \array_unique( $args['class'] );
345 }
346
347 return $args;
348 }
349
350 /**
351 * Function to retrieve Avatar URL if stored in meta.
352 *
353 * @param int|\WP_Comment $comment The comment ID or object.
354 *
355 * @return string The Avatar URL.
356 */
357 public static function get_avatar_url( $comment ) {
358 if ( \is_numeric( $comment ) ) {
359 $comment = \get_comment( $comment );
360 }
361 return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
362 }
363
364 /**
365 * Store permalink in meta, to send delete Activity.
366 *
367 * @param string $post_id The Post ID.
368 */
369 public static function trash_post( $post_id ) {
370 \add_post_meta(
371 $post_id,
372 '_activitypub_canonical_url',
373 \get_permalink( $post_id ),
374 true
375 );
376 }
377
378 /**
379 * Delete permalink from meta.
380 *
381 * @param string $post_id The Post ID.
382 */
383 public static function untrash_post( $post_id ) {
384 \delete_post_meta( $post_id, '_activitypub_canonical_url' );
385 }
386
387 /**
388 * Add rewrite rules.
389 */
390 public static function add_rewrite_rules() {
391 /*
392 * If another system needs to take precedence over the ActivityPub rewrite rules,
393 * they can define their own and will manually call the appropriate functions as required.
394 */
395 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
396 return;
397 }
398
399 if ( ! \class_exists( 'Webfinger' ) ) {
400 \add_rewrite_rule(
401 '^.well-known/webfinger',
402 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
403 'top'
404 );
405 }
406
407 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
408 \add_rewrite_rule(
409 '^.well-known/nodeinfo',
410 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
411 'top'
412 );
413 }
414
415 \add_rewrite_rule(
416 '^@([\w\-\.]+)$',
417 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
418 'top'
419 );
420
421 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
422 }
423
424 /**
425 * Flush rewrite rules.
426 */
427 public static function flush_rewrite_rules() {
428 self::add_rewrite_rules();
429 \flush_rewrite_rules();
430 }
431
432 /**
433 * Adds metabox on wp-admin/tools.php.
434 */
435 public static function tool_box() {
436 if ( \current_user_can( 'edit_posts' ) ) {
437 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' );
438 }
439 }
440
441 /**
442 * Theme compatibility stuff.
443 */
444 public static function theme_compat() {
445 // We assume that you want to use Post-Formats when enabling the setting.
446 if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
447 if ( ! get_theme_support( 'post-formats' ) ) {
448 // Add support for the Aside, Gallery Post Formats...
449 add_theme_support(
450 'post-formats',
451 array(
452 'gallery',
453 'status',
454 'image',
455 'video',
456 'audio',
457 )
458 );
459 }
460 }
461 }
462
463 /**
464 * Display plugin upgrade notice to users.
465 *
466 * @param array $data The plugin data.
467 */
468 public static function plugin_update_message( $data ) {
469 if ( ! isset( $data['upgrade_notice'] ) ) {
470 return;
471 }
472
473 printf(
474 '<div class="update-message">%s</div>',
475 wp_kses(
476 wpautop( $data['upgrade_notice '] ),
477 array(
478 'p' => array(),
479 'a' => array( 'href', 'title' ),
480 'strong' => array(),
481 'em' => array(),
482 )
483 )
484 );
485 }
486
487 /**
488 * Register Custom Post Types.
489 */
490 private static function register_post_types() {
491 \register_post_type(
492 Followers::POST_TYPE,
493 array(
494 'labels' => array(
495 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
496 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
497 ),
498 'public' => false,
499 'hierarchical' => false,
500 'rewrite' => false,
501 'query_var' => false,
502 'delete_with_user' => false,
503 'can_export' => true,
504 'supports' => array(),
505 )
506 );
507
508 \register_post_meta(
509 Followers::POST_TYPE,
510 '_activitypub_inbox',
511 array(
512 'type' => 'string',
513 'single' => true,
514 'sanitize_callback' => 'sanitize_url',
515 )
516 );
517
518 \register_post_meta(
519 Followers::POST_TYPE,
520 '_activitypub_errors',
521 array(
522 'type' => 'string',
523 'single' => false,
524 'sanitize_callback' => function ( $value ) {
525 if ( ! is_string( $value ) ) {
526 throw new Exception( 'Error message is no valid string' );
527 }
528
529 return esc_sql( $value );
530 },
531 )
532 );
533
534 \register_post_meta(
535 Followers::POST_TYPE,
536 '_activitypub_user_id',
537 array(
538 'type' => 'string',
539 'single' => false,
540 'sanitize_callback' => function ( $value ) {
541 return esc_sql( $value );
542 },
543 )
544 );
545
546 \register_post_meta(
547 Followers::POST_TYPE,
548 '_activitypub_actor_json',
549 array(
550 'type' => 'string',
551 'single' => true,
552 'sanitize_callback' => function ( $value ) {
553 return sanitize_text_field( $value );
554 },
555 )
556 );
557
558 // Register Outbox Post-Type.
559 register_post_type(
560 Outbox::POST_TYPE,
561 array(
562 'labels' => array(
563 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ),
564 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ),
565 ),
566 'capabilities' => array(
567 'create_posts' => false,
568 ),
569 'map_meta_cap' => true,
570 'public' => false,
571 'show_in_rest' => true,
572 'rewrite' => false,
573 'query_var' => false,
574 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
575 'delete_with_user' => true,
576 'can_export' => true,
577 'exclude_from_search' => true,
578 )
579 );
580
581 /**
582 * Register Activity Type meta for Outbox items.
583 *
584 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
585 */
586 \register_post_meta(
587 Outbox::POST_TYPE,
588 '_activitypub_activity_type',
589 array(
590 'type' => 'string',
591 'description' => 'The type of the activity',
592 'single' => true,
593 'show_in_rest' => true,
594 'sanitize_callback' => function ( $value ) {
595 $value = ucfirst( strtolower( $value ) );
596 $schema = array(
597 'type' => 'string',
598 'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
599 'default' => 'Announce',
600 );
601
602 if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
603 return $schema['default'];
604 }
605
606 return $value;
607 },
608 )
609 );
610
611 \register_post_meta(
612 Outbox::POST_TYPE,
613 '_activitypub_activity_actor',
614 array(
615 'type' => 'string',
616 'single' => true,
617 'show_in_rest' => true,
618 'sanitize_callback' => function ( $value ) {
619 $schema = array(
620 'type' => 'string',
621 'enum' => array( 'application', 'blog', 'user' ),
622 'default' => 'user',
623 );
624
625 if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
626 return $schema['default'];
627 }
628
629 return $value;
630 },
631 )
632 );
633
634 \register_post_meta(
635 Outbox::POST_TYPE,
636 '_activitypub_outbox_offset',
637 array(
638 'type' => 'integer',
639 'single' => true,
640 'description' => 'Keeps track of the followers offset when processing outbox items.',
641 'sanitize_callback' => 'absint',
642 'default' => 0,
643 )
644 );
645
646 \register_post_meta(
647 Outbox::POST_TYPE,
648 '_activitypub_object_id',
649 array(
650 'type' => 'string',
651 'single' => true,
652 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.',
653 'sanitize_callback' => 'sanitize_url',
654 )
655 );
656
657 \register_post_meta(
658 Outbox::POST_TYPE,
659 'activitypub_content_visibility',
660 array(
661 'type' => 'string',
662 'single' => true,
663 'show_in_rest' => true,
664 'sanitize_callback' => function ( $value ) {
665 $schema = array(
666 'type' => 'string',
667 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
668 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
669 );
670
671 if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
672 return $schema['default'];
673 }
674
675 return $value;
676 },
677 )
678 );
679
680 // Both User and Blog Extra Fields types have the same args.
681 $args = array(
682 'labels' => array(
683 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
684 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
685 'add_new' => __( 'Add new', 'activitypub' ),
686 'add_new_item' => __( 'Add new extra field', 'activitypub' ),
687 'new_item' => __( 'New extra field', 'activitypub' ),
688 'edit_item' => __( 'Edit extra field', 'activitypub' ),
689 'view_item' => __( 'View extra field', 'activitypub' ),
690 'all_items' => __( 'All extra fields', 'activitypub' ),
691 ),
692 'public' => false,
693 'hierarchical' => false,
694 'query_var' => false,
695 'has_archive' => false,
696 'publicly_queryable' => false,
697 'show_in_menu' => false,
698 'delete_with_user' => true,
699 'can_export' => true,
700 'exclude_from_search' => true,
701 'show_in_rest' => true,
702 'map_meta_cap' => true,
703 'show_ui' => true,
704 'supports' => array( 'title', 'editor', 'page-attributes' ),
705 );
706
707 \register_post_type( Extra_Fields::USER_POST_TYPE, $args );
708 \register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
709
710 /**
711 * Fires after ActivityPub custom post types have been registered.
712 */
713 \do_action( 'activitypub_after_register_post_type' );
714 }
715
716 /**
717 * Add the 'activitypub' capability to users who can publish posts.
718 *
719 * @param int $user_id User ID.
720 */
721 public static function user_register( $user_id ) {
722 if ( \user_can( $user_id, 'publish_posts' ) ) {
723 $user = \get_user_by( 'id', $user_id );
724 $user->add_cap( 'activitypub' );
725 }
726 }
727
728 /**
729 * Delete `activitypub_content_visibility` when updated to an empty value.
730 *
731 * @param int $meta_id ID of updated metadata entry.
732 * @param int $object_id Post ID.
733 * @param string $meta_key Metadata key.
734 * @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value
735 * if the value is an array, an object, or itself a PHP-serialized string.
736 */
737 public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) {
738 if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) {
739 \delete_post_meta( $object_id, 'activitypub_content_visibility' );
740 }
741 }
742 }
743