ecd22672 by Jeff Balicki

Content Control

Signed-off-by: Jeff <jeff@gotenzing.com>
1 parent 5ce961ea
Showing 190 changed files with 4891 additions and 0 deletions
1 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2 <path d="M21.8611 8.50354L19.7251 10.6395C20.1421 11.16 20.4841 11.6355 20.7286 12C19.5886 13.695 16.4536 17.73 12.3871 17.9775L9.66455 20.7C10.4086 20.886 11.1856 21 12.0001 21C19.0606 21 23.6161 13.074 23.8066 12.738C24.0631 12.282 24.0646 11.724 23.8081 11.268C23.7376 11.1405 23.0581 9.94654 21.8611 8.50354Z" fill="red"/>
3 <path d="M0.439696 23.5606C0.732196 23.8531 1.1162 24.0001 1.5002 24.0001C1.8842 24.0001 2.2682 23.8531 2.5607 23.5606L23.5607 2.56063C24.1472 1.97413 24.1472 1.02613 23.5607 0.439631C22.9742 -0.146869 22.0262 -0.146869 21.4397 0.439631L17.3222 4.55713C15.7727 3.64663 13.9967 3.00013 12.0002 3.00013C4.8677 3.00013 0.376696 10.9336 0.189196 11.2711C-0.0643035 11.7256 -0.0628035 12.2791 0.192196 12.7336C0.297196 12.9211 1.7582 15.4366 4.2317 17.6476L0.438196 21.4411C-0.146804 22.0261 -0.146804 22.9741 0.439696 23.5606ZM3.2717 11.9986C4.4372 10.2526 7.7192 6.00013 12.0002 6.00013C13.1132 6.00013 14.1557 6.30163 15.1172 6.76213L12.7682 9.11113C12.5222 9.04363 12.2672 9.00013 12.0002 9.00013C10.3427 9.00013 9.0002 10.3426 9.0002 12.0001C9.0002 12.2671 9.0437 12.5221 9.1112 12.7681L6.3602 15.5191C4.9277 14.2651 3.8387 12.8431 3.2717 11.9986Z" fill="red"/>
4 </svg>
1 <?xml version="1.0" encoding="UTF-8"?>
2 <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none">
3 <path d="M13 2C6.92487 2 2 6.92487 2 13C2 15.249 2.67495 17.3404 3.83333 19.0826M20.3333 4.80094C22.5837 6.81512 24 9.74217 24 13C24 19.0751 19.0751 24 13 24C11.7143 24 10.4802 23.7794 9.33333 23.3741M13 9.33333V16.6667" stroke="#007CBA" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path>
4 </svg>
1 <?php
2 /**
3 * Plugin container.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Base;
11
12 defined( 'ABSPATH' ) || exit;
13
14 use ContentControl\Vendor\Pimple\Container as Base;
15
16 /**
17 * Localized container class.
18 */
19 class Container extends Base {
20 /**
21 * Get item from container
22 *
23 * @param string $id Key for the item.
24 *
25 * @return mixed Current value of the item.
26 */
27 public function get( $id ) {
28 return $this->offsetGet( $id );
29 }
30
31 /**
32 * Set item in container
33 *
34 * @param string $id Key for the item.
35 * @param mixed $value Value to be set.
36 *
37 * @return void
38 */
39 public function set( $id, $value ) {
40 $this->offsetSet( $id, $value );
41 }
42 }
1 <?php
2 /**
3 * Plugin controller.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Base;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Localized container class.
16 */
17 abstract class Controller implements \ContentControl\Interfaces\Controller {
18
19 /**
20 * Plugin Container.
21 *
22 * @var \ContentControl\Plugin\Core
23 */
24 public $container;
25
26 /**
27 * Initialize based on dependency injection principles.
28 *
29 * @param \ContentControl\Plugin\Core $container Plugin container.
30 * @return void
31 */
32 public function __construct( $container ) {
33 $this->container = $container;
34 }
35
36 /**
37 * Check if controller is enabled.
38 *
39 * @return bool
40 */
41 public function controller_enabled() {
42 return true;
43 }
44 }
1 <?php
2 /**
3 * Plugin controller.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Base;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * HTTP Stream class.
16 */
17 class Stream {
18
19 /**
20 * Stream name.
21 *
22 * @var string
23 */
24 protected $stream_name;
25
26 /**
27 * Version.
28 *
29 * @var string
30 */
31 const VERSION = '1.0.0';
32
33 /**
34 * Stream constructor.
35 *
36 * @param string $stream_name Stream name.
37 */
38 public function __construct( $stream_name = 'stream' ) {
39 $this->stream_name = $stream_name;
40 }
41
42 /**
43 * Start SSE stream.
44 *
45 * @return void
46 */
47 public function start() {
48 if ( headers_sent() ) {
49 // Do not start the stream if headers have already been sent.
50 return;
51 }
52
53 // Disable default disconnect checks.
54 ignore_user_abort( true );
55
56 // phpcs:disable WordPress.PHP.IniSet.Risky, WordPress.PHP.NoSilencedErrors.Discouraged
57 @ini_set( 'zlib.output_compression', '0' );
58 @ini_set( 'implicit_flush', '1' );
59 @ini_set( 'log_limit', '8096' );
60
61 @ob_end_clean();
62 set_time_limit( 0 );
63 // phpcs:enable WordPress.PHP.IniSet.Risky, WordPress.PHP.NoSilencedErrors.Discouraged
64
65 $this->send_headers();
66 }
67
68 /**
69 * Send SSE headers.
70 *
71 * @return void
72 */
73 public function send_headers() {
74 header( 'Content-Type: text/event-stream' );
75 header( 'Stream-Name: ' . $this->stream_name );
76 header( 'Cache-Control: no-cache' );
77 header( 'Connection: keep-alive' );
78 // Nginx: unbuffered responses suitable for Comet and HTTP streaming applications.
79 header( 'X-Accel-Buffering: no' );
80 $this->flush_buffers();
81 }
82
83 /**
84 * Flush buffers.
85 *
86 * Uses a micro delay to prevent the stream from flushing too quickly.
87 *
88 * @return void
89 */
90 protected function flush_buffers() {
91 // This is for the buffer achieve the minimum size in order to flush data.
92
93 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
94 echo str_repeat( ' ', 1024 * 8 ) . PHP_EOL;
95
96 flush(); // Unless both are called. Some browsers will still cache.
97
98 // Neccessary to prevent the stream from flushing too quickly.
99 usleep( 1000 );
100 }
101
102 /**
103 * Send general message/data to the client.
104 *
105 * @param mixed $data Data to send.
106 *
107 * @return void
108 */
109 public function send_data( $data ) {
110 $data = is_string( $data ) ? $data : \wp_json_encode( $data );
111
112 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
113 echo "data: {$data}" . PHP_EOL;
114 echo PHP_EOL;
115
116 $this->flush_buffers();
117 }
118
119 /**
120 * Send an event to the client.
121 *
122 * @param string $event Event name.
123 * @param mixed $data Data to send.
124 *
125 * @return void
126 */
127 public function send_event( $event, $data = '' ) {
128 $data = is_string( $data ) ? $data : \wp_json_encode( $data );
129
130 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
131 echo "event: {$event}" . PHP_EOL;
132 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
133 echo "data: {$data}" . PHP_EOL;
134 echo PHP_EOL;
135
136 $this->flush_buffers();
137 }
138
139 /**
140 * Send an error to the client.
141 *
142 * @param array{message:string}|string $error Error message.
143 *
144 * @return void
145 */
146 public function send_error( $error ) {
147 $this->send_event( 'error', $error );
148 }
149
150 /**
151 * Check if the connection should abort.
152 *
153 * @return bool
154 */
155 public function should_abort() {
156 return (bool) connection_aborted();
157 }
158 }
1 <?php
2 /**
3 * Plugin controller.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Base;
11
12 defined( 'ABSPATH' ) || exit;
13
14 use Closure;
15 use stdClass;
16
17 /**
18 * Base Upgrade class.
19 */
20 abstract class Upgrade implements \ContentControl\Interfaces\Upgrade {
21
22 /**
23 * Type.
24 *
25 * @var string Uses data versioning types.
26 */
27 const TYPE = '';
28
29 /**
30 * Version.
31 *
32 * @var int
33 */
34 const VERSION = 1;
35
36 /**
37 * Stream.
38 *
39 * @var \ContentControl\Services\UpgradeStream|null
40 */
41 public $stream;
42
43 /**
44 * Upgrade constructor.
45 */
46 public function __construct() {
47 }
48
49 /**
50 * Upgrade label
51 *
52 * @return string
53 */
54 abstract public function label();
55
56 /**
57 * Return full description for this upgrade.
58 *
59 * @return string
60 */
61 public function description() {
62 return '';
63 }
64
65 /**
66 * Check if the upgrade is required.
67 *
68 * @return bool
69 */
70 public function is_required() {
71 $current_version = \ContentControl\get_data_version( static::TYPE );
72 return $current_version && $current_version < static::VERSION;
73 }
74
75 /**
76 * Get the type of upgrade.
77 *
78 * @return string
79 */
80 public function get_type() {
81 return static::TYPE;
82 }
83
84 /**
85 * Check if the prerequisites are met.
86 *
87 * @return bool
88 */
89 public function prerequisites_met() {
90 return true;
91 }
92
93 /**
94 * Get the dependencies for this upgrade.
95 *
96 * @return string[]
97 */
98 public function get_dependencies() {
99 return [];
100 }
101
102 /**
103 * Run the upgrade.
104 *
105 * @return void|\WP_Error|false
106 */
107 abstract public function run();
108
109 /**
110 * Run the upgrade.
111 *
112 * @param \ContentControl\Services\UpgradeStream $stream Stream.
113 *
114 * @return bool|\WP_Error
115 */
116 public function stream_run( $stream ) {
117 $this->stream = $stream;
118
119 $return = $this->run();
120
121 unset( $this->stream );
122
123 if ( is_bool( $return ) || is_wp_error( $return ) ) {
124 return $return;
125 }
126
127 return true;
128 }
129
130 /**
131 * Return the stream.
132 *
133 * If no stream is available it returns a mock object with no-op methods to prevent errors.
134 *
135 * @return \ContentControl\Services\UpgradeStream|(object{
136 * send_event: Closure,
137 * send_error: Closure,
138 * send_data: Closure,
139 * update_status: Closure,
140 * update_task_status: Closure,
141 * start_upgrades: Closure,
142 * complete_upgrades: Closure,
143 * start_task: Closure,
144 * update_task_progress:Closure,
145 * complete_task: Closure
146 * }&\stdClass) Stream.
147 */
148 public function stream() {
149 $noop =
150 /**
151 * No-op.
152 *
153 * @return void
154 */
155 function () {};
156
157 return is_a( $this->stream, '\ContentControl\Services\UpgradeStream' ) ? $this->stream : (object) [
158 'send_event' => $noop,
159 'send_error' => $noop,
160 'send_data' => $noop,
161 'update_status' => $noop,
162 'update_task_status' => $noop,
163 'start_upgrades' => $noop,
164 'complete_upgrades' => $noop,
165 'start_task' => $noop,
166 'update_task_progress' => $noop,
167 'complete_task' => $noop,
168 ];
169 }
170 }
1 <?php
2 /**
3 * Admin controller.
4 *
5 * @copyright (c) 2022, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Controllers;
11
12 use ContentControl\Base\Controller;
13 use ContentControl\Controllers\Admin\Reviews;
14 use ContentControl\Controllers\Admin\SettingsPage;
15 use ContentControl\Controllers\Admin\Upgrades;
16 use ContentControl\Controllers\Admin\UserExperience;
17 use ContentControl\Controllers\Admin\WidgetEditor;
18
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Admin controller class.
23 *
24 * @package ContentControl
25 */
26 class Admin extends Controller {
27
28 /**
29 * Initialize admin controller.
30 *
31 * @return void
32 */
33 public function init() {
34 $this->container->register_controllers( [
35 'Admin\Reviews' => new Reviews( $this->container ),
36 'Admin\Settings' => new SettingsPage( $this->container ),
37 'Admin\Upgrades' => new Upgrades( $this->container ),
38 'Admin\UserExperience' => new UserExperience( $this->container ),
39 'Admin\WidgetEditor' => new WidgetEditor( $this->container ),
40 ] );
41 }
42 }
1 <?php
2 /**
3 * Settings Page Controller Class.
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Admin;
9
10 use ContentControl\Base\Controller;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Settings Page Controller.
16 *
17 * @package ContentControl\Admin
18 */
19 class SettingsPage extends Controller {
20
21 /**
22 * Initialize the settings page.
23 */
24 public function init() {
25 add_action( 'admin_menu', [ $this, 'register_page' ], 999 );
26 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
27 // These are here to listen for incoming webhooks from the Upgrader service when user chooses to install Pro.
28 add_action( 'wp_ajax_content_control_connect_verify_connection', [ $this, 'process_verify_connection' ] );
29 add_action( 'wp_ajax_nopriv_content_control_connect_verify_connection', [ $this, 'process_verify_connection' ] );
30 add_action( 'wp_ajax_content_control_connect_webhook', [ $this, 'process_webhook' ] );
31 add_action( 'wp_ajax_nopriv_content_control_connect_webhook', [ $this, 'process_webhook' ] );
32 }
33
34 /**
35 * Register admin options pages.
36 *
37 * @return void
38 */
39 public function register_page() {
40 add_options_page(
41 __( 'Content Control', 'content-control' ),
42 __( 'Content Control', 'content-control' ),
43 'manage_options',
44 'content-control-settings',
45 [ $this, 'render_page' ]
46 );
47 }
48
49 /**
50 * Render settings page title & container.
51 *
52 * @return void
53 */
54 public function render_page() {
55 ?>
56 <div id="content-control-root-container"></div>
57 <script>jQuery(() => window.contentControl.settingsPage.init());</script>
58 <?php
59 }
60
61 /**
62 * Enqueue assets for the settings page.
63 *
64 * @param string $hook Page hook name.
65 *
66 * @return void
67 */
68 public function enqueue_scripts( $hook ) {
69 if ( 'settings_page_content-control-settings' !== $hook ) {
70 return;
71 }
72
73 wp_enqueue_editor();
74 wp_tinymce_inline_scripts();
75
76 wp_enqueue_script( 'content-control-settings-page' );
77 }
78
79 /**
80 * Verify the connection.
81 *
82 * @return void
83 */
84 public function process_verify_connection() {
85 $this->container->get( 'connect' )->process_verify_connection();
86 }
87
88 /**
89 * Listen for incoming secure webhooks from the API server.
90 *
91 * @return void
92 */
93 public function process_webhook() {
94 $this->container->get( 'connect' )->process_webhook();
95 }
96 }
1 <?php
2 /**
3 * Admin User Experience controller.
4 *
5 * @package ContentControl\Admin
6 * @copyright (c) 2023 Code Atlantic LLC
7 */
8
9 namespace ContentControl\Controllers\Admin;
10
11 use ContentControl\Base\Controller;
12
13 /**
14 * UserExperience controller class.
15 */
16 class UserExperience extends Controller {
17
18 /**
19 * Initialize widget editor UX.
20 *
21 * @return void
22 */
23 public function init() {
24 add_filter( 'plugin_action_links', [ $this, 'plugin_action_links' ], 10, 2 );
25 add_action( "after_plugin_row_{$this->container->get('basename')}", [
26 $this,
27 'after_plugin_row',
28 ], 10, 2 );
29 }
30
31 /**
32 * Render plugin action links.
33 *
34 * @param array<string,string> $links Existing links.
35 * @param string $file Plugin file path.
36 *
37 * @return mixed
38 */
39 public function plugin_action_links( $links, $file ) {
40 $basename = $this->container->get( 'basename' );
41
42 if ( $file === $basename ) {
43 $plugin_action_links = apply_filters(
44 'content_control/plugin_action_links',
45 [
46 'upgrade' => '<a target="_blank" href="https://contentcontrolplugin.com/pricing/?utm_campaign=upgrade-to-pro&utm_source=plugins-page&utm_medium=plugin-ui&utm_content=action-links-upgrade-text">' . __( 'Upgrade to Pro', 'content-control' ) . '</a>',
47 'settings' => '<a href="' . admin_url( 'options-general.php?page=content-control-settings' ) . '">' . __( 'Settings', 'content-control' ) . '</a>',
48 ]
49 );
50
51 if ( $this->container->is_license_active() || $this->container->is_pro_active() ) {
52 unset( $plugin_action_links['upgrade'] );
53 }
54
55 if ( substr( get_locale(), 0, 2 ) === 'en' ) {
56 $plugin_action_links = array_merge( [ 'translate' => '<a href="' . sprintf( 'https://translate.wordpress.org/locale/%s/default/wp-plugins/content-control/', substr( get_locale(), 0, 2 ) ) . '" target="_blank">' . __( 'Translate', 'content-control' ) . '</a>' ], $plugin_action_links );
57 }
58
59 foreach ( $plugin_action_links as $link ) {
60 array_unshift( $links, $link );
61 }
62 }
63
64 return $links;
65 }
66
67 /**
68 * Add a row to the plugin list table.
69 *
70 * @param string $file Plugin file path.
71 * @param array<string,string|int> $plugin_data Plugin data.
72 *
73 * @return void
74 */
75 public function after_plugin_row( $file, $plugin_data ) {
76 $plugin_slug = $this->container->get( 'slug' );
77
78 if ( $plugin_slug !== $plugin_data['slug'] ) {
79 return;
80 }
81
82 $plugin_url = $this->container->get( 'url' );
83
84 ?>
85 <!-- <tr class="plugin-update-tr active">
86 <td colspan="3" class="plugin-update colspanchange">
87 <div class="update-message notice inline notice-warning notice-alt">
88 <p>
89 <?php
90 printf(
91 /* translators: %1$s: Plugin name, %2$s: Plugin URL */
92 esc_html__( 'You are using the %1$s plugin. Please visit the %2$s to learn how to use it.', 'content-control' ),
93 '<strong>' . esc_html__( 'Content Control', 'content-control' ) . '</strong>',
94 '<a href="' . esc_url( $plugin_url ) . '" target="_blank">' . esc_html__( 'plugin website', 'content-control' ) . '</a>'
95 );
96 ?>
97 </p>
98 </div>
99 </td>
100 </tr> -->
101
102 <?php
103 }
104 }
1 <?php
2 /**
3 * Admin Widget Editor controller.
4 *
5 * @note This is only used for the old WP -4.9 widget editor.
6 *
7 * @package ContentControl\Admin
8 * @copyright (c) 2023 Code Atlantic LLC
9 */
10
11 namespace ContentControl\Controllers\Admin;
12
13 use WP_Widget;
14 use ContentControl\Base\Controller;
15
16 use function ContentControl\Rules\allowed_user_roles;
17 use function ContentControl\Widgets\parse_options as parse_widget_options;
18
19 /**
20 * WidgetEditor controller class.
21 */
22 class WidgetEditor extends Controller {
23
24 /**
25 * Initialize widget editor UX.
26 *
27 * @return void
28 */
29 public function init() {
30 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
31 add_action( 'in_widget_form', [ $this, 'fields' ], 5, 3 );
32 add_filter( 'widget_update_callback', [ $this, 'save' ], 5, 3 );
33 }
34
35 /**
36 * Enqueue v1 admin scripts.
37 *
38 * @param mixed $hook Admin page hook name.
39 *
40 * @return void
41 */
42 public function enqueue_assets( $hook ) {
43 if ( 'widgets.php' === $hook ) {
44 wp_enqueue_style( 'content-control-widget-editor' );
45 wp_enqueue_script( 'content-control-widget-editor' );
46 }
47 }
48
49 /**
50 * Renders additional widget option fields.
51 *
52 * @param \WP_Widget $widget Widget instance.
53 * @param bool $ret Whether to return the output.
54 * @param array<string,mixed> $instance Widget instance options.
55 *
56 * @return void
57 */
58 public function fields( $widget, $ret, $instance ) {
59 $allowed_user_roles = allowed_user_roles();
60
61 wp_nonce_field( 'content-control-widget-editor-nonce', 'content-control-widget-editor-nonce' );
62
63 $which_users_options = [
64 '' => __( 'Everyone', 'content-control' ),
65 'logged_out' => __( 'Logged Out Users', 'content-control' ),
66 'logged_in' => __( 'Logged In Users', 'content-control' ),
67 ];
68
69 $instance = parse_widget_options( $instance );
70
71 ?>
72 <p class="widget_options-which_users">
73 <label for="<?php echo esc_attr( $widget->get_field_id( 'which_users' ) ); ?>">
74 <?php esc_html_e( 'Who can see this widget?', 'content-control' ); ?><br />
75 <select name="<?php echo esc_attr( $widget->get_field_name( 'which_users' ) ); ?>" id="<?php echo esc_attr( $widget->get_field_id( 'which_users' ) ); ?>" class="widefat">
76 <?php foreach ( $which_users_options as $option => $label ) : ?>
77 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $option, $instance['which_users'] ); ?>>
78 <?php echo esc_html( $label ); ?>
79 </option>
80 <?php endforeach; ?>
81 </select>
82 </label>
83 </p>
84
85 <p class="widget_options-roles">
86 <?php esc_html_e( 'Choose which roles can see this widget', 'content-control' ); ?><br />
87 <?php foreach ( $allowed_user_roles as $option => $label ) : ?>
88 <label>
89 <input type="checkbox" name="<?php echo esc_attr( $widget->get_field_name( 'roles' ) ); ?>[]" value="<?php echo esc_attr( $option ); ?>" <?php checked( in_array( $option, $instance['roles'], true ), true ); ?>/>
90 <?php echo esc_html( $label ); ?>
91 </label>
92 <?php endforeach; ?>
93 </p>
94 <?php
95 }
96
97 /**
98 * Validates & saves additional widget options.
99 *
100 * @param array<string,mixed> $instance Widget instance options.
101 * @param array<string,mixed> $new_instance New widget instance options.
102 * @param array<string,mixed> $old_instance Old widget instance options.
103 *
104 * @return array<string,mixed>|bool
105 */
106 public function save( $instance, $new_instance, $old_instance ) {
107 if ( isset( $_POST['content-control-widget-editor-nonce'] ) && wp_verify_nonce( wp_unslash( sanitize_key( $_POST['content-control-widget-editor-nonce'] ) ), 'content-control-widget-editor-nonce' ) ) {
108 $new_instance = parse_widget_options( $new_instance );
109 $instance['which_users'] = $new_instance['which_users'];
110 $instance['roles'] = $new_instance['roles'];
111
112 if ( 'logged_in' === $instance['which_users'] ) {
113 $allowed_roles = allowed_user_roles();
114
115 // Validate chosen roles and remove non-allowed roles.
116 foreach ( (array) $instance['roles'] as $key => $role ) {
117 if ( ! array_key_exists( $role, $allowed_roles ) ) {
118 unset( $instance['roles'][ $key ] );
119 }
120 }
121 } else {
122 unset( $instance['roles'] );
123 }
124 } else {
125 // Failed validation, use old instance.
126 $old_instance = parse_widget_options( $old_instance );
127 $instance['which_users'] = $old_instance['which_users'];
128
129 if ( empty( $old_instance['roles'] ) ) {
130 unset( $instance['roles'] );
131 } else {
132 $instance['roles'] = $old_instance['roles'];
133 }
134 }
135
136 return $instance;
137 }
138 }
1 <?php
2 /**
3 * Plugin assets controller.
4 *
5 * @package ContentControl\Admin
6 * @copyright (c) 2023 Code Atlantic LLC.
7 */
8
9 namespace ContentControl\Controllers;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\get_all_plugin_options;
14 use function ContentControl\Rules\allowed_user_roles;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Admin assets controller.
20 *
21 * @package ContentControl\Admin
22 */
23 class Assets extends Controller {
24
25 /**
26 * Initialize the assets controller.
27 */
28 public function init() {
29 add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ], 0 );
30 add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ], 0 );
31 add_action( 'wp_print_scripts', [ $this, 'autoload_styles_for_scripts' ], 0 );
32 add_action( 'admin_print_scripts', [ $this, 'autoload_styles_for_scripts' ], 0 );
33 }
34
35 /**
36 * Get list of plugin packages.
37 *
38 * @return array<string,array<string,mixed>>
39 */
40 public function get_packages() {
41 $permissions = $this->container->get_permissions();
42
43 foreach ( $permissions as $permission => $cap ) {
44 $permissions[ $permission ] = current_user_can( $cap );
45 }
46
47 $wp_version = get_bloginfo( 'version' );
48 // Strip last number from version as they won't be breaking changes.
49 $wp_version = preg_replace( '/\.\d+$/', '', $wp_version );
50
51 $is_pro_installed = \ContentControl\is_plugin_installed( 'content-control-pro/content-control-pro.php' );
52
53 $packages = [
54 'block-editor' => [
55 'handle' => 'content-control-block-editor',
56 'styles' => true,
57 'varsName' => 'contentControlBlockEditor',
58 'vars' => [
59 'adminUrl' => admin_url(),
60 'wpVersion' => $wp_version,
61 'pluginUrl' => $this->container->get_url(),
62 'advancedMode' => $this->container->get_option( 'advanced_mode', false ),
63 'allowedBlocks' => [],
64 'userRoles' => allowed_user_roles(),
65 'excludedBlocks' => array_merge( $this->container->get_option( 'excludedBlocks', [] ), [
66 'core/nextpage',
67 'core/freeform',
68 ] ),
69 'permissions' => $permissions,
70 ],
71 ],
72 'components' => [
73 'handle' => 'content-control-components',
74 'styles' => true,
75 'deps' => [
76 // This is required for tinymce components.
77 'wp-tinymce',
78 // This is required for all tinyMCE plugins.
79 'wp-block-library',
80 ],
81 ],
82 'core-data' => [
83 'handle' => 'content-control-core-data',
84 'deps' => [
85 'wp-api',
86 ],
87 'varsName' => 'contentControlCoreData',
88 'vars' => [
89 'currentSettings' => get_all_plugin_options(),
90 ],
91 ],
92 'data' => [
93 'handle' => 'content-control-data',
94 ],
95 'fields' => [
96 'handle' => 'content-control-fields',
97 ],
98 'icons' => [
99 'handle' => 'content-control-icons',
100 'styles' => true,
101 ],
102 'rule-engine' => [
103 'handle' => 'content-control-rule-engine',
104 'varsName' => 'contentControlRuleEngine',
105 'vars' => [
106 'adminUrl' => admin_url(),
107 'registeredRules' => $this->container->get( 'rules' )->get_block_editor_rules(),
108 ],
109 'styles' => true,
110 ],
111 'settings-page' => [
112 'handle' => 'content-control-settings-page',
113 'varsName' => 'contentControlSettingsPage',
114 'vars' => [
115 'pluginUrl' => $this->container->get( 'url' ),
116 'wpVersion' => $wp_version,
117 'adminUrl' => admin_url(),
118 'restBase' => 'content-control/v2',
119 'userRoles' => allowed_user_roles(),
120 'logUrl' => current_user_can( 'manage_options' ) ? $this->container->get( 'logging' )->get_file_url() : false,
121 'rolesAndCaps' => wp_roles()->roles,
122 'version' => $this->container->get( 'version' ),
123 'permissions' => $permissions,
124 'isProInstalled' => $is_pro_installed,
125 'isProActivated' => $is_pro_installed && is_plugin_active( 'content-control-pro/content-control-pro.php' ),
126 ],
127 'styles' => true,
128 ],
129 'utils' => [
130 'handle' => 'content-control-utils',
131 ],
132 'widget-editor' => [
133 'handle' => 'content-control-widget-editor',
134 'styles' => true,
135 ],
136 ];
137
138 return $packages;
139 }
140
141 /**
142 * Register all package scripts & styles.
143 *
144 * @return void
145 */
146 public function register_scripts() {
147 $packages = $this->get_packages();
148
149 // Register front end block styles.
150 wp_register_style( 'content-control-block-styles', $this->container->get_url( 'dist/style-block-editor.css' ), [], $this->container->get( 'version' ) );
151
152 foreach ( $packages as $package => $package_data ) {
153 $handle = $package_data['handle'];
154 $meta = $this->get_asset_meta( $package );
155
156 $js_deps = isset( $package_data['deps'] ) ? $package_data['deps'] : [];
157
158 wp_register_script( $handle, $this->container->get_url( "dist/$package.js" ), array_merge( $meta['dependencies'], $js_deps ), $meta['version'], true );
159
160 if ( isset( $package_data['styles'] ) && $package_data['styles'] ) {
161 wp_register_style( $handle, $this->container->get_url( "dist/$package.css" ), [ 'wp-components', 'wp-block-editor', 'dashicons' ], $meta['version'] );
162 }
163
164 if ( isset( $package_data['varsName'] ) && ! empty( $package_data['vars'] ) ) {
165 $localized_vars = apply_filters( "content_control/{$package}_localized_vars", $package_data['vars'] );
166 wp_localize_script( $handle, $package_data['varsName'], $localized_vars );
167 }
168
169 /**
170 * May be extended to wp_set_script_translations( 'my-handle', 'my-domain',
171 * plugin_dir_path( MY_PLUGIN ) . 'languages' ) ). For details see
172 * https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/
173 */
174 wp_set_script_translations( $handle, 'content-control' );
175 }
176 }
177
178 /**
179 * Auto load styles if scripts are enqueued.
180 *
181 * @return void
182 */
183 public function autoload_styles_for_scripts() {
184 $packages = $this->get_packages();
185
186 foreach ( $packages as $package => $package_data ) {
187 if ( wp_script_is( $package_data['handle'], 'enqueued' ) ) {
188 if ( isset( $package_data['styles'] ) && $package_data['styles'] ) {
189 wp_enqueue_style( $package_data['handle'] );
190 }
191 }
192 }
193 }
194
195 /**
196 * Get asset meta from generated files.
197 *
198 * @param string $package Package name.
199 * @return array{dependencies:string[],version:string}
200 */
201 public function get_asset_meta( $package ) {
202 $meta_path = $this->container->get_path( "dist/$package.asset.php" );
203 return file_exists( $meta_path ) ? require $meta_path : [
204 'dependencies' => [],
205 'version' => $this->container->get( 'version' ),
206 ];
207 }
208 }
1 <?php
2 /**
3 * Frontend general setup.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers;
10
11 use ContentControl\Base\Controller;
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Class BlockEditor
17 *
18 * @version 2.0.0
19 */
20 class BlockEditor extends Controller {
21
22 /**
23 * Initiate hooks & filter.
24 *
25 * @return void
26 */
27 public function init() {
28 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] );
29 add_action( 'enqueue_block_assets', [ $this, 'enqueue_block_assets' ] );
30 }
31
32 /**
33 * Enqueue block editor assets.
34 *
35 * @return void
36 */
37 public function enqueue_assets() {
38 wp_enqueue_script( 'content-control-block-editor' );
39 }
40
41 /**
42 * Enqueue block assets.
43 *
44 * @return void
45 */
46 public function enqueue_block_assets() {
47 wp_enqueue_style( 'content-control-block-styles' );
48 }
49 }
1 <?php
2 /**
3 * Compatibility controller.
4 *
5 * @copyright (c) 2022, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Controllers;
11
12 use ContentControl\Base\Controller;
13 use ContentControl\Controllers\Compatibility\BetterDocs;
14 use ContentControl\Controllers\Compatibility\Divi;
15 use ContentControl\Controllers\Compatibility\Elementor;
16 use ContentControl\Controllers\Compatibility\QueryMonitor;
17 use ContentControl\Controllers\Compatibility\TheEventsCalendar;
18
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Admin controller class.
23 *
24 * @package ContentControl
25 */
26 class Compatibility extends Controller {
27
28 /**
29 * Initialize admin controller.
30 *
31 * @return void
32 */
33 public function init() {
34 $this->container->register_controllers( [
35 'Compatibility\BetterDocs' => new BetterDocs( $this->container ),
36 'Compatibility\Divi' => new Divi( $this->container ),
37 'Compatibility\Elementor' => new Elementor( $this->container ),
38 'Compatibility\QueryMonitor' => new QueryMonitor( $this->container ),
39 'Compatibility\TheEventsCalendar' => new TheEventsCalendar( $this->container ),
40 ] );
41 }
42 }
1 <?php
2 /**
3 * BetterDocs controller class.
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Compatibility;
9
10 use ContentControl\Base\Controller;
11
12 /**
13 * BetterDocs controller class.
14 */
15 class BetterDocs extends Controller {
16
17 /**
18 * Initiate hooks & filter.
19 *
20 * @return void
21 */
22 public function init() {
23 add_filter( 'content_control/get_rest_api_intent', [ $this, 'get_rest_api_intent' ], 10 );
24 }
25
26 /**
27 * Check if controller is enabled.
28 *
29 * @return bool
30 */
31 public function controller_enabled() {
32 return defined( 'BETTERDOCS_PLUGIN_FILE' );
33 }
34
35 /**
36 * Get intent for BetterDocs.
37 *
38 * @param array<string,mixed> $intent Intent.
39 *
40 * @return array<string,mixed>
41 */
42 public function get_rest_api_intent( $intent ) {
43 global $wp;
44
45 $rest_route = $wp->query_vars['rest_route'];
46 $endpoint_parts = explode( '/', str_replace( '/wp/v2/', '', $rest_route ) );
47
48 // Set the custom search intent.
49 if ( isset( $wp->query_vars['search'] ) ) {
50 $intent['search'] = sanitize_title( $wp->query_vars['search'] );
51 }
52
53 if ( 'unknown' === $intent['type'] && 'docs' === $intent['name'] ) {
54 // If we have a post type or taxonomy, the name is the first part (posts, categories).
55 $post_type = sanitize_key( $endpoint_parts[0] );
56
57 if ( 'docs' === $post_type ) {
58 $intent['type'] = 'post_type';
59 }
60 }
61
62 // phpcs:disable WordPress.Security.NonceVerification.Recommended
63 if ( isset( $_REQUEST['post_type'] ) ) {
64 $post_type = sanitize_text_field( wp_unslash( $_REQUEST['post_type'] ) );
65
66 // Check if any ct_forced_* request aregs are set. If so we should use the post type intent.
67 if ( strpos( $post_type, 'ct_forced_' ) !== false ) {
68 $intent['type'] = 'post_type';
69
70 $post_type = str_replace( 'ct_forced_', '', $post_type );
71
72 $intent['name'] = explode( ':', $post_type );
73 }
74 }
75 // phpcs:enable WordPress.Security.NonceVerification.Recommended
76
77 return $intent;
78 }
79 }
1 <?php
2 /**
3 * Divi compatibility controller.
4 *
5 * @package ContentControl\Admin
6 * @copyright (c) 2023 Code Atlantic LLC
7 */
8
9 namespace ContentControl\Controllers\Compatibility;
10
11 use ContentControl\Base\Controller;
12
13 /**
14 * Divi controller class.
15 */
16 class Divi extends Controller {
17
18 /**
19 * Initialize widget editor UX.
20 *
21 * @return void
22 */
23 public function init() {
24 add_filter( 'content_control/protection_is_disabled', [ $this, 'protection_is_disabled' ] );
25 }
26
27 /**
28 * Check if controller is enabled.
29 *
30 * @return bool
31 */
32 public function controller_enabled() {
33 return defined( 'ET_CORE_VERSION' );
34 }
35
36 /**
37 * Conditionally disable Content Control for Divi builder.
38 *
39 * @param boolean $protection_is_disabled Whether protection is disabled.
40 * @return boolean
41 */
42 public function protection_is_disabled( $protection_is_disabled ) {
43 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
44 if ( isset( $_GET['et_fb'] ) && ! empty( $_GET['et_fb'] ) ) {
45 return true;
46 }
47
48 return $protection_is_disabled;
49 }
50 }
1 <?php
2 /**
3 * Elementor compatibility controller.
4 *
5 * @package ContentControl\Admin
6 * @copyright (c) 2023 Code Atlantic LLC
7 */
8
9 namespace ContentControl\Controllers\Compatibility;
10
11 use ContentControl\Base\Controller;
12
13 /**
14 * Elementor controller class.
15 */
16 class Elementor extends Controller {
17
18 /**
19 * Initialize widget editor UX.
20 *
21 * @return void
22 */
23 public function init() {
24 add_filter( 'content_control/post_types_to_ignore', [ $this, 'post_types_to_ignore' ] );
25 add_filter( 'content_control/protection_is_disabled', [ $this, 'protection_is_disabled' ] );
26 }
27
28 /**
29 * Check if controller is enabled.
30 *
31 * @return bool
32 */
33 public function controller_enabled() {
34 return class_exists( '\Elementor\Plugin' ) || did_action( 'elementor/loaded' );
35 }
36
37 /**
38 * Conditionally disable Content Control for Elementor builder.
39 *
40 * @param boolean $is_disabled Whether protection is disabled.
41 * @return boolean
42 */
43 public function protection_is_disabled( $is_disabled ) {
44 // If already disabled, no reason to continue.
45 if ( $is_disabled || ! did_action( 'elementor/loaded' ) ) {
46 return $is_disabled;
47 }
48
49 return $this->elementor_builder_is_active();
50 }
51
52 /**
53 * Add Elementor font post type to ignored post types.
54 *
55 * @param string[] $post_types Post types to ignore.
56 * @return string[]
57 */
58 public function post_types_to_ignore( $post_types ) {
59 $post_types[] = 'elementor_font';
60 $post_types[] = 'elementor_icons';
61 $post_types[] = 'elementor_library';
62 $post_types[] = 'elementor_snippet';
63
64 return $post_types;
65 }
66
67 /**
68 * Check if Elementor builder is active.
69 *
70 * @return boolean
71 */
72 public function elementor_builder_is_active() {
73 // Check if this is the admin theme builder app.
74 // phpcs:ignore WordPress.Security.NonceVerification.Recommended Simple & direct string comparison.
75 if (
76 is_admin() &&
77 // Disable notices as this is a generic string comparison to prevent doing a lot of work.
78 // phpcs:disable WordPress.Security.NonceVerification.Recommended
79 ! empty( $_GET['page'] ) &&
80 'elementor-app' === $_GET['page']
81 // phpcs:enable WordPress.Security.NonceVerification.Recommended
82 ) {
83 return true;
84 }
85
86 if ( ! class_exists( '\Elementor\Plugin' ) || ! isset( \Elementor\Plugin::$instance ) ) {
87 return false;
88 }
89
90 /**
91 * Elementor instance.
92 *
93 * @var \Elementor\Plugin $elementor
94 */
95 $elementor = \Elementor\Plugin::$instance;
96
97 /**
98 * Elementor preview instance.
99 *
100 * @var \Elementor\Preview|(object{is_preview_mod:\Closure}&\stdClass)|false $preview
101 */
102 $preview = isset( $elementor->preview ) ? $elementor->preview : false;
103
104 if ( false === $preview || ! method_exists( $preview, 'is_preview_mode' ) ) {
105 return false;
106 }
107
108 // Check if the page builder is active.
109 return $preview->is_preview_mode();
110 }
111 }
1 <?php
2 /**
3 * QueryMonitor
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Compatibility;
9
10 use ContentControl\Base\Controller;
11 use ContentControl\QueryMonitor\Output;
12 use ContentControl\QueryMonitor\Collector;
13 use QM_Collectors;
14
15 use function ContentControl\is_frontend;
16
17 /**
18 * QueryMonitor
19 */
20 class QueryMonitor extends Controller {
21
22 /**
23 * Initialize the class
24 *
25 * @return void
26 */
27 public function init() {
28 $this->register_collector();
29 add_filter( 'qm/outputter/html', [ $this, 'register_output_html' ], 10 );
30 }
31
32 /**
33 * Check if controller is enabled.
34 *
35 * @return bool
36 */
37 public function controller_enabled() {
38 return class_exists( 'QueryMonitor' );
39 }
40
41 /**
42 * Register collector.
43 *
44 * @return void
45 */
46 public function register_collector() {
47 QM_Collectors::add( new Collector() );
48 }
49
50 /**
51 * Add Query Monitor outputter.
52 *
53 * @param array<string,\QM_Output_Html> $output Outputters.
54 * @return array<string,\QM_Output_Html> Outputters.
55 */
56 public function register_output_html( $output ) {
57 if ( ! is_frontend() ) {
58 return $output;
59 }
60
61 $collector = QM_Collectors::get( 'content-control' );
62
63 if ( $collector ) {
64 $output['content-control'] = new Output( $collector );
65 }
66
67 return $output;
68 }
69 }
1 <?php
2 /**
3 * The Events Calendar
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Compatibility;
9
10 use ContentControl\Base\Controller;
11
12 /**
13 * TheEventsCalendar controller class.
14 */
15 class TheEventsCalendar extends Controller {
16
17 /**
18 * Initiate hooks & filter.
19 *
20 * @return void
21 */
22 public function init() {
23 // 'wp_redirect'
24 add_action( 'content_control/restrict_main_query', [ $this, 'restrict_main_query' ], 10 );
25 }
26
27 /**
28 * Check if controller is enabled.
29 *
30 * @return bool
31 */
32 public function controller_enabled() {
33 return class_exists( '\Tribe__Events__Main' ) && defined( 'TRIBE_EVENTS_FILE' );
34 }
35
36 /**
37 * Handle restrictions on the main query.
38 *
39 * When the main query is set to be redirected, TEC was cancelling the redirect. Returing true will allow the redirect to happen.
40 *
41 * @return void
42 */
43 public function restrict_main_query() {
44 // If during the main query, a redirect is called on the events page, we need to allow it to happen.
45 add_filter( 'wp_redirect', function ( $location ) {
46 // Only call this filter within the redirect filter. Limiting the scope of the filter.
47 add_filter( 'tec_events_views_v2_redirected', '__return_true' );
48 return $location;
49 }, 0 );
50 }
51 }
1 <?php
2 /**
3 * Frontend general setup.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers;
10
11 use ContentControl\Base\Controller;
12
13 use ContentControl\Controllers\Frontend\Blocks;
14 use ContentControl\Controllers\Frontend\Restrictions;
15 use ContentControl\Controllers\Frontend\Widgets;
16
17 defined( 'ABSPATH' ) || exit;
18
19 /**
20 * Class Frontend
21 */
22 class Frontend extends Controller {
23
24 /**
25 * Initialize Hooks & Filters
26 */
27 public function init() {
28 $this->container->register_controllers([
29 'Frontend\Blocks' => new Blocks( $this->container ),
30 'Frontend\Restrictions' => new Restrictions( $this->container ),
31 'Frontend\Widgets' => new Widgets( $this->container ),
32 ]);
33
34 $this->hooks();
35 }
36
37 /**
38 * Register general frontend hooks.
39 *
40 * @return void
41 */
42 public function hooks() {
43 $this->replicate_core_content_filters();
44
45 add_filter( 'content_control/restricted_post_content', '\ContentControl\append_post_excerpts', 9, 2 );
46 add_filter( 'content_control/restricted_post_excerpt', '\ContentControl\append_post_excerpts', 9, 2 );
47 }
48
49 /**
50 * Replicate core content filters.
51 *
52 * @return void
53 */
54 private function replicate_core_content_filters() {
55 /**
56 * Instance of WP_Embed class.
57 *
58 * @var \WP_Embed $wp_embed
59 */
60 global $wp_embed;
61
62 $the_content = 'content_control/restricted_post_content';
63 $the_excerpt = 'content_control/restricted_post_excerpt';
64
65 // These all follow WP core's `the_content` filter.
66 add_filter( $the_content, 'do_blocks', 9 );
67 add_filter( $the_content, 'wptexturize' );
68 add_filter( $the_content, 'convert_smilies', 20 );
69 add_filter( $the_content, 'wpautop' );
70 add_filter( $the_content, 'shortcode_unautop' );
71 add_filter( $the_content, 'prepend_attachment' );
72 add_filter( $the_content, 'wp_replace_insecure_home_url' );
73 add_filter( $the_content, 'do_shortcode', 11 ); // AFTER wpautop().
74 add_filter( $the_content, 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
75 add_filter( $the_content, 'capital_P_dangit', 11 );
76 add_filter( $the_content, [ $wp_embed, 'run_shortcode' ], 8 );
77 add_filter( $the_content, [ $wp_embed, 'autoembed' ], 8 );
78
79 // These all follow WP core's `the_excerpt` filter.
80 add_filter( $the_excerpt, 'wptexturize' );
81 add_filter( $the_excerpt, 'convert_smilies' );
82 add_filter( $the_excerpt, 'convert_chars' );
83 add_filter( $the_excerpt, 'wpautop' );
84 add_filter( $the_excerpt, 'shortcode_unautop' );
85 add_filter( $the_excerpt, 'wp_replace_insecure_home_url' );
86 add_filter( $the_excerpt, 'wp_filter_content_tags', 12 );
87 add_filter( $the_excerpt, 'capital_P_dangit', 11 );
88 }
89 }
1 <?php
2 /**
3 * Frontend restrictions setup.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend;
10
11 use ContentControl\Base\Controller;
12 use ContentControl\Controllers\Frontend\Restrictions\MainQuery;
13 use ContentControl\Controllers\Frontend\Restrictions\QueryPosts;
14 use ContentControl\Controllers\Frontend\Restrictions\PostContent;
15 use ContentControl\Controllers\Frontend\Restrictions\QueryTerms;
16 use ContentControl\Controllers\Frontend\Restrictions\RestAPI;
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * Class for handling global restrictions.
22 *
23 * @package ContentControl
24 */
25 class Restrictions extends Controller {
26
27 /**
28 * Initiate functionality.
29 */
30 public function init() {
31 $this->container->register_controllers( [
32 'Frontend\Restrictions\MainQuery' => new MainQuery( $this->container ),
33 'Frontend\Restrictions\QueryPosts' => new QueryPosts( $this->container ),
34 'Frontend\Restrictions\QueryTerms' => new QueryTerms( $this->container ),
35 'Frontend\Restrictions\PostContent' => new PostContent( $this->container ),
36 'Frontend\Restrictions\RestAPI' => new RestAPI( $this->container ),
37 ] );
38 }
39 }
1 <?php
2 /**
3 * Frontend main query restrictions.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend\Restrictions;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\redirect;
14 use function ContentControl\get_main_wp_query;
15 use function ContentControl\set_query_to_page;
16 use function ContentControl\reset_query_context;
17 use function ContentControl\query_can_be_ignored;
18 use function ContentControl\content_is_restricted;
19 use function ContentControl\override_query_context;
20 use function ContentControl\protection_is_disabled;
21 use function ContentControl\get_applicable_restriction;
22 use function ContentControl\get_restriction_matches_for_queried_posts;
23
24 defined( 'ABSPATH' ) || exit;
25
26 /**
27 * Class for handling global restrictions of the Main Query.
28 *
29 * @package ContentControl
30 */
31 class MainQuery extends Controller {
32
33 /**
34 * Initiate functionality.
35 */
36 public function init() {
37 // This can be done no later than template_redirect, and no sooner than send_headers (when conditional tags are available).
38 // Can be done on send_headers, posts_selection, or wp as well.
39 add_action( 'template_redirect', [ $this, 'restrict_main_query' ], 10 );
40 }
41
42 /**
43 * Handle a restriction on the main query.
44 *
45 * NOTE: This is only for redirecting or replacing pages and
46 * should not be used to filter or hide post contents.
47 *
48 * @return void
49 */
50 public function restrict_main_query() {
51 if ( ! \is_main_query() || protection_is_disabled() ) {
52 return;
53 }
54
55 $this->check_main_query();
56 $this->check_main_query_posts();
57 }
58
59 /**
60 * Handle restrictions on the main query.
61 *
62 * NOTE: This is only for redirecting or replacing archives and
63 * should not be used to filter or hide post contents.
64 *
65 * @return void
66 */
67 public function check_main_query() {
68 // Bail if we didn't match any restrictions.
69 if ( content_is_restricted() ) {
70 $restriction = get_applicable_restriction();
71
72 /**
73 * Use this filter to prevent a post from being restricted, or to handle it yourself.
74 *
75 * @param null|mixed $pre Whether to prevent the post from being restricted.
76 * @param null|\ContentControl\Models\Restriction $restriction Restriction object.
77 * @return null|mixed
78 */
79 if ( null !== apply_filters( 'content_control/pre_restrict_main_query', null, $restriction ) ) {
80 return;
81 }
82
83 /**
84 * Fires when a post is restricted, but before the restriction is handled.
85 *
86 * @param \ContentControl\Models\Restriction $restriction Restriction object.
87 */
88 do_action( 'content_control/restrict_main_query', $restriction );
89
90 $method = $restriction->get_setting( 'protectionMethod' );
91
92 switch ( $method ) {
93 case 'redirect':
94 redirect( $restriction->get_setting( 'redirectType' ), $restriction->get_setting( 'redirectUrl' ) );
95 return;
96
97 case 'replace':
98 if ( 'page' === $restriction->get_setting( 'replacementType' ) ) {
99 set_query_to_page( $restriction->get_setting( 'replacementPage' ) );
100 return;
101 }
102 }
103 }
104 }
105
106 /**
107 * Handle restrictions on the main query posts.
108 *
109 * NOTE: This is only for redirecting or replacing archives and
110 * should not be used to filter or hide post contents.
111 *
112 * @return void
113 */
114 public function check_main_query_posts() {
115 $query = get_main_wp_query();
116
117 if ( query_can_be_ignored( $query ) ) {
118 return;
119 }
120
121 // Ensure rules are checked in the correct context.
122 override_query_context( 'main/posts' );
123
124 // Get restrictions for the queried posts.
125 $post_restrictions = get_restriction_matches_for_queried_posts( $query );
126
127 // Reset query context.
128 reset_query_context();
129
130 if ( ! $post_restrictions ) {
131 return;
132 }
133
134 // Only the highest priority restriction is needed with redirect or replace handling.
135 $restriction_match = false;
136
137 // Find the highest priority restriction with redirect or replace handling.
138 foreach ( $post_restrictions as $post_restriction ) {
139 /**
140 * Restriction object.
141 *
142 * @var \ContentControl\Models\Restriction
143 */
144 $restriction = $post_restriction['restriction'];
145
146 if ( 'redirect' === $restriction->get_setting( 'archiveHandling' ) || 'replace_archive_page' === $restriction->get_setting( 'archiveHandling' ) ) {
147 $restriction_match = $post_restriction;
148 break;
149 }
150 }
151
152 if ( false === $restriction_match ) {
153 return;
154 }
155
156 /**
157 * Restriction object.
158 *
159 * @var \ContentControl\Models\Restriction
160 */
161 $restriction = $restriction_match['restriction'];
162 $post_ids = $restriction_match['post_ids'];
163
164 /**
165 * Use this filter to prevent a post from being restricted, or to handle it yourself.
166 *
167 * @param null|mixed $pre Whether to prevent the post from being restricted.
168 * @param null|\ContentControl\Models\Restriction $restriction Restriction object.
169 * @param int[] $post_id Post ID.
170 *
171 * @return null|mixed
172 */
173 if ( null !== apply_filters( 'content_control/pre_restrict_main_query_post', null, $restriction, $post_ids ) ) {
174 return;
175 }
176
177 /**
178 * Fires when a post is restricted, but before the restriction is handled.
179 *
180 * @param \ContentControl\Models\Restriction $restriction Restriction object.
181 * @param int[] $post_id Post ID.
182 */
183 do_action( 'content_control/restrict_main_query_post', $restriction, $post_ids );
184
185 switch ( $restriction->get_setting( 'archiveHandling' ) ) {
186 case 'replace_archive_page':
187 set_query_to_page( $restriction->get_setting( 'archiveReplacementPage' ) );
188 break;
189 case 'redirect':
190 redirect( $restriction->get_setting( 'archiveRedirectType' ), $restriction->get_setting( 'archiveRedirectUrl' ) );
191 break;
192 }
193 }
194 }
1 <?php
2 /**
3 * Frontend post content restrictions.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend\Restrictions;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\content_is_restricted;
14 use function ContentControl\protection_is_disabled;
15 use function ContentControl\get_applicable_restriction;
16
17 defined( 'ABSPATH' ) || exit;
18
19 /**
20 * Class for handling global restrictions of the post contents.
21 *
22 * @package ContentControl
23 */
24 class PostContent extends Controller {
25
26 /**
27 * Initiate functionality.
28 */
29 public function init() {
30 $this->enable_filters();
31 }
32
33 /**
34 * Enable filters.
35 *
36 * @return void
37 */
38 public function enable_filters() {
39 add_filter( 'the_content', [ $this, 'filter_the_content_if_restricted' ], 1000 );
40 add_filter( 'get_the_excerpt', [ $this, 'filter_the_excerpt_if_restricted' ], 1000, 2 );
41 // phpcs:disable Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.InlineComment.InvalidEndChar -- These are for future use.
42 // add_filter( 'the_title', [ $this, 'filter_the_title_if_restricted'], 1000, 2 );
43 // add_filter( 'get_the_excerpt', [ $this, 'filter_the_excerpt_if_restricted' ], 1000, 2 );
44 // add_filter( 'post_class', [ $this, 'filter_post_class_if_restricted' ], 1000, 3 );
45 // add_filter( 'post_password_required', [ $this, 'require_password_if_restricted' ], 1000, 2 );
46 // add_filter( 'the_password_form', [ $this, 'filter_password_form_if_restricted' ], 1000, 2 );
47 // phpcs:enable Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.InlineComment.InvalidEndChar
48 }
49
50 /**
51 * Disable filters.
52 *
53 * @return void
54 */
55 public function disable_filters() {
56 remove_filter( 'the_content', [ $this, 'filter_the_content_if_restricted' ], 1000 );
57 remove_filter( 'get_the_excerpt', [ $this, 'filter_the_excerpt_if_restricted' ], 1000 );
58 }
59
60 /**
61 * Filter post content when needed.
62 *
63 * NOTE: If we got this far with restricted content, this is the last attempt to protect
64 * it. This serves as the default fallback protection method if all others fail.
65 *
66 * @param string $content Content of post being checked.
67 *
68 * @return string
69 */
70 public function filter_the_content_if_restricted( $content ) {
71 $filter_name = 'content_control/restricted_post_content';
72
73 // If this isn't a post type that can be restricted, bail.
74 if ( protection_is_disabled() ) {
75 return $content;
76 }
77
78 if ( ! content_is_restricted() ) {
79 return $content;
80 }
81
82 // Ensure we don't get into an infinite loop.
83 if ( doing_filter( $filter_name ) || doing_filter( 'get_the_excerpt' ) ) {
84 return $content;
85 }
86
87 $restriction = get_applicable_restriction();
88
89 if ( false === $restriction ) {
90 return $content;
91 }
92
93 // If this is a replacement page, bail.
94 if (
95 ( 'replace' === $restriction->get_setting( 'protectionMethod' ) && 'page' === $restriction->get_setting( 'replacementType' ) && is_page( $restriction->get_setting( 'replacementPage' ) ) ) ||
96 ( 'replace_archive_page' === $restriction->get_setting( 'archiveHandling' ) && is_page( $restriction->get_setting( 'archiveReplacementPage' ) ) )
97 ) {
98 return $content;
99 }
100
101 $message = \ContentControl\get_default_denial_message();
102
103 /**
104 * If the restriction has a custom message, use it.
105 *
106 * We could check $restriction->replacement_type, but we need a safe default for
107 * all cases. Further we do content filtering for all sub queries and currently
108 * don't offer a way to override the message for those.
109 *
110 * In this way currently users can change to content replacement, set the override
111 * message, then change back to page replacement and the override message will still
112 * be used for the post in sub queries.
113 */
114 if ( $restriction->get_setting( 'overrideMessage' ) ) {
115 $message = $restriction->get_message();
116 }
117
118 /**
119 * Filter the message to display when a post is restricted.
120 *
121 * @param string $message Message to display.
122 * @param object $restriction Restriction object.
123 *
124 * @return string
125 */
126 return apply_filters(
127 $filter_name,
128 // If the default message is empty, show a generic message.
129 ! empty( $message ) ? $message : __( 'This content is restricted.', 'content-control' ),
130 $restriction
131 );
132 }
133
134 /**
135 * Filter post excerpt when needed.
136 *
137 * @param string $post_excerpt The post excerpt.
138 * @param \WP_Post $post Post object.
139 *
140 * @return string
141 */
142 public function filter_the_excerpt_if_restricted( $post_excerpt, $post = null ) {
143 $filter_name = 'content_control/restricted_post_excerpt';
144
145 // If this isn't a post type that can be restricted, bail.
146 if ( protection_is_disabled() ) {
147 return $post_excerpt;
148 }
149
150 if ( ! content_is_restricted( $post->ID ) ) {
151 return $post_excerpt;
152 }
153
154 if ( doing_filter( $filter_name ) ) {
155 return $post_excerpt;
156 }
157
158 $restriction = get_applicable_restriction();
159
160 /**
161 * Filter the excerpt to display when a post is restricted.
162 *
163 * @param string $message Message to display.
164 * @param object $restriction Restriction object.
165 *
166 * @return string
167 */
168 return apply_filters(
169 $filter_name,
170 $restriction->get_message(),
171 $restriction
172 );
173 }
174 }
1 <?php
2 /**
3 * Frontend query post restrictions.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend\Restrictions;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\query_can_be_ignored;
14 use function ContentControl\protection_is_disabled;
15 use function ContentControl\get_restriction_matches_for_queried_posts;
16
17 defined( 'ABSPATH' ) || exit;
18
19 /**
20 * Class for handling global restrictions of the query posts.
21 *
22 * @package ContentControl
23 */
24 class QueryPosts extends Controller {
25
26 /**
27 * Initiate functionality.
28 *
29 * @return void
30 */
31 public function init() {
32 // We delay this until functions.php is loaded, so that users can use the content_control/query_filter_init_hook filter.
33 // The assumption is that most code should be registered by init 999999, so we'll use that as the default.
34 add_action( 'init', [ $this, 'register_hooks' ], 999999 );
35 }
36
37 /**
38 * Register hooks.
39 *
40 * @return void
41 */
42 public function register_hooks() {
43 /**
44 * Use this filter to change the hook used to add query post filtering.
45 *
46 * This only applies to alternate queries, not the main query, and is used for removing
47 * posts from the query that are restricted.
48 *
49 * - Register earlier for more restriction coverage.
50 * - Register later for more compatibility with other plugins that late register post types.
51 *
52 * @param null|string $init_hook The hook to use to add the query post filtering.
53 * @return null|string The hook to use, should be: wp_loaded, or maybe even parse_query or wp (if you know what you're doing).
54 */
55 $init_hook = apply_filters( 'content_control/query_filter_init_hook', null );
56
57 /**
58 * Use this filter to change the priority used to add query post filtering.
59 *
60 * @param int $init_priority The priority to use to add the query post filtering.
61 * @return int The priority to use. Default: 999.
62 */
63 $init_priority = apply_filters( 'content_control/query_filter_init_priority', 999 );
64
65 if ( is_null( $init_hook ) || ! did_action( $init_hook ) ) {
66 // If the user has not specified a hook, we'll use the default (now).
67 $this->enable_query_filtering();
68 return;
69 }
70
71 add_action( (string) $init_hook, [ $this, 'enable_query_filtering' ], (int) $init_priority );
72 }
73
74 /**
75 * Late hooks.
76 *
77 * @return void
78 */
79 public function enable_query_filtering() {
80 add_filter( 'the_posts', [ $this, 'restrict_query_posts' ], 10, 2 );
81 }
82
83 /**
84 * Handle restricted content appropriately.
85 *
86 * NOTE. This is only for filtering posts, and should not
87 * be used to redirect or replace the entire page.
88 *
89 * @param \WP_Post[] $posts Array of post objects.
90 * @param \WP_Query $query The WP_Query instance (passed by reference).
91 *
92 * @return \WP_Post[]
93 */
94 public function restrict_query_posts( $posts, $query ) {
95 if ( query_can_be_ignored( $query ) ) {
96 return $posts;
97 }
98
99 if ( protection_is_disabled() ) {
100 return $posts;
101 }
102
103 $post_restrictions = get_restriction_matches_for_queried_posts( $query );
104
105 if ( false === $post_restrictions ) {
106 return $posts;
107 }
108
109 // If we have restrictions on the queried posts, handle them top down.
110 foreach ( $post_restrictions as $match ) {
111 $post_id = $match['post_ids'];
112 $restriction = $match['restriction'];
113
114 /**
115 * Use this filter to prevent a post from being restricted, or to handle it yourself.
116 *
117 * @param null|mixed $pre Whether to prevent the post from being restricted.
118 * @param null|\ContentControl\Models\Restriction $restriction Restriction object.
119 * @param int[] $post_id Post ID.
120 * @return null|mixed
121 */
122 if ( null !== apply_filters( 'content_control/pre_restrict_archive_post', null, $restriction, $post_id ) ) {
123 continue;
124 }
125
126 /**
127 * Fires when a post is restricted, but before the restriction is handled.
128 *
129 * @param \ContentControl\Models\Restriction $restriction Restriction object.
130 * @param int[] $post_id Post ID.
131 */
132 do_action( 'content_control/restrict_archive_post', $restriction, $post_id );
133
134 $handling = $query->is_main_query() ? $restriction->get_setting( 'archiveHandling' ) : $restriction->get_setting( 'additionalQueryHandling' );
135
136 switch ( $handling ) {
137 case 'filter_post_content':
138 // Filter the title/excerpt/contents of the restricted items.
139 break;
140 case 'hide':
141 foreach ( $posts as $key => $post ) {
142 if ( in_array( $post->ID, $post_id, true ) ) {
143 unset( $posts[ $key ] );
144 }
145 }
146
147 // Update the query's post count.
148 $query->post_count = count( $posts );
149 // Reset post indexes.
150 $posts = array_values( $posts );
151 break;
152 }
153 }
154
155 return $posts;
156 }
157 }
1 <?php
2 /**
3 * Frontend query post restrictions.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend\Restrictions;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\query_can_be_ignored;
14 use function ContentControl\protection_is_disabled;
15 use function ContentControl\get_restriction_matches_for_queried_terms;
16 use function ContentControl\is_rest;
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * Class for handling global restrictions of the query posts.
22 *
23 * @package ContentControl
24 */
25 class QueryTerms extends Controller {
26
27 /**
28 * Initiate functionality.
29 *
30 * @return void
31 */
32 public function init() {
33 // We delay this until functions.php is loaded, so that users can use the content_control/query_filter_init_hook filter.
34 // The assumption is that most code should be registered by init 999999, so we'll use that as the default.
35 add_action( 'init', [ $this, 'register_hooks' ], 999999 );
36 }
37
38 /**
39 * Register hooks.
40 *
41 * @return void
42 */
43 public function register_hooks() {
44 /**
45 * Use this filter to change the hook used to add query post filtering.
46 *
47 * This only applies to alternate queries, not the main query, and is used for removing
48 * posts from the query that are restricted.
49 *
50 * - Register earlier for more restriction coverage.
51 * - Register later for more compatibility with other plugins that late register post types.
52 *
53 * @param null|string $init_hook The hook to use to add the query post filtering.
54 * @return null|string The hook to use, should be: wp_loaded, or maybe even parse_query or wp (if you know what you're doing).
55 */
56 $init_hook = apply_filters( 'content_control/query_filter_init_hook', null );
57
58 /**
59 * Use this filter to change the priority used to add query post filtering.
60 *
61 * @param int $init_priority The priority to use to add the query post filtering.
62 * @return int The priority to use. Default: 999.
63 */
64 $init_priority = apply_filters( 'content_control/query_filter_init_priority', 999 );
65
66 if ( is_null( $init_hook ) || ! did_action( $init_hook ) ) {
67 // If the user has not specified a hook, we'll use the default (now).
68 $this->enable_query_filtering();
69 return;
70 }
71
72 add_action( (string) $init_hook, [ $this, 'enable_query_filtering' ], (int) $init_priority );
73 }
74
75 /**
76 * Late hooks.
77 *
78 * @return void
79 */
80 public function enable_query_filtering() {
81 add_filter( 'get_terms', [ $this, 'restrict_query_terms' ], 10, 4 );
82 }
83
84 /**
85 * Handle restricted content appropriately.
86 *
87 * NOTE. This is only for filtering terms, and should not
88 * be used to redirect or replace the entire page.
89 *
90 * @param \WP_Term[] $terms Array of terms to filter.
91 * @param string $taxonomy The taxonomy.
92 * @param array<string,mixed> $query_vars Array of query vars.
93 * @param \WP_Term_Query $query The WP_Query instance (passed by reference).
94 *
95 * @return \WP_Term[]
96 */
97 public function restrict_query_terms( $terms, $taxonomy, $query_vars, $query ) {
98 if ( query_can_be_ignored( $query ) ) {
99 return $terms;
100 }
101
102 if ( protection_is_disabled() ) {
103 return $terms;
104 }
105
106 $term_restrictions = get_restriction_matches_for_queried_terms( $query );
107
108 if ( false === $term_restrictions ) {
109 return $terms;
110 }
111
112 // If we have restrictions on the queried terms, handle them top down.
113 foreach ( $term_restrictions as $match ) {
114 $term_id = $match['term_ids'];
115 $restriction = $match['restriction'];
116
117 /**
118 * Use this filter to prevent a term from being restricted, or to handle it yourself.
119 *
120 * @param null|mixed $pre Whether to prevent the term from being restricted.
121 * @param null|\ContentControl\Models\Restriction $restriction Restriction object.
122 * @param int[] $term_id Term ID.
123 * @return null|mixed
124 */
125 if ( null !== apply_filters( 'content_control/pre_restrict_archive_term', null, $restriction, $term_id ) ) {
126 continue;
127 }
128
129 /**
130 * Fires when a term is restricted, but before the restriction is handled.
131 *
132 * @param \ContentControl\Models\Restriction $restriction Restriction object.
133 * @param int[] $term_id Perm ID.
134 */
135 do_action( 'content_control/restrict_archive_term', $restriction, $term_id );
136
137 $handling = $restriction->get_setting( 'additionalQueryHandling' );
138
139 switch ( $handling ) {
140 case 'hide':
141 foreach ( $terms as $key => $term ) {
142 if ( in_array( $term->term_id, $term_id, true ) ) {
143 unset( $terms[ $key ] );
144 }
145 }
146
147 // Reset term indexes.
148 $query->terms = array_values( $terms );
149 break;
150 }
151 }
152
153 return $terms;
154 }
155 }
1 <?php
2 /**
3 * Frontend Rest API query restrictions.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend\Restrictions;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\content_is_restricted;
14 use function ContentControl\protection_is_disabled;
15 use function ContentControl\get_applicable_restriction;
16
17 defined( 'ABSPATH' ) || exit;
18
19 /**
20 * Class for handling global restrictions of the Rest API.
21 *
22 * @package ContentControl
23 */
24 class RestAPI extends Controller {
25
26 /**
27 * Initiate functionality.
28 *
29 * @return void
30 */
31 public function init() {
32 add_filter( 'rest_pre_dispatch', [ $this, 'pre_dispatch' ], 1, 3 );
33 }
34
35 /**
36 * Handle a restriction on the rest api via pre_dispatch.
37 *
38 * @param mixed $result Response to replace the requested resource with. Can be anything a normal endpoint can return, or null to not hijack the request.
39 * @param mixed $server Server instance.
40 * @param mixed $request Request used to generate the response.
41 *
42 * @return mixed
43 */
44 public function pre_dispatch( $result, $server, $request ) { // phpcs:ignore
45 if ( protection_is_disabled() ) {
46 return $result;
47 }
48
49 if ( content_is_restricted() ) {
50 $restriction = get_applicable_restriction();
51
52 /**
53 * Fires when a post is restricted, but before the restriction is handled.
54 *
55 * @param \ContentControl\Models\Restriction $restriction Restriction object.
56 */
57 do_action( 'content_control/restrict_rest_query', $restriction );
58
59 $method = $restriction->get_setting( 'restApiQueryHandling', 'forbidden' );
60
61 switch ( $method ) {
62 // If we got here, the default is to return a rest_forbidden response.
63 case 'forbidden':
64 // Mimic a rest_forbidden response.
65 return new \WP_Error(
66 'rest_forbidden',
67 $restriction->get_setting( 'restApiQueryMessage', __( 'You do not have permission to do this.', 'content-control' ), ),
68 [ 'status' => 403 ]
69 );
70 }
71 }
72
73 return $result;
74 }
75 }
1 <?php
2 /**
3 * Frontend feed setup.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers\Frontend;
10
11 defined( 'ABSPATH' ) || exit;
12
13 use ContentControl\Base\Controller;
14
15 use WP_Customize_Manager;
16
17 use function ContentControl\is_rest;
18 use function ContentControl\protection_is_disabled;
19 use function ContentControl\user_meets_requirements;
20 use function ContentControl\Widgets\get_options as get_widget_options;
21
22 /**
23 * Class ContentControl\Frontend\Widgets
24 */
25 class Widgets extends Controller {
26
27 /**
28 * Initialize Widgets Frontend.
29 */
30 public function init() {
31 add_filter( 'sidebars_widgets', [ $this, 'exclude_widgets' ] );
32 }
33
34 /**
35 * Checks for and excludes widgets based on their chosen options.
36 *
37 * @param array<string,array<string>> $widget_areas An array of widget areas and their widgets.
38 *
39 * @return array<string,array<string>> The modified $widget_area array.
40 */
41 public function exclude_widgets( $widget_areas ) {
42 if ( is_rest() || protection_is_disabled() || $this->is_customize_preview() ) {
43 return $widget_areas;
44 }
45
46 foreach ( $widget_areas as $widget_area => $widgets ) {
47 if ( ! empty( $widgets ) && 'wp_inactive_widgets' !== $widget_area ) {
48 foreach ( $widgets as $position => $widget_id ) {
49 $options = get_widget_options( $widget_id );
50
51 // If no options, then skip this one.
52 if ( empty( $options['which_users'] ) ) {
53 continue;
54 }
55
56 // If not accessible then exclude this item.
57
58 /**
59 * Filter whether to exclude a widget.
60 *
61 * @param bool $exclude Whether to exclude the widget.
62 * @param array $options Widget options.
63 * @param string $widget_id Widget ID.
64 *
65 * @return bool
66 */
67 $exclude = apply_filters(
68 'content_control/should_exclude_widget',
69 ! user_meets_requirements( $options['which_users'], $options['roles'] ),
70 $options,
71 $widget_id
72 );
73
74 // unset non-visible item.
75 if ( $exclude ) {
76 unset( $widget_areas[ $widget_area ][ $position ] );
77 }
78 }
79 }
80 }
81
82 return $widget_areas;
83 }
84
85 /**
86 * Is customizer.
87 *
88 * @return boolean
89 */
90 public function is_customize_preview() {
91 global $wp_customize;
92
93 return ( $wp_customize instanceof WP_Customize_Manager ) && $wp_customize->is_preview();
94 }
95 }
1 <?php
2 /**
3 * Post type setup.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers;
10
11 use ContentControl\Base\Controller;
12
13 /**
14 * Post type controller.
15 */
16 class PostTypes extends Controller {
17
18 /**
19 * Init controller.
20 *
21 * @return void
22 */
23 public function init() {
24 add_action( 'init', [ $this, 'register_post_type' ] );
25 add_action( 'init', [ $this, 'register_rest_fields' ] );
26 add_action( 'save_post_cc_restriction', [ $this, 'save_post' ], 10, 3 );
27 add_filter( 'rest_pre_dispatch', [ $this, 'rest_pre_dispatch' ], 10, 3 );
28 add_filter( 'content_control/sanitize_restriction_settings', [ $this, 'sanitize_restriction_settings' ], 10, 2 );
29 add_filter( 'content_control/validate_restriction_settings', [ $this, 'validate_restriction_settings' ], 10, 2 );
30 }
31
32 /**
33 * Register `restriction` post type.
34 *
35 * @return void
36 */
37 public function register_post_type() {
38 /**
39 * Post Type: Restrictions.
40 */
41 $labels = [
42 'name' => __( 'Restrictions', 'content-control' ),
43 'singular_name' => __( 'Restriction', 'content-control' ),
44 ];
45
46 $args = [
47 'label' => __( 'Restrictions', 'content-control' ),
48 'labels' => $labels,
49 'description' => '',
50 'public' => false,
51 'publicly_queryable' => false,
52 'show_ui' => false,
53 'show_in_rest' => true,
54 'rest_base' => 'restrictions',
55 'rest_namespace' => 'content-control/v2',
56 'has_archive' => false,
57 'show_in_menu' => false,
58 'show_in_nav_menus' => false,
59 'delete_with_user' => false,
60 'exclude_from_search' => true,
61 'map_meta_cap' => true,
62 'hierarchical' => false,
63 'can_export' => true,
64 'rewrite' => false,
65 'query_var' => false,
66 'supports' => [
67 'title',
68 'excerpt',
69 // 'editor',
70 ],
71 'show_in_graphql' => false,
72 'capabilities' => [
73 'create_posts' => $this->container->get_permission( 'edit_restrictions' ),
74 'edit_posts' => $this->container->get_permission( 'edit_restrictions' ),
75 'delete_posts' => $this->container->get_permission( 'edit_restrictions' ),
76 ],
77 ];
78
79 register_post_type( 'cc_restriction', $args );
80 }
81
82 /**
83 * Registers custom REST API fields for cc_restrictions post type.
84 *
85 * @return void
86 */
87 public function register_rest_fields() {
88 register_rest_field( 'cc_restriction', 'settings', [
89 'get_callback' => function ( $obj, $field, $request ) {
90 $settings = get_post_meta( $obj['id'], 'restriction_settings', true );
91
92 // Backfill from content if empty.
93 if ( empty( $settings['customMessage'] ) ) {
94 $settings['customMessage'] = get_post_field( 'post_content', $obj['id'], 'raw' );
95 }
96
97 if ( ! empty( $settings['customMessage'] ) ) {
98 // Change output based on context.
99 $settings['customMessage'] = 'edit' === $request->get_param( 'context' ) ?
100 sanitize_post_field( 'post_content', $settings['customMessage'], $obj['id'], 'raw' ) :
101 sanitize_post_field( 'post_content', $settings['customMessage'], $obj['id'], 'display' );
102 }
103
104 return $settings;
105 },
106 'update_callback' => function ( $value, $obj ) {
107 $custom_message = ! empty( $value['customMessage'] ) ? $value['customMessage'] : '';
108
109 // Save custom message to restriction content for now.
110 wp_update_post( [
111 'ID' => $obj->ID,
112 'post_content' => $custom_message,
113 ] );
114
115 // Update the field/meta value.
116 update_post_meta( $obj->ID, 'restriction_settings', $value );
117 },
118 'schema' => [
119 'type' => 'object',
120 'arg_options' => [
121 'sanitize_callback' => function ( $settings, $request ) {
122 /**
123 * Sanitize the restriction settings.
124 *
125 * @param array<string,mixed> $settings The settings to sanitize.
126 * @param int $id The restriction ID.
127 * @param \WP_REST_Request $request The request object.
128 *
129 * @return array<string,mixed> The sanitized settings.
130 */
131 return apply_filters( 'content_control/sanitize_restriction_settings', $settings, $request->get_param( 'id' ), $request );
132 },
133 'validate_callback' => function ( $settings, $request ) {
134 /**
135 * Validate the restriction settings.
136 *
137 * @param array<string,mixed> $settings The settings to validate.
138 * @param int $id The restriction ID.
139 * @param \WP_REST_Request $request The request object.
140 *
141 * @return bool|\WP_Error True if valid, WP_Error if not.
142 */
143 return apply_filters( 'content_control/validate_restriction_settings', $settings, $request->get_param( 'id' ), $request );
144 },
145 ],
146 ],
147 'permission_callback' => function () {
148 return current_user_can( $this->container->get_permission( 'edit_restrictions' ) );
149 },
150 ] );
151
152 register_rest_field( 'cc_restriction', 'priority', [
153 'get_callback' => function ( $obj ) {
154 return (int) get_post_field( 'menu_order', $obj['id'], 'raw' );
155 },
156 'update_callback' => function ( $value, $obj ) {
157 wp_update_post( [
158 'ID' => $obj->ID,
159 'menu_order' => $value,
160 ] );
161 },
162 'permission_callback' => function () {
163 return current_user_can( $this->container->get_permission( 'edit_restrictions' ) );
164 },
165 'schema' => [
166 'type' => 'integer',
167 'arg_options' => [
168 'sanitize_callback' => function ( $priority ) {
169 return absint( $priority );
170 },
171 'validate_callback' => function ( $priority ) {
172 return is_int( $priority );
173 },
174 ],
175 ],
176 ] );
177
178 register_rest_field( 'cc_restriction', 'data_version', [
179 'get_callback' => function ( $obj ) {
180 return get_post_meta( $obj['id'], 'data_version', true );
181 },
182 'update_callback' => function ( $value, $obj ) {
183 // Update the field/meta value.
184 update_post_meta( $obj->ID, 'data_version', $value );
185 },
186 'permission_callback' => function () {
187 return current_user_can( $this->container->get_permission( 'edit_restrictions' ) );
188 },
189 ] );
190 }
191
192 /**
193 * Sanitize restriction settings.
194 *
195 * @param array<string,mixed> $settings The settings to sanitize.
196 * @param int $id The restriction ID.
197 *
198 * @return array<string,mixed> The sanitized settings.
199 */
200 public function sanitize_restriction_settings( $settings, $id ) {
201
202 // Sanitize custom message.
203 if ( ! empty( $settings['customMessage'] ) ) {
204 $settings['customMessage'] = sanitize_post_field( 'post_content', $settings['customMessage'], $id, 'db' );
205 }
206
207 return $settings;
208 }
209
210 /**
211 * Validate restriction settings.
212 *
213 * @param array<string,mixed> $settings The settings to validate.
214 * @param int $id The restriction ID.
215 *
216 * @return bool|\WP_Error True if valid, WP_Error if not.
217 */
218 public function validate_restriction_settings( $settings, $id ) {
219 // TODO Validate all known settings by type.
220 return true;
221 }
222
223
224 /**
225 * Add data version meta to new restrictions.
226 *
227 * @param int $post_id Post ID.
228 * @param \WP_Post $post Post object.
229 * @param bool $update Whether this is an existing post being updated or not.
230 *
231 * @return void
232 */
233 public function save_post( $post_id, $post, $update ) {
234 if ( $update ) {
235 return;
236 }
237
238 add_post_meta( $post_id, 'data_version', 1 );
239 }
240
241 /**
242 * Prevent access to restrictions endpoint.
243 *
244 * @param mixed $result Response to replace the requested version with.
245 * @param \WP_REST_Server $server Server instance.
246 * @param \WP_REST_Request<array<string,mixed>> $request Request used to generate the response.
247 * @return mixed
248 */
249 public function rest_pre_dispatch( $result, $server, $request ) {
250 // Get the route being requested.
251 $route = $request->get_route();
252
253 // Only proceed if we're creating a user.
254 if ( false === strpos( $route, '/content-control/v2/restrictions' ) ) {
255 return $result;
256 }
257
258 $current_user_can = current_user_can( $this->container->get_permission( 'edit_restrictions' ) );
259
260 // Prevent discovery of the endpoints data from unauthorized users.
261 if ( ! $current_user_can ) {
262 return new \WP_Error(
263 'rest_forbidden',
264 __( 'Access to this endpoint requires authorization.', 'content-control' ),
265 [
266 'status' => rest_authorization_required_code(),
267 ]
268 );
269 }
270
271 // Return data to the client to parse.
272 return $result;
273 }
274 }
1 <?php
2 /**
3 * RestAPI blocks setup.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers;
10
11 defined( 'ABSPATH' ) || exit;
12
13 use ContentControl\Base\Controller;
14
15 /**
16 * RestAPI function initialization.
17 */
18 class RestAPI extends Controller {
19 /**
20 * Initiate rest api integrations.
21 */
22 public function init() {
23 add_action( 'rest_api_init', [ $this, 'register_routes' ] );
24 }
25
26 /**
27 * Register Rest API routes.
28 *
29 * @return void
30 */
31 public function register_routes() {
32 ( new \ContentControl\RestAPI\BlockTypes() )->register_routes();
33 ( new \ContentControl\RestAPI\License() )->register_routes();
34 ( new \ContentControl\RestAPI\ObjectSearch() )->register_routes();
35 ( new \ContentControl\RestAPI\Settings() )->register_routes();
36 }
37 }
1 <?php
2 /**
3 * Shortcode setup.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 * @package ContentControl
7 */
8
9 namespace ContentControl\Controllers;
10
11 use ContentControl\Base\Controller;
12
13 use function ContentControl\user_meets_requirements;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Class Shortcodes
19 *
20 * @package ContentControl
21 */
22 class Shortcodes extends Controller {
23
24 /**
25 * Initialize Widgets
26 */
27 public function init() {
28 add_shortcode( 'content_control', [ $this, 'content_control' ] );
29 }
30
31 /**
32 * Process the [content_control] shortcode.
33 *
34 * @param array<string,string|int|null> $atts Array or shortcode attributes.
35 * @param string $content Content inside shortcode.
36 *
37 * @return string
38 */
39 public function content_control( $atts, $content = '' ) {
40 // Deprecated.
41 $deprecated_atts = shortcode_atts( [
42 'logged_out' => null, // @deprecated 2.0.
43 'roles' => null, // @deprecated 2.0.
44 ], $atts );
45
46 $atts = shortcode_atts( [
47 'status' => 'logged_in', // 'logged_in' or 'logged_out
48 'allowed_roles' => null,
49 'excluded_roles' => null,
50 'class' => '',
51 'message' => $this->container->get_option( 'defaultDenialMessage', '' ),
52 ], $this->normalize_empty_atts( $atts ), 'content_control' );
53
54 // Handle old args.
55 if ( isset( $deprecated_atts['logged_out'] ) ) {
56 $atts['status'] = (bool) $deprecated_atts['logged_out'] ? 'logged_out' : 'logged_in';
57 }
58
59 if ( isset( $deprecated_atts['roles'] ) && ! empty( $deprecated_atts['roles'] ) ) {
60 $atts['allowed_roles'] = $deprecated_atts['roles'];
61 }
62
63 $user_roles = [];
64 $match_type = 'any';
65
66 // Normalize args.
67 if ( ! empty( $atts['excluded_roles'] ) ) {
68 $user_roles = $atts['excluded_roles'];
69 $match_type = 'exclude';
70 } elseif ( ! empty( $atts['allowed_roles'] ) ) {
71 $user_roles = $atts['allowed_roles'];
72 $match_type = 'match';
73 }
74
75 // Convert classes to array.
76 $classes = ! empty( $atts['class'] ) ? explode( ' ', $atts['class'] ) : [];
77
78 $classes[] = 'content-control-container';
79 // @deprecated 2.0.0
80 $classes[] = 'jp-cc';
81
82 if ( user_meets_requirements( $atts['status'], $user_roles, $match_type ) ) {
83 $classes[] = 'content-control-accessible';
84 // @deprecated 2.0.0
85 $classes[] = 'jp-cc-accessible';
86 $container = '<div class="%1$s">%2$s</div>';
87 } else {
88 $classes[] = 'content-control-not-accessible';
89 // @deprecated 2.0.0
90 $classes[] = 'jp-cc-not-accessible';
91 $container = '<div class="%1$s">%3$s</div>';
92 }
93
94 $classes = implode( ' ', $classes );
95
96 return sprintf( $container, esc_attr( $classes ), do_shortcode( $content ), do_shortcode( $atts['message'] ) );
97 }
98
99 /**
100 * Takes set but empty attributes and sets them to true.
101 *
102 * These are typically valueless boolean attributes.
103 *
104 * @param array<string|int,string|int|null> $atts Array of shortcode attributes.
105 *
106 * @return (int|null|string|true)[]
107 *
108 * @psalm-return array<int|string, int|null|string|true>
109 */
110 public function normalize_empty_atts( $atts = [] ) {
111 if ( ! is_array( $atts ) || empty( $atts ) ) {
112 $atts = [];
113 }
114
115 foreach ( $atts as $attribute => $value ) {
116 if ( is_int( $attribute ) ) {
117 $atts[ strtolower( $value ) ] = true;
118 unset( $atts[ $attribute ] );
119 }
120 }
121
122 return $atts;
123 }
124 }
1 <?php
2 /**
3 * TrustedLogin.
4 *
5 * @copyright (c) 2023, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Controllers;
11
12 use ContentControl\Base\Controller;
13 use ContentControl\Vendor\TrustedLogin\Client;
14 use ContentControl\Vendor\TrustedLogin\Config;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * TrustedLogin.
20 *
21 * @package ContentControl
22 */
23 class TrustedLogin extends Controller {
24
25 /**
26 * TrustedLogin init.
27 */
28 public function init() {
29 $this->hooks();
30
31 $config = [
32 'auth' => [
33 'api_key' => 'f97f5be6e02d1565',
34 'license_key' => $this->container->get( 'license' )->get_license_key(),
35 ],
36 'vendor' => [
37 'namespace' => 'content-control',
38 'title' => 'Content Control',
39 'display_name' => 'Content Control Support',
40 'logo_url' => $this->container->get_url( 'assets/images/logo.svg' ),
41 'email' => 'support+{hash}@contentcontrolplugin.com',
42 'website' => 'https://contentcontrolplugin.com?utm_campaign=grant-access&utm_source=plugin-settings-page&utm_medium=plugin-ui&utm_content=grant-access-title-link',
43 'support_url' => 'https://contentcontrolplugin.com/support/?utm_campaign=grant-access&utm_source=plugin-settings-page&utm_medium=plugin-ui&utm_content=support-footer-link',
44 ],
45 'role' => 'administrator',
46 'caps' => [
47 'add' => [
48 $this->container->get_permission( 'manage_settings' ) => __( 'This allows us to check your global restrictions and plugin settings.', 'content-control' ),
49 $this->container->get_permission( 'edit_block_controls' ) => __( 'This allows us to check your block control settings.', 'content-control' ),
50 ],
51 'remove' => [
52 'delete_published_pages' => 'Your published posts cannot and will not be deleted by support staff',
53 'manage_woocommerce' => 'We don\'t need to manage your shop!',
54 ],
55 ],
56 'decay' => WEEK_IN_SECONDS,
57 'menu' => [
58 'slug' => false,
59 ],
60 'logging' => [
61 'enabled' => false,
62 ],
63 'require_ssl' => false,
64 'webhook' => [
65 'url' => null,
66 'debug_data' => false,
67 'create_ticket' => false,
68 ],
69 'paths' => [
70 'js' => $this->container->get_url( 'vendor-prefixed/trustedlogin/client/src/assets/trustedlogin.js' ),
71 'css' => $this->container->get_url( 'dist/settings-page.css' ),
72 ],
73 ];
74
75 try {
76 new Client(
77 new Config( $config )
78 );
79 } catch ( \Exception $exception ) {
80 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
81 \error_log( $exception->getMessage() );
82 }
83 }
84
85 /**
86 * Hooks.
87 *
88 * @return void
89 */
90 public function hooks() {
91 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
92 }
93
94 /**
95 * Admin menu.
96 *
97 * @return void
98 */
99 public function admin_menu() {
100 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
101 if ( ! isset( $_GET['page'] ) || 'grant-content-control-access' !== $_GET['page'] ) {
102 return;
103 }
104
105 add_options_page(
106 __( 'Content Control Support Access', 'content-control' ),
107 __( 'Content Control Support Access', 'content-control' ),
108 $this->container->get_permission( 'manage_settings' ),
109 'grant-content-control-access',
110 function () {
111 // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
112 do_action( 'trustedlogin/content-control/auth_screen' );
113 }
114 );
115 }
116 }
1 <?php
2 /**
3 * Install_Skin class.
4 *
5 * @package ContentControl
6 * @subpackage Installers
7 * @since 2.0.0
8 */
9
10 namespace ContentControl\Installers;
11
12 /**
13 * Skin for on-the-fly addon installations.
14 *
15 * @since 1.0.0
16 * @since 2.0.0 Extend PluginSilentUpgraderSkin and clean up the class.
17 */
18 class Install_Skin extends PluginSilentUpgraderSkin {
19
20 /**
21 * Instead of outputting HTML for errors, json_encode the errors and send them
22 * back to the Ajax script for processing.
23 *
24 * @since 2.0.0
25 *
26 * @param string|\WP_Error $errors Array of errors with the install process.
27 */
28 public function error( $errors ) {
29 if ( ! empty( $errors ) ) {
30 return wp_send_json_error( $errors );
31 }
32
33 return $errors;
34 }
35 }
1 <?php
2 /**
3 * PluginSilentUpgraderSkin class.
4 *
5 * @package ContentControl
6 * @subpackage Installers
7 * @since 2.0.0
8 */
9
10 namespace ContentControl\Installers;
11
12 /** \WP_Upgrader_Skin class */
13 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
14
15 /**
16 * Class PluginSilentUpgraderSkin.
17 *
18 * @internal Please do not use this class outside of core plugin development. May be removed at any time.
19 *
20 * @since 2.0.0
21 */
22 class PluginSilentUpgraderSkin extends \WP_Upgrader_Skin {
23
24 /**
25 * Empty out the header of its HTML content and only check to see if it has
26 * been performed or not.
27 *
28 * @return void
29 */
30 public function header() {
31 }
32
33 /**
34 * Empty out the footer of its HTML contents.
35 *
36 * @return void
37 */
38 public function footer() {
39 }
40
41 /**
42 * Instead of outputting HTML for errors, just return them.
43 * Ajax request will just ignore it.
44 *
45 * @param string|\WP_Error $errors Array of errors with the install process.
46 *
47 * @return string|\WP_Error
48 */
49 public function error( $errors ) {
50 return $errors;
51 }
52
53 /**
54 * Empty out JavaScript output that calls function to decrement the update counts.
55 *
56 * @param string $type Type of update count to decrement.
57 *
58 * @return void
59 */
60 public function decrement_update_count( $type ) {
61 }
62 }
1 <?php
2 /**
3 * Plugin container.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Interfaces;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Localized controller class.
16 */
17 interface Controller {
18
19 /**
20 * Handle hooks & filters or various other init tasks.
21 *
22 * @return void
23 */
24 public function init();
25
26 /**
27 * Check if controller is enabled.
28 *
29 * @return bool
30 */
31 public function controller_enabled();
32 }
1 <?php
2 /**
3 * Plugin upgrade.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl
8 */
9
10 namespace ContentControl\Interfaces;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Localized controller class.
16 */
17 interface Upgrade {
18
19 /**
20 * Return label for this upgrade.
21 *
22 * @return string
23 */
24 public function label();
25
26 /**
27 * Return full description for this upgrade.
28 *
29 * @return string
30 */
31 public function description();
32
33 /**
34 * Check if this upgrade is required.
35 *
36 * @return bool
37 */
38 public function is_required();
39
40 /**
41 * Check if prerequisites are met.
42 *
43 * @return bool
44 */
45 public function prerequisites_met();
46
47 /**
48 * Run the upgrade.
49 *
50 * @return void|\WP_Error|false
51 */
52 public function run();
53 }
1 <?php
2 /**
3 * Restriction model.
4 *
5 * @package ContentControl\RuleEngine
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models;
10
11 use ContentControl\Models\RuleEngine\Query;
12
13 use function ContentControl\fetch_key_from_array;
14 use function ContentControl\get_default_restriction_settings;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Model for restriction sets.
20 *
21 * @version 3.0.0
22 * @since 2.1.0
23 *
24 * @package ContentControl\Models
25 */
26 class Restriction {
27
28 /**
29 * Current model version.
30 *
31 * @var int
32 */
33 const MODEL_VERSION = 3;
34
35 /**
36 * Post object.
37 *
38 * @var \WP_Post
39 */
40 private $post;
41
42 /**
43 * Restriction id.
44 *
45 * @var int
46 */
47 public $id = 0;
48
49 /**
50 * Restriction slug.
51 *
52 * @var string
53 */
54 public $slug;
55
56 /**
57 * Restriction label.
58 *
59 * @var string
60 */
61 public $title;
62
63 /**
64 * Restriction description.
65 *
66 * @var string|null
67 */
68 public $description;
69
70 /**
71 * Restriction Message.
72 *
73 * @var string|null
74 */
75 public $message;
76
77 /**
78 * Restriction status.
79 *
80 * @var string
81 */
82 public $status;
83
84 /**
85 * Restriction priority.
86 *
87 * @var int
88 */
89 public $priority;
90
91 /**
92 * Restriction Condition Query.
93 *
94 * @var Query
95 */
96 public $query;
97
98 /**
99 * Restriction Settings.
100 *
101 * @var array<string,mixed>
102 */
103 public $settings;
104
105 /**
106 * Data version.
107 *
108 * @var int
109 */
110 public $data_version;
111
112 /**
113 * Build a restriction.
114 *
115 * @param \WP_Post|array<string,mixed> $restriction Restriction data.
116 */
117 public function __construct( $restriction ) {
118 if ( ! is_a( $restriction, '\WP_Post' ) ) {
119 $this->setup_v1_restriction( $restriction );
120 } else {
121 $this->post = $restriction;
122
123 /**
124 * Restriction settings.
125 *
126 * @var array<string,mixed>|false $settings
127 */
128 $settings = get_post_meta( $restriction->ID, 'restriction_settings', true );
129
130 if ( ! $settings ) {
131 $settings = [];
132 }
133
134 $settings = wp_parse_args(
135 $settings,
136 get_default_restriction_settings()
137 );
138
139 $this->settings = $settings;
140
141 $properties = [
142 'id' => $restriction->ID,
143 'slug' => $restriction->post_name,
144 'title' => $restriction->post_title,
145 'status' => $restriction->post_status,
146 'priority' => $restriction->menu_order,
147 // We set this late.. on first use.
148 'description' => null,
149 'message' => null,
150 ];
151
152 foreach ( $properties as $key => $value ) {
153 $this->$key = $value;
154 }
155
156 $this->data_version = get_post_meta( $restriction->ID, 'data_version', true );
157
158 if ( ! $this->data_version ) {
159 $this->data_version = 2;
160 update_post_meta( $restriction->ID, 'data_version', 2 );
161 }
162
163 $this->query = new Query( $this->get_setting( 'conditions' ) );
164 }
165 }
166
167 /**
168 * Map old v1 restriction to new v2 restriction object.
169 *
170 * @param array<string,mixed> $restriction Restriction data.
171 *
172 * @return void
173 */
174 public function setup_v1_restriction( $restriction ) {
175 static $index = 0;
176
177 $restriction = \wp_parse_args( $restriction, [
178 'title' => '',
179 'who' => '',
180 'roles' => [],
181 'protection_method' => 'redirect',
182 'show_excerpts' => false,
183 'override_default_message' => false,
184 'custom_message' => '',
185 'redirect_type' => 'login',
186 'redirect_url' => '',
187 'conditions' => '',
188 ] );
189
190 $this->data_version = 1;
191
192 $this->id = 0;
193 $this->slug = '';
194 $this->title = $restriction['title'];
195 $this->description = '';
196 $this->status = 'publish';
197 $this->priority = $index;
198
199 $user_roles = is_array( $restriction['roles'] ) ? $restriction['roles'] : [];
200
201 $settings = [
202 'userStatus' => $restriction['who'],
203 'roleMatch' => count( $user_roles ) > 0 ? 'match' : 'any',
204 'userRoles' => $user_roles,
205 'protectionMethod' => 'custom_message' === $restriction['protection_method'] ? 'replace' : 'redirect',
206 'redirectType' => $restriction['redirect_type'],
207 'redirectUrl' => $restriction['redirect_url'],
208 'replacementType' => 'message',
209 'replacementPage' => 0,
210 'archiveHandling' => 'filter_post_content',
211 'archiveReplacementPage' => 0,
212 'archiveRedirectType' => $restriction['redirect_type'],
213 'archiveRedirectUrl' => $restriction['redirect_url'],
214 'additionalQueryHandling' => 'filter_post_content',
215 'overrideMessage' => $restriction['override_default_message'],
216 'customMessage' => $restriction['custom_message'],
217 'showExcerpts' => $restriction['show_excerpts'],
218 'conditions' => \ContentControl\remap_conditions_to_query( $restriction['conditions'] ),
219 ];
220
221 $this->settings = $settings;
222
223 $this->query = new Query( $settings['conditions'] );
224
225 ++$index;
226 }
227
228 /**
229 * Get the restriction settings array.
230 *
231 * @return array<string,mixed>
232 */
233 public function get_settings() {
234 return $this->settings;
235 }
236
237 /**
238 * Get a restriction setting.
239 *
240 * Settings are stored in JS based camelCase. But WP prefers snake_case.
241 *
242 * This method supports camelCase based dot.notation, as well as snake_case.
243 *
244 * @param string $key Setting key.
245 * @param mixed $default_value Default value.
246 *
247 * @return mixed|false
248 */
249 public function get_setting( $key, $default_value = false ) {
250 // Support camelCase, snake_case, and dot.notation.
251 // Check for camelKeys & dot.notation.
252 $value = \ContentControl\fetch_key_from_array( $key, $this->settings, 'camelCase' );
253
254 if ( null === $value ) {
255 $value = $default_value;
256 }
257
258 /**
259 * Filter the option.
260 *
261 * @param mixed $value Option value.
262 * @param string $key Option key.
263 * @param mixed $default_value Default value.
264 * @param int $restriction_id Restriction ID.
265 *
266 * @return mixed
267 */
268 return apply_filters( 'content_control/get_restriction_setting', $value, $key, $default_value, $this->id );
269 }
270
271 /**
272 * Check if this set has JS based rules.
273 *
274 * @return bool
275 */
276 public function has_js_rules() {
277 return $this->query->has_js_rules();
278 }
279
280 /**
281 * Check this sets rules.
282 *
283 * @return bool
284 */
285 public function check_rules() {
286 if ( ! $this->query->has_rules() ) {
287 // No rules should be treated as no restrictions.
288 return false;
289 }
290
291 return $this->query->check_rules();
292 }
293
294 /**
295 * Check if this restriction applies to the current user.
296 *
297 * @return bool
298 */
299 public function user_meets_requirements() {
300 // Filter to allow override user status check early.
301 $bypass = \apply_filters( 'content_control/restriction/bypass_user_requirements', null, $this );
302
303 if ( null !== $bypass ) {
304 return $bypass;
305 }
306
307 return \ContentControl\user_meets_requirements( $this->get_setting( 'userStatus' ), $this->get_setting( 'userRoles' ), $this->get_setting( 'roleMatch' ) );
308 }
309
310 /**
311 * Get the description for this restriction.
312 *
313 * @return string
314 */
315 public function get_description() {
316 if ( ! isset( $this->description ) ) {
317 $this->description = get_the_excerpt( $this->id );
318
319 if ( empty( $this->description ) ) {
320 $this->description = __( 'This content is restricted.', 'content-control' );
321 }
322 }
323
324 return $this->description;
325 }
326
327 /**
328 * Get the message for this restriction.
329 *
330 * @uses \get_the_content()
331 * @uses \ContentControl\get_default_denial_message()
332 *
333 * @param string $context Context. 'display' or 'raw'.
334 *
335 * @return string
336 */
337 public function get_message( $context = 'display' ) {
338 if ( ! isset( $this->message ) ) {
339 $message = '';
340
341 if ( ! empty( $this->get_setting( 'customMessage' ) ) ) {
342 $message = $this->get_setting( 'customMessage' );
343 } elseif ( ! empty( $this->post->post_content ) ) {
344 $message = 'display' === $context
345 ? \get_the_content( null, false, $this->id )
346 : $this->post->post_content;
347 }
348
349 $this->message = $message;
350 }
351
352 return sanitize_post_field( 'post_content', $this->message, $this->id, $context );
353 }
354
355 /**
356 * Whether to show excerpts for posts that are restricted.
357 *
358 * @return bool
359 */
360 public function show_excerpts() {
361 return (bool) $this->get_setting( 'showExcerpts' );
362 }
363
364 /**
365 * Check if this uses the redirect method.
366 *
367 * @return bool
368 */
369 public function uses_redirect_method() {
370 return 'redirect' === $this->get_setting( 'protectionMethod' );
371 }
372
373 /**
374 * Check if this uses the replace method.
375 *
376 * @return bool
377 */
378 public function uses_replace_method() {
379 return 'replace' === $this->get_setting( 'protectionMethod' );
380 }
381
382 /**
383 * Get edit link.
384 *
385 * @return string
386 */
387 public function get_edit_link() {
388 if ( current_user_can( 'edit_post', $this->id ) ) {
389 return admin_url( 'options-general.php?page=content-control-settings&view=restrictions&edit=' . $this->id );
390 }
391
392 return '';
393 }
394
395 /**
396 * Convert this restriction to an array.
397 *
398 * @return array<string,mixed>
399 */
400 public function to_array() {
401 $settings = $this->get_settings();
402
403 return array_merge( [
404 'id' => $this->id,
405 'slug' => $this->slug,
406 'title' => $this->title,
407 'description' => $this->get_description(),
408 'message' => $this->get_message(),
409 'status' => $this->status,
410 'priority' => $this->priority,
411 ], $settings );
412 }
413
414 /**
415 * Convert this restriction to a v1 array.
416 *
417 * @return array<string,mixed>
418 */
419 public function to_v1_array() {
420 return [
421 'id' => $this->id,
422 'title' => $this->title,
423 'who' => $this->get_setting( 'userStatus' ),
424 'roles' => $this->get_setting( 'userRoles' ),
425 'protection_method' => $this->get_setting( 'protectionMethod' ),
426 'show_excerpts' => $this->get_setting( 'showExcerpts' ),
427 'override_default_message' => $this->get_setting( 'overrideMessage' ),
428 'custom_message' => $this->get_setting( 'customMessage' ),
429 'redirect_type' => $this->get_setting( 'redirectType' ),
430 'redirect_url' => $this->get_setting( 'redirectUrl' ),
431 'conditions' => $this->get_setting( 'conditions' ),
432 ];
433 }
434 }
1 <?php
2 /**
3 * Rule engine group model.
4 *
5 * @package ContentControl
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models\RuleEngine;
10
11 /**
12 * Handler for condition groups.
13 *
14 * @package ContentControl
15 */
16 class Group extends Item {
17
18 /**
19 * Group id.
20 *
21 * @var string
22 */
23 public $id;
24
25 /**
26 * Group label.
27 *
28 * @var string
29 */
30 public $label;
31
32 /**
33 * Group query.
34 *
35 * @var Query
36 */
37 public $query;
38
39 /**
40 * Build a group.
41 *
42 * @param array{id:string,label:string,query:array<mixed>} $group Group data.
43 */
44 public function __construct( $group ) {
45 $group = wp_parse_args( $group, [
46 'id' => '',
47 'label' => '',
48 'query' => [],
49 ]);
50
51 $this->id = $group['id'];
52 $this->label = $group['label'];
53 $this->query = new Query( $group['query'] );
54 }
55
56 /**
57 * Check if this group has JS based rules.
58 *
59 * @return bool
60 */
61 public function has_js_rules() {
62 return $this->query->has_js_rules();
63 }
64
65 /**
66 * Check this groups rules.
67 *
68 * @return bool
69 */
70 public function check_rules() {
71 return $this->query->check_rules();
72 }
73
74 /**
75 * Check this groups rules.
76 *
77 * @return array<bool|null|array<bool|null>>
78 */
79 public function get_checks() {
80 return $this->query->get_checks();
81 }
82
83 /**
84 * Return the rule check as an array of information.
85 *
86 * Useful for debugging.
87 *
88 * @return array<mixed>
89 */
90 public function get_check_info() {
91 return $this->query->get_check_info();
92 }
93 }
1 <?php
2 /**
3 * Rule engine item model.
4 *
5 * @package ContentControl
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models\RuleEngine;
10
11 /**
12 * Handler for condition items.
13 *
14 * @package ContentControl
15 */
16 abstract class Item {
17
18 /**
19 * Item id.
20 *
21 * @var string
22 */
23 public $id;
24
25 /**
26 * Return the checks as an array of information.
27 *
28 * Useful for debugging.
29 *
30 * @return array<mixed>
31 */
32 abstract public function get_check_info();
33 }
1 <?php
2 /**
3 * Rule engine query model.
4 *
5 * @package ContentControl
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models\RuleEngine;
10
11 /**
12 * Handler for condition queries.
13 *
14 * @package ContentControl
15 */
16 class Query {
17
18 /**
19 * Query logical comparison operator.
20 *
21 * @var string `and` | `or`
22 */
23 public $logical_operator;
24
25 /**
26 * Query items.
27 *
28 * @var Item[]
29 */
30 public $items;
31
32 /**
33 * Build a query.
34 *
35 * @param array{logicalOperator:string,items:array<mixed>} $query Query data.
36 */
37 public function __construct( $query ) {
38 $query = wp_parse_args( $query, [
39 'logicalOperator' => 'and',
40 'items' => [],
41 ]);
42
43 $this->logical_operator = $query['logicalOperator'];
44 $this->items = [];
45
46 foreach ( $query['items'] as $item ) {
47 $is_group = ( isset( $item['type'] ) && 'group' === $item['type'] )
48 || isset( $item['query'] );
49
50 $this->items[] = $is_group ? new Group( $item ) : new Rule( $item );
51 }
52 }
53
54 /**
55 * Check if this query has any rules.
56 *
57 * @return bool
58 */
59 public function has_rules() {
60 return ! empty( $this->items );
61 }
62
63 /**
64 * Check if this query has JS based rules.
65 *
66 * @return bool
67 */
68 public function has_js_rules() {
69 foreach ( $this->items as $item ) {
70 if ( $item instanceof Rule ) {
71 if ( $item->is_js_rule() ) {
72 return true;
73 }
74 } elseif ( $item instanceof Group ) {
75 if ( $item->has_js_rules() ) {
76 return true;
77 }
78 }
79 }
80
81 return false;
82 }
83
84 /**
85 * Check rules in a recursive nested pattern.
86 *
87 * @return bool
88 */
89 public function check_rules() {
90 $checks = [];
91
92 if ( empty( $this->items ) ) {
93 return true;
94 }
95
96 foreach ( $this->items as $item ) {
97 // Missing rules should result in restricted content.
98 $result = false;
99
100 if ( $item instanceof Rule ) {
101 $result = $item->check_rule();
102 } elseif ( $item instanceof Group ) {
103 $result = $item->check_rules();
104 }
105
106 $checks[] = $result;
107
108 // Bail as early as we can.
109 if (
110 // If we have a true result and are using `or`.
111 ( true === $result && 'or' === $this->logical_operator ) ||
112 // If we have a false result and are using `and`.
113 ( false === $result && 'and' === $this->logical_operator )
114 ) {
115 break;
116 }
117 }
118
119 /*
120 * This method ignores null values (JS conditions),
121 * if changed, null needs to be accounted for.
122 */
123 if ( 'or' === $this->logical_operator ) {
124 // If any values are true or null, return true.
125 return in_array( true, $checks, true );
126 } else {
127 // If any values are false, return false.
128 return ! in_array( false, $checks, true );
129 }
130 }
131
132 /**
133 * Return the checks as an array.
134 *
135 * Useful for debugging or passing to JS.
136 *
137 * @return array<bool|null|array<bool|null>>
138 */
139 public function get_checks() {
140 $checks = [];
141
142 foreach ( $this->items as $item ) {
143 if ( $item instanceof Rule ) {
144 $checks[] = $item->get_check();
145 } elseif ( $item instanceof Group ) {
146 $checks[] = $item->get_checks();
147 }
148 }
149
150 return $checks;
151 }
152
153 /**
154 * Return the checks as an array of information.
155 *
156 * Useful for debugging.
157 *
158 * @return array<mixed>
159 */
160 public function get_check_info() {
161 $checks = [];
162
163 foreach ( $this->items as $key => $item ) {
164 $checks[ $key ] = $item->get_check_info();
165 }
166
167 return $checks;
168 }
169 }
1 <?php
2 /**
3 * Rule engine rule model.
4 *
5 * @package ContentControl
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models\RuleEngine;
10
11 use function ContentControl\plugin;
12 use function ContentControl\Rules\current_rule;
13
14 /**
15 * Handler for condition rules.
16 *
17 * @package ContentControl
18 */
19 class Rule extends Item {
20
21 /**
22 * Unique Hash ID.
23 *
24 * @var string
25 */
26 public $id;
27
28 /**
29 * Rule name.
30 *
31 * @var string
32 */
33 public $name;
34
35 /**
36 * Rule options.
37 *
38 * @var array<string,mixed>
39 */
40 public $options;
41
42 /**
43 * Rule not operand.
44 *
45 * @var boolean
46 */
47 public $not_operand;
48
49 /**
50 * Rule extras.
51 *
52 * Such as post type or taxnomy like meta.
53 *
54 * @var array<string,mixed>
55 */
56 public $extras = [];
57
58 /**
59 * Rule is frontend only.
60 *
61 * @var boolean
62 */
63 public $frontend_only = false;
64
65 /**
66 * Rule definition.
67 *
68 * @var array<string,mixed>|null
69 */
70 public $definition;
71
72 /**
73 * Rule is deprecated.
74 *
75 * @var boolean
76 */
77 public $deprecated = false;
78
79 /**
80 * Build a rule.
81 *
82 * @param array{id:string,name:string,notOperand:bool,options:array<string,mixed>,extras:array<string,mixed>} $rule Rule data.
83 */
84 public function __construct( $rule ) {
85 $rule = wp_parse_args( $rule, [
86 'id' => '',
87 'name' => '',
88 'notOperand' => false,
89 'options' => [],
90 'extras' => [],
91 ]);
92
93 if ( isset( $rule['deprecated'] ) ) {
94 $this->deprecated = $rule['deprecated'];
95 }
96
97 $name = $rule['name'];
98
99 $this->definition = plugin( 'rules' )->get_rule( $name );
100
101 if ( ! $this->definition ) {
102 /* translators: 1. Rule name. */
103 plugin( 'logging' )->log_unique( 'ERROR: ' . sprintf( __( 'Rule `%s` not found.', 'content-control' ), $name ) );
104 }
105
106 $extras = isset( $this->definition['extras'] ) ? $this->definition['extras'] : [];
107
108 $this->id = $rule['id'];
109 $this->name = $name;
110 $this->not_operand = $rule['notOperand'];
111 $this->frontend_only = isset( $this->definition['frontend'] ) ? $this->definition['frontend'] : false;
112 $this->options = $this->parse_options( $rule['options'] );
113 $this->extras = array_merge( $extras, $rule['extras'] );
114 }
115
116 /**
117 * Parse rule options based on rule definitions.
118 *
119 * @param array<string,mixed> $options Array of rule opions.
120 * @return array<string,mixed>
121 */
122 public function parse_options( $options = [] ) {
123 return $options;
124 }
125
126 /**
127 * Check the results of this rule.
128 *
129 * @return bool
130 */
131 public function check_rule() {
132 if ( $this->is_js_rule() ) {
133 return true;
134 }
135
136 $check = $this->run_check();
137
138 return $this->not_operand ? ! $check : $check;
139 }
140
141 /**
142 * Check the results of this rule.
143 *
144 * @return bool True if rule passes, false if not.
145 */
146 private function run_check() {
147 $callback = isset( $this->definition['callback'] ) ? $this->definition['callback'] : null;
148
149 if ( ! $callback ) {
150 /* translators: 1. Rule name. */
151 plugin( 'logging' )->log_unique( 'ERROR: ' . esc_html( sprintf( __( 'Rule `%s` has no callback.', 'content-control' ), $this->name ) ) );
152 return false;
153 }
154
155 if ( ! is_callable( $callback ) ) {
156 /* translators: 1. Rule name. 2. Callback name. */
157 plugin( 'logging' )->log_unique( 'ERROR: ' . esc_html( sprintf( __( 'Rule `%1$s` callback is not callable (%2$s).', 'content-control' ), $this->name, $callback ) ) );
158 return false;
159 }
160
161 // Set global current rule so it can be easily accessed.
162 current_rule( $this );
163
164 if ( $this->deprecated ) {
165 $settings = [
166 'target' => $this->name,
167 'settings' => $this->options,
168 ];
169
170 // Old rules had the settings passed as the first argument.
171 $check = call_user_func( $callback, $settings );
172 } else {
173 /**
174 * All rule options can be accessed via the global.
175 *
176 * @see \ContentControl\Rules\current_rule()
177 */
178 $check = call_user_func( $callback );
179 }
180
181 // Clear global current rule.
182 current_rule( null );
183
184 return $check;
185 }
186
187 /**
188 * Check if this rule's callback is based in JS rather than PHP.
189 *
190 * @return bool
191 */
192 public function is_js_rule() {
193 return $this->frontend_only;
194 }
195
196 /**
197 * Return the rule check as boolean or null if the rule is JS based.
198 *
199 * @return bool|null
200 */
201 public function get_check() {
202 if ( $this->is_js_rule() ) {
203 return null;
204 }
205
206 return $this->run_check();
207 }
208
209 /**
210 * Return the rule check as an array of information.
211 *
212 * Useful for debugging.
213 *
214 * @return array<string,mixed>|null
215 */
216 public function get_check_info() {
217 if ( $this->is_js_rule() ) {
218 return null;
219 }
220
221 return [
222 'result' => $this->run_check(),
223 'id' => $this->id,
224 'rule' => $this->name,
225 'not' => $this->not_operand,
226 'args' => $this->options,
227 'def' => $this->definition,
228 ];
229 }
230 }
1 <?php
2 /**
3 * Rule engine set model.
4 *
5 * @package ContentControl
6 * @subpackage Models
7 */
8
9 namespace ContentControl\Models\RuleEngine;
10
11 /**
12 * Handler for condition sets.
13 *
14 * @package ContentControl
15 */
16 class Set {
17
18 /**
19 * Set id.
20 *
21 * @var string
22 */
23 public $id;
24
25 /**
26 * Set label.
27 *
28 * @var string
29 */
30 public $label;
31
32 /**
33 * Set query.
34 *
35 * @var Query
36 */
37 public $query;
38
39 /**
40 * Build a set.
41 *
42 * @param array{id:string,label:string,query:array<mixed>} $set Set data.
43 */
44 public function __construct( $set ) {
45 $set = wp_parse_args( $set, [
46 'id' => '',
47 'label' => '',
48 'query' => [],
49 ]);
50
51 $this->id = $set['id'];
52 $this->label = $set['label'];
53 $this->query = new Query( $set['query'] );
54 }
55
56 /**
57 * Check if this set has JS based rules.
58 *
59 * @return bool
60 */
61 public function has_js_rules() {
62 return $this->query->has_js_rules();
63 }
64
65 /**
66 * Check this sets rules.
67 *
68 * @return bool
69 */
70 public function check_rules() {
71 return $this->query->check_rules();
72 }
73
74 /**
75 * Get the check array for further post processing.
76 *
77 * @return array<bool|null|array<bool|null>>
78 */
79 public function get_checks() {
80 return $this->query->get_checks();
81 }
82
83 /**
84 * Return the checks as an array of information.
85 *
86 * Useful for debugging.
87 *
88 * @return array<string,mixed>
89 */
90 public function get_check_info() {
91 return $this->query->get_check_info();
92 }
93 }
1 <?php
2 /**
3 * Includes the composer Autoloader used for packages and classes in the classes/ directory.
4 *
5 * @package ContentControl\Plugin
6 */
7
8 namespace ContentControl\Plugin;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Autoloader class.
14 */
15 class Autoloader {
16
17 /**
18 * Static-only class.
19 */
20 private function __construct() {
21 }
22
23 /**
24 * Require the autoloader and return the result.
25 *
26 * If the autoloader is not present, let's log the failure and display a nice admin notice.
27 *
28 * @param string $name Plugin name for error messaging.
29 * @param string $path Path to the plugin.
30 *
31 * @return boolean
32 */
33 public static function init( $name = '', $path = '' ) {
34 $autoloader = $path . '/vendor/autoload.php';
35
36 if ( ! \is_readable( $autoloader ) ) {
37 self::missing_autoloader( $name );
38
39 return false;
40 }
41
42 require_once $autoloader;
43
44 return true;
45 }
46
47 /**
48 * If the autoloader is missing, add an admin notice.
49 *
50 * @param string $plugin_name Plugin name for error messaging.
51 *
52 * @return void
53 */
54 protected static function missing_autoloader( $plugin_name = '' ) {
55 /* translators: 1. Plugin name */
56 $text = __( 'Your installation of %1$s is incomplete. If you installed %1$s from GitHub, please refer to this document to set up your development environment.', 'content-control' );
57
58 $message = sprintf( $text, $plugin_name );
59
60 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
61 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
62 error_log(
63 esc_html( $message )
64 );
65 }
66
67 add_action(
68 'admin_notices',
69 function () use ( $message ) {
70 ?>
71 <div class="notice notice-error">
72 <p><?php echo esc_html( $message ); ?></p>
73 </div>
74 <?php
75 }
76 );
77 }
78 }
1 <?php
2 /**
3 * Plugin installer.
4 *
5 * @copyright (c) 2021, Code Atlantic LLC.
6 *
7 * @package ContentControl\Plugin
8 */
9
10 namespace ContentControl\Plugin;
11
12 use function ContentControl\plugin;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Class Install
18 *
19 * @since 1.0.0
20 */
21 class Install {
22
23 /**
24 * Activation wrapper.
25 *
26 * @param bool $network_wide Weather to activate network wide.
27 *
28 * @return void
29 */
30 public static function activate_plugin( $network_wide ) {
31 self::do_multisite( $network_wide, [ __CLASS__, 'activate_site' ] );
32 }
33
34 /**
35 * Deactivation wrapper.
36 *
37 * @param bool $network_wide Weather to deactivate network wide.
38 *
39 * @return void
40 */
41 public static function deactivate_plugin( $network_wide ) {
42 self::do_multisite( $network_wide, [ __CLASS__, 'deactivate_site' ] );
43 }
44
45 /**
46 * Uninstall the plugin.
47 *
48 * @return void
49 */
50 public static function uninstall_plugin() {
51 self::do_multisite( true, [ __CLASS__, 'uninstall_site' ] );
52 }
53
54 /**
55 * Handle single & multisite processes.
56 *
57 * @param bool $network_wide Weather to do it network wide.
58 * @param callable $method Callable method for each site.
59 * @param array<string,mixed> $args Array of extra args.
60 *
61 * @return void
62 */
63 private static function do_multisite( $network_wide, $method, $args = [] ) {
64 global $wpdb;
65
66 if ( is_multisite() && $network_wide ) {
67 $activated = get_site_option( 'content_control_activated', [] );
68
69 /* phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery */
70 $blog_ids = $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" );
71
72 // Try to reduce the chances of a timeout with a large number of sites.
73 if ( \count( $blog_ids ) > 2 ) {
74 ignore_user_abort( true );
75
76 if ( ! \ContentControl\is_func_disabled( 'set_time_limit' ) ) {
77 /* phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged */
78 @set_time_limit( 0 );
79 }
80 }
81
82 foreach ( $blog_ids as $blog_id ) {
83 switch_to_blog( $blog_id );
84 call_user_func_array( $method, [ $args ] );
85
86 $activated[] = $blog_id;
87
88 restore_current_blog();
89 }
90
91 update_site_option( 'content_control_activated', $activated );
92 } else {
93 call_user_func_array( $method, [ $args ] );
94 }
95 }
96
97 /**
98 * Activate on single site.
99 *
100 * @return void
101 */
102 public static function activate_site() {
103 // Add a temporary option that will fire a hookable action on next load.
104 \set_transient( '_content_control_installed', true, 3600 );
105
106 $version = plugin()->get( 'version' );
107
108 // Add version info.
109 \add_option( 'content_control_version', [
110 'version' => $version,
111 'upgraded_from' => null,
112 'initial_version' => $version,
113 'installed_on' => gmdate( 'Y-m-d H:i:s' ),
114 ] );
115
116 // Add data versions if missing.
117 \add_option( 'content_control_data_versioning', \ContentControl\current_data_versions() );
118 }
119
120 /**
121 * Deactivate on single site.
122 *
123 * @return void
124 */
125 public static function deactivate_site() {
126 }
127
128 /**
129 * Uninstall single site.
130 *
131 * @return void
132 */
133 public static function uninstall_site() {
134 }
135 }
1 <?php
2 /**
3 * Logging class.
4 *
5 * @package ContentControl\Plugin
6 */
7
8 namespace ContentControl\Plugin;
9
10 /**
11 * Logging class.
12 */
13 class Logging {
14
15 /**
16 * Log file prefix.
17 */
18 const LOG_FILE_PREFIX = 'content-control-';
19
20 /**
21 * Whether the log file is writable.
22 *
23 * @var bool|null
24 */
25 private $is_writable;
26
27 /**
28 * Log file name.
29 *
30 * @var string
31 */
32 private $filename = '';
33
34 /**
35 * Log file path.
36 *
37 * @var string
38 */
39 private $file = '';
40
41 /**
42 * File system API.
43 *
44 * @var \WP_Filesystem_Base|null
45 */
46 private $fs;
47
48 /**
49 * Log file content.
50 *
51 * @var string|null
52 */
53 private $content;
54
55 /**
56 * Initialize logging.
57 */
58 public function __construct() {
59 $this->init();
60
61 $this->register_hooks();
62 }
63
64 /**
65 * Register hooks.
66 *
67 * @return void
68 */
69 public function register_hooks() {
70 // On shutdown, save the log file.
71 add_action( 'shutdown', [ $this, 'save_logs' ] );
72 }
73
74 /**
75 * Gets the Uploads directory
76 *
77 * @return bool|array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: string|false} An associated array with baseurl and basedir or false on failure
78 */
79 public function get_upload_dir() {
80 // Used if you only need to fetch data, not create missing folders.
81 $wp_upload_dir = wp_get_upload_dir();
82
83 // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
84 // $wp_upload_dir = wp_upload_dir(); // Disable this on IS_WPCOM if used.
85
86 if ( isset( $wp_upload_dir['error'] ) && false !== $wp_upload_dir['error'] ) {
87 return false;
88 } else {
89 return $wp_upload_dir;
90 }
91 }
92
93 /**
94 * Gets the uploads directory URL
95 *
96 * @param string $path A path to append to end of upload directory URL.
97 * @return bool|string The uploads directory URL or false on failure
98 */
99 public function get_upload_dir_url( $path = '' ) {
100 $upload_dir = $this->get_upload_dir();
101 if ( false !== $upload_dir && isset( $upload_dir['baseurl'] ) ) {
102 $url = preg_replace( '/^https?:/', '', $upload_dir['baseurl'] );
103 if ( null === $url ) {
104 return false;
105 }
106 if ( ! empty( $path ) ) {
107 $url = trailingslashit( $url ) . $path;
108 }
109 return $url;
110 } else {
111 return false;
112 }
113 }
114
115 /**
116 * Chek if logging is enabled.
117 *
118 * @return bool
119 */
120 public function enabled() {
121 $disabled = defined( '\CONTENT_CONTROL_DISABLE_LOGGING' ) && true === \CONTENT_CONTROL_DISABLE_LOGGING;
122
123 return ! $disabled && $this->is_writable();
124 }
125
126 /**
127 * Get working WP Filesystem instance
128 *
129 * @return \WP_Filesystem_Base|false
130 */
131 public function fs() {
132 if ( isset( $this->fs ) ) {
133 return $this->fs;
134 }
135
136 global $wp_filesystem;
137
138 require_once ABSPATH . 'wp-admin/includes/file.php';
139
140 // If for some reason the include doesn't work as expected just return false.
141 if ( ! function_exists( 'WP_Filesystem' ) ) {
142 return false;
143 }
144
145 $writable = WP_Filesystem( false, '', true );
146
147 // We consider the directory as writable if it uses the direct transport,
148 // otherwise credentials would be needed.
149 $this->fs = ( $writable && 'direct' === $wp_filesystem->method ) ? $wp_filesystem : false;
150
151 return $this->fs;
152 }
153
154 /**
155 * Check if the log file is writable.
156 *
157 * @return boolean
158 */
159 public function is_writable() {
160 if ( isset( $this->is_writable ) ) {
161 return $this->is_writable;
162 }
163
164 $file_system = $this->fs();
165
166 if ( false === $file_system ) {
167 $this->is_writable = false;
168 return $this->is_writable;
169 }
170
171 $this->is_writable = 'direct' === $file_system->method;
172
173 $upload_dir = $this->get_upload_dir();
174
175 if ( ! $file_system->is_writable( $upload_dir['basedir'] ) ) {
176 $this->is_writable = false;
177 }
178
179 return $this->is_writable;
180 }
181
182 /**
183 * Get things started
184 *
185 * @return void
186 */
187 public function init() {
188 $upload_dir = $this->get_upload_dir();
189 $file_system = $this->fs();
190
191 if ( false === $upload_dir || false === $file_system ) {
192 return;
193 }
194
195 $file_token = \get_option( 'content_control_debug_log_token' );
196 if ( false === $file_token ) {
197 $file_token = uniqid( (string) wp_rand(), true );
198 \update_option( 'content_control_debug_log_token', $file_token );
199 }
200
201 $this->filename = self::LOG_FILE_PREFIX . "debug-{$file_token}.log"; // ex. content-control-debug-5c2f6a9b9b5a3.log.
202 $this->file = trailingslashit( $upload_dir['basedir'] ) . $this->filename;
203
204 if ( ! $file_system->exists( $this->file ) ) {
205 $this->setup_new_log();
206 } else {
207 $this->content = $this->get_file( $this->file );
208 }
209
210 // Truncate long log files.
211 if ( $file_system->exists( $this->file ) && $file_system->size( $this->file ) >= 1048576 ) {
212 $this->truncate_log();
213 }
214 }
215
216 /**
217 * Get the log file path.
218 *
219 * @return string
220 */
221 public function get_file_path() {
222 return $this->file;
223 }
224
225 /**
226 * Retrieves the url to the file
227 *
228 * @return string|bool The url to the file or false on failure
229 */
230 public function get_file_url() {
231 if ( ! $this->enabled() ) {
232 return false;
233 }
234
235 return $this->get_upload_dir_url( $this->filename );
236 }
237
238 /**
239 * Retrieve the log data
240 *
241 * @return false|string
242 */
243 public function get_log() {
244 return $this->get_log_content();
245 }
246
247 /**
248 * Delete the log file and token.
249 *
250 * @return void
251 */
252 public function delete_logs() {
253 $file_system = $this->fs();
254
255 if ( false === $file_system ) {
256 return;
257 }
258
259 $file_system->delete( $this->file );
260 \delete_option( 'content_control_debug_log_token' );
261 }
262
263 /**
264 * Log message to file
265 *
266 * @param string $message The message to log.
267 *
268 * @return void
269 */
270 public function log( $message = '' ) {
271 $this->write_to_log( wp_date( 'Y-n-d H:i:s' ) . ' - ' . $message );
272 }
273
274 /**
275 * Log unique message to file.
276 *
277 * @param string $message The unique message to log.
278 *
279 * @return void
280 */
281 public function log_unique( $message = '' ) {
282 $contents = $this->get_log_content();
283
284 if ( strpos( $contents, $message ) !== false ) {
285 return;
286 }
287
288 $this->log( $message );
289 }
290
291 /**
292 * Get the log file contents.
293 *
294 * @return false|string
295 */
296 public function get_log_content() {
297 if ( ! isset( $this->content ) ) {
298 $this->content = $this->get_file();
299 }
300
301 return $this->content;
302 }
303
304 /**
305 * Set the log file contents in memory.
306 *
307 * @param mixed $content The content to set.
308 * @param bool $save Whether to save the content to the file immediately.
309 * @return void
310 */
311 private function set_log_content( $content, $save = false ) {
312 $this->content = $content;
313
314 if ( $save ) {
315 $this->save_logs();
316 }
317 }
318
319 /**
320 * Retrieve the contents of a file.
321 *
322 * @param string|boolean $file File to get contents of.
323 *
324 * @return false|string
325 */
326 protected function get_file( $file = false ) {
327 $file = $file ? $file : $this->file;
328
329 $file_system = $this->fs();
330
331 if ( false === $file_system || ! $this->enabled() ) {
332 return '';
333 }
334
335 $content = '';
336
337 if ( $file_system->exists( $file ) ) {
338 $content = $file_system->get_contents( $file );
339 }
340
341 return $content;
342 }
343
344 /**
345 * Write the log message
346 *
347 * @param string $message The message to write.
348 *
349 * @return void
350 */
351 protected function write_to_log( $message = '' ) {
352 if ( ! $this->enabled() ) {
353 return;
354 }
355
356 $contents = $this->get_log_content();
357
358 // If it doesn't end with a new line, add one. \r\n length is 2.
359 if ( substr( $contents, -2 ) !== "\r\n" ) {
360 $contents .= "\r\n";
361 }
362
363 $this->set_log_content( $contents . $message );
364 }
365
366 /**
367 * Save the current contents to file.
368 *
369 * @return void
370 */
371 public function save_logs() {
372 $file_system = $this->fs();
373
374 if ( false === $file_system || ! $this->enabled() ) {
375 return;
376 }
377
378 $file_system->put_contents( $this->file, $this->content, FS_CHMOD_FILE );
379 }
380
381 /**
382 * Get a line count.
383 *
384 * @return int
385 */
386 public function count_lines() {
387 $file = $this->get_log_content();
388 $lines = explode( "\r\n", $file );
389
390 return count( $lines );
391 }
392
393 /**
394 * Truncates a log file to maximum of 250 lines.
395 *
396 * @return void
397 */
398 public function truncate_log() {
399 $content = $this->get_log_content();
400 $lines = explode( "\r\n", $content );
401 $lines = array_slice( $lines, 0, 250 ); // 50 is how many lines you want to keep
402 $truncated_content = implode( "\r\n", $lines );
403 $this->set_log_content( $truncated_content, true );
404 }
405
406 /**
407 * Set up a new log file.
408 *
409 * @return void
410 */
411 public function setup_new_log() {
412 $this->set_log_content( "Content Control Debug Logs:\r\n" . wp_date( 'Y-n-d H:i:s' ) . " - Log file initialized\r\n", true );
413 }
414
415 /**
416 * Delete the log file.
417 *
418 * @return void
419 */
420 public function clear_log() {
421 $file_system = $this->fs();
422
423 if ( false === $file_system ) {
424 return;
425 }
426
427 // Delete the file.
428 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
429 @$file_system->delete( $this->file );
430
431 if ( $this->enabled() ) {
432 $this->setup_new_log();
433 }
434 }
435
436 /**
437 * Log a deprecated notice.
438 *
439 * @param string $func_name Function name.
440 * @param string $version Versoin deprecated.
441 * @param string $replacement Replacement function (optional).
442 *
443 * @return void
444 */
445 public function log_deprecated_notice( $func_name, $version, $replacement = null ) {
446 if ( ! is_null( $replacement ) ) {
447 $notice = sprintf( '%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', $func_name, $version, $replacement );
448 } else {
449 $notice = sprintf( '%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.', $func_name, $version );
450 }
451
452 $this->log_unique( $notice );
453 }
454 }