Version Description
- Access control is no longer restricted to the groups_read_post capability: now any capability can be used to limit access to posts so that different groups can be granted access to different sets of posts.
Download this release
Release Info
Developer | itthinx |
Plugin | Groups |
Version | 1.2.0 |
Comparing to | |
See all releases |
Code changes from version 1.1.5 to 1.2.0
- groups.php +2 -2
- lib/access/class-groups-access-meta-boxes.php +34 -23
- lib/access/class-groups-post-access.php +74 -56
- lib/admin/groups-admin-options.php +69 -35
- lib/core/class-groups-controller.php +7 -1
- lib/core/constants.php +5 -0
- readme.txt +54 -11
- screenshot-4.png +0 -0
- screenshot-5.png +0 -0
- screenshot-6.png +0 -0
- screenshot-7.png +0 -0
- screenshot-8.png +0 -0
groups.php
CHANGED
@@ -21,13 +21,13 @@
|
|
21 |
* Plugin Name: Groups
|
22 |
* Plugin URI: http://www.itthinx.com/plugins/groups
|
23 |
* Description: Groups provides group-based user membership management, group-based capabilities and content access control.
|
24 |
-
* Version: 1.
|
25 |
* Author: itthinx
|
26 |
* Author URI: http://www.itthinx.com
|
27 |
* Donate-Link: http://www.itthinx.com
|
28 |
* License: GPLv3
|
29 |
*/
|
30 |
-
define( 'GROUPS_CORE_VERSION', '1.
|
31 |
define( 'GROUPS_FILE', __FILE__ );
|
32 |
if ( !defined( 'GROUPS_CORE_DIR' ) ) {
|
33 |
define( 'GROUPS_CORE_DIR', WP_PLUGIN_DIR . '/groups' );
|
21 |
* Plugin Name: Groups
|
22 |
* Plugin URI: http://www.itthinx.com/plugins/groups
|
23 |
* Description: Groups provides group-based user membership management, group-based capabilities and content access control.
|
24 |
+
* Version: 1.2.0
|
25 |
* Author: itthinx
|
26 |
* Author URI: http://www.itthinx.com
|
27 |
* Donate-Link: http://www.itthinx.com
|
28 |
* License: GPLv3
|
29 |
*/
|
30 |
+
define( 'GROUPS_CORE_VERSION', '1.2.0' );
|
31 |
define( 'GROUPS_FILE', __FILE__ );
|
32 |
if ( !defined( 'GROUPS_CORE_DIR' ) ) {
|
33 |
define( 'GROUPS_CORE_DIR', WP_PLUGIN_DIR . '/groups' );
|
lib/access/class-groups-access-meta-boxes.php
CHANGED
@@ -29,6 +29,7 @@ class Groups_Access_Meta_Boxes {
|
|
29 |
const NONCE = 'groups-meta-box-nonce';
|
30 |
const SET_CAPABILITY = 'set-capability';
|
31 |
const READ_ACCESS = 'read-access';
|
|
|
32 |
|
33 |
/**
|
34 |
* Hooks for capabilities meta box and saving options.
|
@@ -76,9 +77,11 @@ class Groups_Access_Meta_Boxes {
|
|
76 |
* @param Object $box
|
77 |
*/
|
78 |
public static function capability( $object = null, $box = null ) {
|
79 |
-
|
|
|
|
|
80 |
$output = "";
|
81 |
-
|
82 |
$post_id = isset( $object->ID ) ? $object->ID : null;
|
83 |
$post_type = isset( $object->post_type ) ? $object->post_type : null;
|
84 |
$post_singular_name = __( "Post", GROUPS_PLUGIN_DOMAIN );
|
@@ -91,25 +94,28 @@ class Groups_Access_Meta_Boxes {
|
|
91 |
}
|
92 |
}
|
93 |
}
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
}
|
99 |
}
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
$checked = '';
|
104 |
-
}
|
105 |
-
|
106 |
-
$output .= '<input type="checkbox" name="' . self::READ_ACCESS . '" ' . $checked . ' />';
|
107 |
-
$output .= ' ';
|
108 |
-
$output .= '<label for="' . self::READ_ACCESS . '">';
|
109 |
-
$output .= __( "Enforce read access", GROUPS_PLUGIN_DOMAIN );
|
110 |
-
$output .= '</label> ';
|
111 |
$output .= '<p class="description">';
|
112 |
-
$output .= sprintf( __( "Only groups or users that have the
|
113 |
$output .= '</p>';
|
114 |
$output .= wp_nonce_field( self::SET_CAPABILITY, self::NONCE, true, false );
|
115 |
echo $output;
|
@@ -128,11 +134,16 @@ class Groups_Access_Meta_Boxes {
|
|
128 |
$post_type = isset( $_POST["post_type"] ) ? $_POST["post_type"] : null;
|
129 |
if ( $post_type !== null ) {
|
130 |
if ( current_user_can( 'edit_'.$post_type ) ) {
|
131 |
-
|
132 |
-
if ( $
|
133 |
-
|
134 |
-
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
136 |
}
|
137 |
}
|
138 |
}
|
29 |
const NONCE = 'groups-meta-box-nonce';
|
30 |
const SET_CAPABILITY = 'set-capability';
|
31 |
const READ_ACCESS = 'read-access';
|
32 |
+
const CAPABILITY = 'capability';
|
33 |
|
34 |
/**
|
35 |
* Hooks for capabilities meta box and saving options.
|
77 |
* @param Object $box
|
78 |
*/
|
79 |
public static function capability( $object = null, $box = null ) {
|
80 |
+
|
81 |
+
global $wpdb;
|
82 |
+
|
83 |
$output = "";
|
84 |
+
|
85 |
$post_id = isset( $object->ID ) ? $object->ID : null;
|
86 |
$post_type = isset( $object->post_type ) ? $object->post_type : null;
|
87 |
$post_singular_name = __( "Post", GROUPS_PLUGIN_DOMAIN );
|
94 |
}
|
95 |
}
|
96 |
}
|
97 |
+
|
98 |
+
$output .= __( "Enforce read access", GROUPS_PLUGIN_DOMAIN );
|
99 |
+
$read_caps = get_post_meta( $post_id, Groups_Post_Access::POSTMETA_PREFIX . Groups_Post_Access::READ_POST_CAPABILITY );
|
100 |
+
$valid_read_caps = Groups_Options::get_option( Groups_Post_Access::READ_POST_CAPABILITIES, array( Groups_Post_Access::READ_POST_CAPABILITY ) );
|
101 |
+
$output .= '<div style="padding:0 1em;margin:1em 0;border:1px solid #ccc;border-radius:4px;">';
|
102 |
+
$output .= '<ul>';
|
103 |
+
foreach( $valid_read_caps as $valid_read_cap ) {
|
104 |
+
if ( $capability = Groups_Capability::read_by_capability( $valid_read_cap ) ) {
|
105 |
+
$checked = in_array( $capability->capability, $read_caps ) ? ' checked="checked" ' : '';
|
106 |
+
$output .= '<li>';
|
107 |
+
$output .= '<label>';
|
108 |
+
$output .= '<input name="' . self::CAPABILITY . '[]" ' . $checked . ' type="checkbox" value="' . esc_attr( $capability->capability_id ) . '" />';
|
109 |
+
$output .= wp_filter_nohtml_kses( $capability->capability );
|
110 |
+
$output .= '</label>';
|
111 |
+
$output .= '</li>';
|
112 |
}
|
113 |
}
|
114 |
+
$output .= '</ul>';
|
115 |
+
$output .= '</div>';
|
116 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
$output .= '<p class="description">';
|
118 |
+
$output .= sprintf( __( "Only groups or users that have one of the selected capabilities are allowed to read this %s.", GROUPS_PLUGIN_DOMAIN ), $post_singular_name );
|
119 |
$output .= '</p>';
|
120 |
$output .= wp_nonce_field( self::SET_CAPABILITY, self::NONCE, true, false );
|
121 |
echo $output;
|
134 |
$post_type = isset( $_POST["post_type"] ) ? $_POST["post_type"] : null;
|
135 |
if ( $post_type !== null ) {
|
136 |
if ( current_user_can( 'edit_'.$post_type ) ) {
|
137 |
+
Groups_Post_Access::delete( $post_id, null );
|
138 |
+
if ( !empty( $_POST[self::CAPABILITY] ) ) {
|
139 |
+
foreach ( $_POST[self::CAPABILITY] as $capability_id ) {
|
140 |
+
if ( $capability = Groups_Capability::read( $capability_id ) ) {
|
141 |
+
Groups_Post_Access::create( array(
|
142 |
+
'post_id' => $post_id,
|
143 |
+
'capability' => $capability->capability
|
144 |
+
) );
|
145 |
+
}
|
146 |
+
}
|
147 |
}
|
148 |
}
|
149 |
}
|
lib/access/class-groups-post-access.php
CHANGED
@@ -28,6 +28,7 @@ class Groups_Post_Access {
|
|
28 |
|
29 |
const READ_POST_CAPABILITY = "groups_read_post";
|
30 |
const READ_POST_CAPABILITY_NAME = "Read Post";
|
|
|
31 |
|
32 |
/**
|
33 |
* Create needed capabilities on plugin activation.
|
@@ -36,24 +37,22 @@ class Groups_Post_Access {
|
|
36 |
public static function activate() {
|
37 |
if ( !Groups_Capability::read_by_capability( self::READ_POST_CAPABILITY ) ) {
|
38 |
Groups_Capability::create( array( "capability" => self::READ_POST_CAPABILITY ) );
|
|
|
|
|
|
|
|
|
|
|
39 |
}
|
40 |
}
|
41 |
|
42 |
public static function init() {
|
43 |
-
|
44 |
-
// for translation
|
45 |
-
// @see self::READ_POST_CAPABILITY_NAME
|
46 |
-
__( "Read Post", GROUPS_PLUGIN_DOMAIN );
|
47 |
-
|
48 |
// post access
|
49 |
add_filter( 'get_pages', array( __CLASS__, "get_pages" ), 1 );
|
50 |
add_filter( 'the_posts', array( __CLASS__, "the_posts" ), 1, 2 );
|
51 |
add_filter( 'wp_get_nav_menu_items', array( __CLASS__, "wp_get_nav_menu_items" ), 1, 3 );
|
52 |
-
|
53 |
// content access
|
54 |
add_filter( "get_the_excerpt", array( __CLASS__, "get_the_excerpt" ), 1 );
|
55 |
add_filter( "the_content", array( __CLASS__, "the_content" ), 1 );
|
56 |
-
|
57 |
// @todo these could be interesting to add later ...
|
58 |
// add_filter( "plugin_row_meta", array( __CLASS__, "plugin_row_meta" ), 1 );
|
59 |
// add_filter( "posts_join_paged", array( __CLASS__, "posts_join_paged" ), 1 );
|
@@ -66,22 +65,14 @@ class Groups_Post_Access {
|
|
66 |
* @param array $pages
|
67 |
*/
|
68 |
public static function get_pages( $pages ) {
|
69 |
-
|
70 |
$result = array();
|
|
|
71 |
foreach ( $pages as $page ) {
|
72 |
-
|
73 |
-
if ( $read_post_capability ) {
|
74 |
-
$user_id = get_current_user_id();
|
75 |
-
$groups_user = new Groups_User( $user_id );
|
76 |
-
if ( $groups_user->can( self::READ_POST_CAPABILITY ) ) {
|
77 |
-
$result[] = $page;
|
78 |
-
}
|
79 |
-
} else {
|
80 |
$result[] = $page;
|
81 |
}
|
82 |
}
|
83 |
return $result;
|
84 |
-
|
85 |
}
|
86 |
|
87 |
/**
|
@@ -91,17 +82,10 @@ class Groups_Post_Access {
|
|
91 |
* @param WP_Query $query
|
92 |
*/
|
93 |
public static function the_posts( $posts, $query ) {
|
94 |
-
|
95 |
$result = array();
|
|
|
96 |
foreach ( $posts as $post ) {
|
97 |
-
|
98 |
-
if ( $read_post_capability ) {
|
99 |
-
$user_id = get_current_user_id();
|
100 |
-
$groups_user = new Groups_User( $user_id );
|
101 |
-
if ( $groups_user->can( self::READ_POST_CAPABILITY ) ) {
|
102 |
-
$result[] = $post;
|
103 |
-
}
|
104 |
-
} else {
|
105 |
$result[] = $post;
|
106 |
}
|
107 |
}
|
@@ -111,23 +95,19 @@ class Groups_Post_Access {
|
|
111 |
/**
|
112 |
* Filter menu items by access capability.
|
113 |
*
|
|
|
|
|
114 |
* @param array $items
|
115 |
* @param mixed $menu
|
116 |
* @param array $args
|
117 |
*/
|
118 |
public static function wp_get_nav_menu_items( $items = null, $menu = null, $args = null ) {
|
119 |
$result = array();
|
|
|
120 |
foreach ( $items as $item ) {
|
121 |
// @todo might want to check $item->object and $item->type first,
|
122 |
// for example these are 'page' and 'post_type' for a page
|
123 |
-
|
124 |
-
if ( $read_post_capability ) {
|
125 |
-
$user_id = get_current_user_id();
|
126 |
-
$groups_user = new Groups_User( $user_id );
|
127 |
-
if ( $groups_user->can( self::READ_POST_CAPABILITY ) ) {
|
128 |
-
$result[] = $item;
|
129 |
-
}
|
130 |
-
} else {
|
131 |
$result[] = $item;
|
132 |
}
|
133 |
}
|
@@ -144,14 +124,7 @@ class Groups_Post_Access {
|
|
144 |
global $post;
|
145 |
$result = '';
|
146 |
if ( isset( $post->ID ) ) {
|
147 |
-
|
148 |
-
if ( $read_post_capability ) {
|
149 |
-
$user_id = get_current_user_id();
|
150 |
-
$groups_user = new Groups_User( $user_id );
|
151 |
-
if ( $groups_user->can( self::READ_POST_CAPABILITY ) ) {
|
152 |
-
$result = $output;
|
153 |
-
}
|
154 |
-
} else {
|
155 |
$result = $output;
|
156 |
}
|
157 |
}
|
@@ -168,14 +141,7 @@ class Groups_Post_Access {
|
|
168 |
global $post;
|
169 |
$result = '';
|
170 |
if ( isset( $post->ID ) ) {
|
171 |
-
|
172 |
-
if ( $read_post_capability ) {
|
173 |
-
$user_id = get_current_user_id();
|
174 |
-
$groups_user = new Groups_User( $user_id );
|
175 |
-
if ( $groups_user->can( self::READ_POST_CAPABILITY ) ) {
|
176 |
-
$result = $output;
|
177 |
-
}
|
178 |
-
} else {
|
179 |
$result = $output;
|
180 |
}
|
181 |
}
|
@@ -200,9 +166,13 @@ class Groups_Post_Access {
|
|
200 |
if ( !isset( $capability ) ) {
|
201 |
$capability = self::READ_POST_CAPABILITY;
|
202 |
}
|
203 |
-
|
204 |
if ( !empty( $post_id ) && !empty( $capability) ) {
|
205 |
-
|
|
|
|
|
|
|
|
|
206 |
}
|
207 |
return $result;
|
208 |
}
|
@@ -218,7 +188,12 @@ class Groups_Post_Access {
|
|
218 |
* @return true if the capability is required, otherwise false
|
219 |
*/
|
220 |
public static function read( $post_id, $capability = self::READ_POST_CAPABILITY ) {
|
221 |
-
|
|
|
|
|
|
|
|
|
|
|
222 |
}
|
223 |
|
224 |
/**
|
@@ -235,13 +210,56 @@ class Groups_Post_Access {
|
|
235 |
* Removes a capability requirement from a post.
|
236 |
*
|
237 |
* @param int $post_id
|
238 |
-
* @param string $capability
|
239 |
* @return true on success, otherwise false
|
240 |
*/
|
241 |
public static function delete( $post_id, $capability = self::READ_POST_CAPABILITY ) {
|
242 |
$result = false;
|
243 |
-
|
244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
}
|
246 |
return $result;
|
247 |
}
|
28 |
|
29 |
const READ_POST_CAPABILITY = "groups_read_post";
|
30 |
const READ_POST_CAPABILITY_NAME = "Read Post";
|
31 |
+
const READ_POST_CAPABILITIES = 'read_post_capabilities';
|
32 |
|
33 |
/**
|
34 |
* Create needed capabilities on plugin activation.
|
37 |
public static function activate() {
|
38 |
if ( !Groups_Capability::read_by_capability( self::READ_POST_CAPABILITY ) ) {
|
39 |
Groups_Capability::create( array( "capability" => self::READ_POST_CAPABILITY ) );
|
40 |
+
// default read caps
|
41 |
+
Groups_Options::update_option( Groups_Post_Access::READ_POST_CAPABILITIES, array( Groups_Post_Access::READ_POST_CAPABILITY ) );
|
42 |
+
// for translation
|
43 |
+
// @see self::READ_POST_CAPABILITY_NAME
|
44 |
+
__( "Read Post", GROUPS_PLUGIN_DOMAIN );
|
45 |
}
|
46 |
}
|
47 |
|
48 |
public static function init() {
|
|
|
|
|
|
|
|
|
|
|
49 |
// post access
|
50 |
add_filter( 'get_pages', array( __CLASS__, "get_pages" ), 1 );
|
51 |
add_filter( 'the_posts', array( __CLASS__, "the_posts" ), 1, 2 );
|
52 |
add_filter( 'wp_get_nav_menu_items', array( __CLASS__, "wp_get_nav_menu_items" ), 1, 3 );
|
|
|
53 |
// content access
|
54 |
add_filter( "get_the_excerpt", array( __CLASS__, "get_the_excerpt" ), 1 );
|
55 |
add_filter( "the_content", array( __CLASS__, "the_content" ), 1 );
|
|
|
56 |
// @todo these could be interesting to add later ...
|
57 |
// add_filter( "plugin_row_meta", array( __CLASS__, "plugin_row_meta" ), 1 );
|
58 |
// add_filter( "posts_join_paged", array( __CLASS__, "posts_join_paged" ), 1 );
|
65 |
* @param array $pages
|
66 |
*/
|
67 |
public static function get_pages( $pages ) {
|
|
|
68 |
$result = array();
|
69 |
+
$user_id = get_current_user_id();
|
70 |
foreach ( $pages as $page ) {
|
71 |
+
if ( self::user_can_read_post( $page->ID, $user_id ) ) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
$result[] = $page;
|
73 |
}
|
74 |
}
|
75 |
return $result;
|
|
|
76 |
}
|
77 |
|
78 |
/**
|
82 |
* @param WP_Query $query
|
83 |
*/
|
84 |
public static function the_posts( $posts, $query ) {
|
|
|
85 |
$result = array();
|
86 |
+
$user_id = get_current_user_id();
|
87 |
foreach ( $posts as $post ) {
|
88 |
+
if ( self::user_can_read_post( $post->ID, $user_id ) ) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
$result[] = $post;
|
90 |
}
|
91 |
}
|
95 |
/**
|
96 |
* Filter menu items by access capability.
|
97 |
*
|
98 |
+
* @todo admin section: this won't inhibit the items being offered to be added, although when they're added they won't show up in the menu
|
99 |
+
*
|
100 |
* @param array $items
|
101 |
* @param mixed $menu
|
102 |
* @param array $args
|
103 |
*/
|
104 |
public static function wp_get_nav_menu_items( $items = null, $menu = null, $args = null ) {
|
105 |
$result = array();
|
106 |
+
$user_id = get_current_user_id();
|
107 |
foreach ( $items as $item ) {
|
108 |
// @todo might want to check $item->object and $item->type first,
|
109 |
// for example these are 'page' and 'post_type' for a page
|
110 |
+
if ( self::user_can_read_post( $item->object_id, $user_id ) ) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
$result[] = $item;
|
112 |
}
|
113 |
}
|
124 |
global $post;
|
125 |
$result = '';
|
126 |
if ( isset( $post->ID ) ) {
|
127 |
+
if ( self::user_can_read_post( $post->ID ) ) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
$result = $output;
|
129 |
}
|
130 |
}
|
141 |
global $post;
|
142 |
$result = '';
|
143 |
if ( isset( $post->ID ) ) {
|
144 |
+
if ( self::user_can_read_post( $post->ID ) ) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
$result = $output;
|
146 |
}
|
147 |
}
|
166 |
if ( !isset( $capability ) ) {
|
167 |
$capability = self::READ_POST_CAPABILITY;
|
168 |
}
|
169 |
+
|
170 |
if ( !empty( $post_id ) && !empty( $capability) ) {
|
171 |
+
if ( Groups_Capability::read_by_capability( $capability ) ) {
|
172 |
+
if ( !in_array( $capability, get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY ) ) ) {
|
173 |
+
$result = add_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, $capability );
|
174 |
+
}
|
175 |
+
}
|
176 |
}
|
177 |
return $result;
|
178 |
}
|
188 |
* @return true if the capability is required, otherwise false
|
189 |
*/
|
190 |
public static function read( $post_id, $capability = self::READ_POST_CAPABILITY ) {
|
191 |
+
$result = false;
|
192 |
+
$caps = get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY );
|
193 |
+
if ( $caps ) {
|
194 |
+
$result = in_array( $capability, $caps );
|
195 |
+
}
|
196 |
+
return $result;
|
197 |
}
|
198 |
|
199 |
/**
|
210 |
* Removes a capability requirement from a post.
|
211 |
*
|
212 |
* @param int $post_id
|
213 |
+
* @param string $capability defaults to groups_read_post, removes all if null is given
|
214 |
* @return true on success, otherwise false
|
215 |
*/
|
216 |
public static function delete( $post_id, $capability = self::READ_POST_CAPABILITY ) {
|
217 |
$result = false;
|
218 |
+
if ( !empty( $post_id ) ) {
|
219 |
+
if ( !empty( $capability ) ) {
|
220 |
+
$result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY, $capability );
|
221 |
+
} else {
|
222 |
+
$result = delete_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY );
|
223 |
+
}
|
224 |
+
}
|
225 |
+
return $result;
|
226 |
+
}
|
227 |
+
|
228 |
+
/**
|
229 |
+
* Returns a list of capabilities that grant access to the post.
|
230 |
+
*
|
231 |
+
* @param int $post_id
|
232 |
+
* @return array of string, capabilities
|
233 |
+
*/
|
234 |
+
public static function get_read_post_capabilities( $post_id ) {
|
235 |
+
return get_post_meta( $post_id, self::POSTMETA_PREFIX . self::READ_POST_CAPABILITY );
|
236 |
+
}
|
237 |
+
|
238 |
+
/**
|
239 |
+
* Returns true if the user has any of the capabilities that grant access to the post.
|
240 |
+
*
|
241 |
+
* @param int $post_id post id
|
242 |
+
* @param int $user_id user id or null for current user
|
243 |
+
* @return boolean true if user can read the post
|
244 |
+
*/
|
245 |
+
public static function user_can_read_post( $post_id, $user_id = null ) {
|
246 |
+
$result = false;
|
247 |
+
if ( !empty( $post_id ) ) {
|
248 |
+
if ( $user_id === null ) {
|
249 |
+
$user_id = get_current_user_id();
|
250 |
+
}
|
251 |
+
$groups_user = new Groups_User( $user_id );
|
252 |
+
$read_caps = self::get_read_post_capabilities( $post_id );
|
253 |
+
if ( !empty( $read_caps ) ) {
|
254 |
+
foreach( $read_caps as $read_cap ) {
|
255 |
+
if ( $groups_user->can( $read_cap ) ) {
|
256 |
+
$result = true;
|
257 |
+
break;
|
258 |
+
}
|
259 |
+
}
|
260 |
+
} else {
|
261 |
+
$result = true;
|
262 |
+
}
|
263 |
}
|
264 |
return $result;
|
265 |
}
|
lib/admin/groups-admin-options.php
CHANGED
@@ -44,9 +44,9 @@ function groups_admin_options() {
|
|
44 |
|
45 |
echo
|
46 |
'<div>' .
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
'</div>';
|
51 |
|
52 |
$caps = array(
|
@@ -71,6 +71,17 @@ function groups_admin_options() {
|
|
71 |
add_option( GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE, $admin_override ); // WP 3.3.1 : update alone wouldn't create the option when value is false
|
72 |
update_option( GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE, $admin_override );
|
73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
// tree view
|
75 |
if ( !empty( $_POST[GROUPS_SHOW_TREE_VIEW] ) ) {
|
76 |
Groups_Options::update_option( GROUPS_SHOW_TREE_VIEW, true );
|
@@ -155,45 +166,68 @@ function groups_admin_options() {
|
|
155 |
//
|
156 |
echo
|
157 |
'<form action="" name="options" method="post">' .
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
|
165 |
echo
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
echo
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
if ( !$is_sitewide_plugin ) {
|
181 |
echo
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
}
|
191 |
echo
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
'</form>';
|
198 |
Groups_Help::footer();
|
199 |
}
|
44 |
|
45 |
echo
|
46 |
'<div>' .
|
47 |
+
'<h2>' .
|
48 |
+
__( 'Groups options', GROUPS_PLUGIN_DOMAIN ) .
|
49 |
+
'</h2>' .
|
50 |
'</div>';
|
51 |
|
52 |
$caps = array(
|
71 |
add_option( GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE, $admin_override ); // WP 3.3.1 : update alone wouldn't create the option when value is false
|
72 |
update_option( GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE, $admin_override );
|
73 |
|
74 |
+
$valid_read_caps = array( Groups_Post_Access::READ_POST_CAPABILITY );
|
75 |
+
if ( !empty( $_POST[GROUPS_READ_POST_CAPABILITIES] ) ) {
|
76 |
+
$read_caps = $_POST[GROUPS_READ_POST_CAPABILITIES];
|
77 |
+
foreach( $read_caps as $read_cap ) {
|
78 |
+
if ( !in_array( $read_cap, $valid_read_caps ) && ( $valid_cap = Groups_Capability::read( $read_cap ) ) ) {
|
79 |
+
$valid_read_caps[] = $valid_cap->capability;
|
80 |
+
}
|
81 |
+
}
|
82 |
+
}
|
83 |
+
Groups_Options::update_option( Groups_Post_Access::READ_POST_CAPABILITIES, $valid_read_caps );
|
84 |
+
|
85 |
// tree view
|
86 |
if ( !empty( $_POST[GROUPS_SHOW_TREE_VIEW] ) ) {
|
87 |
Groups_Options::update_option( GROUPS_SHOW_TREE_VIEW, true );
|
166 |
//
|
167 |
echo
|
168 |
'<form action="" name="options" method="post">' .
|
169 |
+
'<div>' .
|
170 |
+
'<h3>' . __( 'Administrator Access Override', GROUPS_PLUGIN_DOMAIN ) . '</h3>' .
|
171 |
+
'<p>' .
|
172 |
+
'<input name="' . GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE . '" type="checkbox" ' . ( $admin_override ? 'checked="checked"' : '' ) . '/>' .
|
173 |
+
'<label for="' . GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE . '">' . __( 'Administrators override all access permissions derived from Groups capabilities.', GROUPS_PLUGIN_DOMAIN ) . '</label>' .
|
174 |
+
'</p>';
|
175 |
+
|
176 |
+
echo '<h3>' . __( 'Access restricions', GROUPS_PLUGIN_DOMAIN ) . '</h3>';
|
177 |
+
|
178 |
+
echo '<p class="description">' .
|
179 |
+
__( 'Include these capabilities to enforce read access on posts. The selected capabilities will be offered to restrict access to posts.', GROUPS_PLUGIN_DOMAIN ) .
|
180 |
+
'</p>';
|
181 |
+
|
182 |
+
$capability_table = _groups_get_tablename( "capability" );
|
183 |
+
$capabilities = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $capability_table ORDER BY capability" ) );
|
184 |
+
$applicable_read_caps = Groups_Options::get_option( Groups_Post_Access::READ_POST_CAPABILITIES, array( Groups_Post_Access::READ_POST_CAPABILITY ) );
|
185 |
+
foreach( $capabilities as $capability ) {
|
186 |
+
$checked = in_array( $capability->capability, $applicable_read_caps ) ? ' checked="checked" ' : '';
|
187 |
+
if ( $capability->capability == Groups_Post_Access::READ_POST_CAPABILITY ) {
|
188 |
+
$checked .= ' readonly="readonly" disabled="disabled" ';
|
189 |
+
}
|
190 |
+
echo '<label>';
|
191 |
+
echo '<input name="' . GROUPS_READ_POST_CAPABILITIES . '[]" ' . $checked . ' type="checkbox" value="' . esc_attr( $capability->capability_id ) . '" />';
|
192 |
+
echo wp_filter_nohtml_kses( $capability->capability );
|
193 |
+
echo '</label>';
|
194 |
+
echo ' ';
|
195 |
+
echo '<span class="description">' . wp_filter_nohtml_kses( $capability->description ) . '</span>';
|
196 |
+
echo '<br/>';
|
197 |
+
}
|
198 |
|
199 |
echo
|
200 |
+
'<h3>' . __( 'Tree view', GROUPS_PLUGIN_DOMAIN ) . '</h3>' .
|
201 |
+
'<p>' .
|
202 |
+
'<input name="' . GROUPS_SHOW_TREE_VIEW . '" type="checkbox" ' . ( $show_tree_view ? 'checked="checked"' : '' ) . '/>' .
|
203 |
+
'<label for="' . GROUPS_SHOW_TREE_VIEW . '">' . __( 'Show the Groups tree view.', GROUPS_PLUGIN_DOMAIN ) . '</label>' .
|
204 |
+
'</p>';
|
205 |
echo
|
206 |
+
'<h3>' . __( 'Permissions', GROUPS_PLUGIN_DOMAIN ) . '</h3>' .
|
207 |
+
'<p>' . __( 'These permissions apply to Groups management. They do not apply to access permissions derived from Groups capabilities.', GROUPS_PLUGIN_DOMAIN ) . '</p>' .
|
208 |
+
$caps_table .
|
209 |
+
'<p class="description">' .
|
210 |
+
__( 'A minimum set of permissions will be preserved.', GROUPS_PLUGIN_DOMAIN ) .
|
211 |
+
'<br/>' .
|
212 |
+
__( 'If you lock yourself out, please ask an administrator to help.', GROUPS_PLUGIN_DOMAIN ) .
|
213 |
+
'</p>';
|
214 |
if ( !$is_sitewide_plugin ) {
|
215 |
echo
|
216 |
+
'<h3>' . __( 'Deactivation and data persistence', GROUPS_PLUGIN_DOMAIN ) . '</h3>' .
|
217 |
+
'<p>' .
|
218 |
+
'<input name="delete-data" type="checkbox" ' . ( $delete_data ? 'checked="checked"' : '' ) . '/>' .
|
219 |
+
'<label for="delete-data">' . __( 'Delete all Groups plugin data on deactivation', GROUPS_PLUGIN_DOMAIN ) . '</label>' .
|
220 |
+
'</p>' .
|
221 |
+
'<p class="description warning">' .
|
222 |
+
__( 'CAUTION: If this option is active while the plugin is deactivated, ALL plugin settings and data will be DELETED. If you are going to use this option, now would be a good time to make a backup. By enabling this option you agree to be solely responsible for any loss of data or any other consequences thereof.', GROUPS_PLUGIN_DOMAIN ) .
|
223 |
+
'</p>';
|
224 |
}
|
225 |
echo
|
226 |
+
'<p>' .
|
227 |
+
wp_nonce_field( 'admin', GROUPS_ADMIN_OPTIONS_NONCE, true, false ) .
|
228 |
+
'<input type="submit" name="submit" value="' . __( 'Save', GROUPS_PLUGIN_DOMAIN ) . '"/>' .
|
229 |
+
'</p>' .
|
230 |
+
'</div>' .
|
231 |
'</form>';
|
232 |
Groups_Help::footer();
|
233 |
}
|
lib/core/class-groups-controller.php
CHANGED
@@ -128,7 +128,7 @@ class Groups_Controller {
|
|
128 |
|
129 |
// create WP capabilities
|
130 |
Groups_Controller::set_default_capabilities();
|
131 |
-
|
132 |
$charset_collate = '';
|
133 |
if ( ! empty( $wpdb->charset ) ) {
|
134 |
$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
|
@@ -256,6 +256,12 @@ class Groups_Controller {
|
|
256 |
}
|
257 |
break;
|
258 |
default :
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
} // switch
|
260 |
foreach ( $queries as $query ) {
|
261 |
if ( $wpdb->query( $query ) === false ) {
|
128 |
|
129 |
// create WP capabilities
|
130 |
Groups_Controller::set_default_capabilities();
|
131 |
+
|
132 |
$charset_collate = '';
|
133 |
if ( ! empty( $wpdb->charset ) ) {
|
134 |
$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
|
256 |
}
|
257 |
break;
|
258 |
default :
|
259 |
+
if ( !empty( $previous_version ) ) {
|
260 |
+
if ( strcmp( $previous_version, '1.1.6' ) < 0 ) {
|
261 |
+
Groups_Options::update_option( Groups_Post_Access::READ_POST_CAPABILITIES, array( Groups_Post_Access::READ_POST_CAPABILITY ) );
|
262 |
+
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %s WHERE meta_key = %s", Groups_Post_Access::READ_POST_CAPABILITY, Groups_Post_Access::POSTMETA_PREFIX . Groups_Post_Access::READ_POST_CAPABILITY ) );
|
263 |
+
}
|
264 |
+
}
|
265 |
} // switch
|
266 |
foreach ( $queries as $query ) {
|
267 |
if ( $wpdb->query( $query ) === false ) {
|
lib/core/constants.php
CHANGED
@@ -77,6 +77,11 @@ define( 'GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE', 'groups-admin-override' );
|
|
77 |
*/
|
78 |
define( 'GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE_DEFAULT', true );
|
79 |
|
|
|
|
|
|
|
|
|
|
|
80 |
/**
|
81 |
* Tree view option
|
82 |
* @var string
|
77 |
*/
|
78 |
define( 'GROUPS_ADMINISTRATOR_ACCESS_OVERRIDE_DEFAULT', true );
|
79 |
|
80 |
+
/**
|
81 |
+
* @var string read post capabilities option
|
82 |
+
*/
|
83 |
+
define( 'GROUPS_READ_POST_CAPABILITIES', 'groups-read-post-capabilities' );
|
84 |
+
|
85 |
/**
|
86 |
* Tree view option
|
87 |
* @var string
|
readme.txt
CHANGED
@@ -4,7 +4,8 @@ Donate link: http://www.itthinx.com/plugins/groups
|
|
4 |
Tags: access, access control, capability, capabilities, content, download, downloads, file, file access, files, group, groups, member, members, membership, permission, permissions
|
5 |
Requires at least: 3.0
|
6 |
Tested up to: 3.3.2
|
7 |
-
Stable tag: 1.
|
|
|
8 |
|
9 |
Groups provides group-based user membership management, group-based capabilities and content access control.
|
10 |
|
@@ -62,9 +63,17 @@ It integrates standard WordPress capabilities and application-specific capabilit
|
|
62 |
|
63 |
#### Access Control ####
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
#### Framework ####
|
70 |
|
@@ -92,8 +101,8 @@ Please try to solve problems there before you rate this plugin or say it doesn't
|
|
92 |
|
93 |
##### Access restrictions on posts ####
|
94 |
|
95 |
-
On posts an pages (and custom content types) a new meta box
|
96 |
-
By checking *Enforce read access*, you can restrict access to the post to groups and users who
|
97 |
You need to assign this capability to a group and make users members of that group to allow them to see those posts.
|
98 |
|
99 |
#### Content visibility for members and non-members ####
|
@@ -143,9 +152,10 @@ Capabilities can be assigned to groups and users (1). These capabilities include
|
|
143 |
the *standard WordPress capabilities* but you can also define additional
|
144 |
capabilities for your web-application.
|
145 |
|
146 |
-
Groups defines
|
147 |
-
|
148 |
-
with that capability
|
|
|
149 |
|
150 |
(1) Assigning capabilities to users is not integrated in the user interface yet but can be done through API calls.
|
151 |
|
@@ -223,6 +233,8 @@ For detailed information about this shortcode, please refer to the [Groups plugi
|
|
223 |
|
224 |
= Where is the documentation? =
|
225 |
|
|
|
|
|
226 |
The official Groups documentation root is at the [Groups Documentation](http://www.itthinx.com/documentation/groups/) page.
|
227 |
The documentation is a work in progress, if you don't find anything there yet but want to know about the API, please look at the code as it provides useful documentation on all functions.
|
228 |
|
@@ -230,6 +242,30 @@ The documentation is a work in progress, if you don't find anything there yet bu
|
|
230 |
|
231 |
You can leave a comment at the [Groups plugin page](http://www.itthinx.com/plugins/groups/).
|
232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
== Screenshots ==
|
234 |
|
235 |
See also [Groups](http://www.itthinx.com/plugins/groups/)
|
@@ -237,13 +273,17 @@ See also [Groups](http://www.itthinx.com/plugins/groups/)
|
|
237 |
1. Groups - this is where you add and remove groups and assign capabilities to groups.
|
238 |
2. Capabilities - here you get an overview of the capabilities that are defined and you can add and remove capabilities as well.
|
239 |
3. Users - group membership is managed from the standard Users admin view.
|
240 |
-
4. Access restrictions meta box - on pages and posts (or custom content types) you can restrict access to users who are part of a group with
|
241 |
5. Usage of the [groups_member] and [groups_non_member] shortcodes to limit visibility of content to users who are members of a group or users who are not members of a group. Multiple comma-separated groups can be specified.
|
242 |
6. Usage of the [groups_can] and [groups_can_not] shortcodes. Limits visibility of enclosed content to those users who have the capability or those who do not. Multiple capabilities can be given.
|
243 |
7. Options - you can adjust the plugin's settings here.
|
|
|
244 |
|
245 |
== Changelog ==
|
246 |
|
|
|
|
|
|
|
247 |
= 1.1.5 =
|
248 |
* Added shortcode & API functions [groups_user_group] / [groups_user_groups] that allows to show the list of groups the current user or a specific user belongs to
|
249 |
* Added shortcode & API functions [groups_groups]to show the site's list of groups
|
@@ -294,8 +334,11 @@ Some installations wouldn't work correctly, showing no capabilities and making i
|
|
294 |
|
295 |
== Upgrade Notice ==
|
296 |
|
|
|
|
|
|
|
297 |
= 1.1.5 =
|
298 |
-
|
299 |
|
300 |
= 1.1.4 =
|
301 |
* Several bug fixes and improvements.
|
4 |
Tags: access, access control, capability, capabilities, content, download, downloads, file, file access, files, group, groups, member, members, membership, permission, permissions
|
5 |
Requires at least: 3.0
|
6 |
Tested up to: 3.3.2
|
7 |
+
Stable tag: 1.2.0
|
8 |
+
License: GPLv3
|
9 |
|
10 |
Groups provides group-based user membership management, group-based capabilities and content access control.
|
11 |
|
63 |
|
64 |
#### Access Control ####
|
65 |
|
66 |
+
Access to posts and pages can be restricted by capability.
|
67 |
+
|
68 |
+
Any capability can be used to restrict access, including new capabilities.
|
69 |
+
|
70 |
+
If access to a post is restricted, only users who belong to a group with that
|
71 |
+
capability may access the post.
|
72 |
+
|
73 |
+
Groups defines the groups_read_post capability by default, which can be
|
74 |
+
used to restrict access to certain posts or pages to groups
|
75 |
+
with that capability only. Any other capability (including new ones) can be
|
76 |
+
used to limit access as well.
|
77 |
|
78 |
#### Framework ####
|
79 |
|
101 |
|
102 |
##### Access restrictions on posts ####
|
103 |
|
104 |
+
On posts an pages (and custom content types) a new meta box titled *Access restrictions* appears.
|
105 |
+
By checking a capability under *Enforce read access*, you can restrict access to the post to groups and users who are members of a group with that capability.
|
106 |
You need to assign this capability to a group and make users members of that group to allow them to see those posts.
|
107 |
|
108 |
#### Content visibility for members and non-members ####
|
152 |
the *standard WordPress capabilities* but you can also define additional
|
153 |
capabilities for your web-application.
|
154 |
|
155 |
+
Groups defines the *groups_read_post* capability by default which can be
|
156 |
+
used to restrict access to certain posts or pages to groups (and users)
|
157 |
+
with that capability only. Additional capabilities can be identified on the
|
158 |
+
*Groups > Options* admin screen that may be used to limit access.
|
159 |
|
160 |
(1) Assigning capabilities to users is not integrated in the user interface yet but can be done through API calls.
|
161 |
|
233 |
|
234 |
= Where is the documentation? =
|
235 |
|
236 |
+
Most of the features are currently documented at the [Groups plugin page](http://www.itthinx.com/plugins/groups/).
|
237 |
+
|
238 |
The official Groups documentation root is at the [Groups Documentation](http://www.itthinx.com/documentation/groups/) page.
|
239 |
The documentation is a work in progress, if you don't find anything there yet but want to know about the API, please look at the code as it provides useful documentation on all functions.
|
240 |
|
242 |
|
243 |
You can leave a comment at the [Groups plugin page](http://www.itthinx.com/plugins/groups/).
|
244 |
|
245 |
+
= I want Advanced and Premium members, where the Premium members can access everything that Advanced members can access. How can I do that? =
|
246 |
+
|
247 |
+
Example: Advanced and Premium members
|
248 |
+
|
249 |
+
1. Go to *Groups > Capabilities* and define two new capabilities, let's call them *advanced* and *premium*.
|
250 |
+
2. Go to *Groups > Groups* and define two new groups, let's call them *Advanced Members* and *Premium Members* - select *Advanced Members* as the *Parent* for the *Premium Members* group.
|
251 |
+
3. Assign the *advanced* capability to the *Advanced Members* group and the *premium* capability to the *Premium Members* group.
|
252 |
+
4. Go to *Groups > Options* and tick the checkboxes for *advanced* and *premium* under _Access restrictions_ and hit the *Save* button at the end of the page.
|
253 |
+
5. Now create an example post that only members of the *Advanced Members* group should be able to see and tick the *advanced* checkbox under _Access restrictions_.
|
254 |
+
6. Create another post for *Premium Members* and tick the *premium* checkbox for that post.
|
255 |
+
7. Assign test users to both groups, log in as each user in turn and see which posts will be accessible.
|
256 |
+
|
257 |
+
= How do I limit access to posts so that users in group A can not read the same as those in group B and vice-versa? =
|
258 |
+
|
259 |
+
Example: Green and Red members
|
260 |
+
|
261 |
+
1. Go to *Groups > Capabilities* and define two new capabilities, call them *green* and *red*.
|
262 |
+
2. Go to *Groups > Groups* and define two new groups, let's call them *Green Members* and *Red Members*
|
263 |
+
3. Assign the *green* capability to the *Green Members* group and the *red* capability to the *Red Members* group.
|
264 |
+
4. Go to *Groups > Options* and tick the checkboxes for *green* and *red* under _Access restrictions_ and hit the *Save* button at the end of the page.
|
265 |
+
5. Now create an example post that only members of the *Green Members* group should be able to see and tick the *green* checkbox under _Access restrictions_.
|
266 |
+
6. Create another post for *Red Members* and tick the *red* checkbox for that post.
|
267 |
+
7. Assign a test user to any of the above groups, log in as that user and the post will be accessible.
|
268 |
+
|
269 |
== Screenshots ==
|
270 |
|
271 |
See also [Groups](http://www.itthinx.com/plugins/groups/)
|
273 |
1. Groups - this is where you add and remove groups and assign capabilities to groups.
|
274 |
2. Capabilities - here you get an overview of the capabilities that are defined and you can add and remove capabilities as well.
|
275 |
3. Users - group membership is managed from the standard Users admin view.
|
276 |
+
4. Access restrictions meta box - on pages and posts (or custom content types) you can restrict access to users who are part of a group with capabilities.
|
277 |
5. Usage of the [groups_member] and [groups_non_member] shortcodes to limit visibility of content to users who are members of a group or users who are not members of a group. Multiple comma-separated groups can be specified.
|
278 |
6. Usage of the [groups_can] and [groups_can_not] shortcodes. Limits visibility of enclosed content to those users who have the capability or those who do not. Multiple capabilities can be given.
|
279 |
7. Options - you can adjust the plugin's settings here.
|
280 |
+
8. More options.
|
281 |
|
282 |
== Changelog ==
|
283 |
|
284 |
+
= 1.2.0 =
|
285 |
+
* Access control is no longer restricted to the groups_read_post capability: now any capability can be used to limit access to posts so that different groups can be granted access to different sets of posts.
|
286 |
+
|
287 |
= 1.1.5 =
|
288 |
* Added shortcode & API functions [groups_user_group] / [groups_user_groups] that allows to show the list of groups the current user or a specific user belongs to
|
289 |
* Added shortcode & API functions [groups_groups]to show the site's list of groups
|
334 |
|
335 |
== Upgrade Notice ==
|
336 |
|
337 |
+
= 1.2.0 =
|
338 |
+
* New: Different groups can be granted access to different sets of pages or posts: Any capability - including custom capabilities - can be used to limit access.
|
339 |
+
|
340 |
= 1.1.5 =
|
341 |
+
* New shortcodes.
|
342 |
|
343 |
= 1.1.4 =
|
344 |
* Several bug fixes and improvements.
|
screenshot-4.png
CHANGED
Binary file
|
screenshot-5.png
CHANGED
Binary file
|
screenshot-6.png
CHANGED
Binary file
|
screenshot-7.png
CHANGED
Binary file
|
screenshot-8.png
ADDED
Binary file
|