';
137 |
138 | if ( participad_is_admin_page() ) {
139 | if ( ! participad_api_endpoint() ) {
140 | $message = __( 'You must provide a valid Etherpad Lite URL.', 'participad' );
141 | } else if ( ! participad_api_key() ) {
142 | $message = __( 'You must provide a valid Etherpad Lite API key. You can find this key in the APIKEY.txt file in the root of your Etherpad Lite installation.', 'participad' );
143 | } else {
144 | $message = __( 'We couldn\'t find an Etherpad Lite installation at the URL you provided. Please check the details and try again.', 'participad' );
145 | }
146 |
147 | $html .= $message;
148 | } else {
149 | $html .= sprintf( __( 'Participad is not set up correctly. Visit the settings page to learn more.', 'participad' ), participad_admin_url() );
150 | }
151 |
152 | $html .= '
';
153 | $html .= '';
154 |
155 | echo $html;
156 | }
157 | }
158 | add_action( 'admin_notices', 'participad_setup_admin_notice', 999 );
159 |
160 | /**
161 | * Returns the URL of the admin page
162 | *
163 | * We need this all over the place, so I've thrown it in a function
164 | *
165 | * @return string
166 | */
167 | function participad_admin_url() {
168 | return add_query_arg( 'page', 'participad', admin_url( 'options-general.php' ) );
169 | }
170 |
171 | /**
172 | * Is this the Participad admin page?
173 | *
174 | * @since 1.0
175 | * @return bool
176 | */
177 | function participad_is_admin_page() {
178 | global $pagenow;
179 |
180 | return 'options-general.php' == $pagenow && isset( $_GET['page'] ) && 'participad' == $_GET['page'];
181 | }
182 |
183 | function participad_flush_rewrite_rules() {
184 | if ( ! is_admin() ) {
185 | return;
186 | }
187 |
188 | if ( ! is_super_admin() ) {
189 | return;
190 | }
191 |
192 | if ( ! participad_is_installed_correctly() ) {
193 | return;
194 | }
195 |
196 | global $wp_rewrite;
197 |
198 | // Check to see whether our rules have been registered yet, by
199 | // finding a Notepad rule and then comparing it to the registered rules
200 | foreach ( $wp_rewrite->extra_rules_top as $rewrite => $rule ) {
201 | if ( 0 === strpos( $rewrite, 'notepads' ) ) {
202 | $test_rule = $rule;
203 | }
204 | }
205 | $registered_rules = get_option( 'rewrite_rules' );
206 |
207 | if ( ! empty( $test_rule ) && ! in_array( $test_rule, (array) $registered_rules ) ) {
208 | flush_rewrite_rules();
209 | }
210 | }
211 | add_action( 'admin_init', 'participad_flush_rewrite_rules' );
212 |
--------------------------------------------------------------------------------
/modules/dashboard/dashboard.php:
--------------------------------------------------------------------------------
1 | id = 'dashboard';
22 |
23 | if ( is_wp_error( $this->init() ) ) {
24 | return;
25 | }
26 |
27 | if ( 'no' === participad_is_module_enabled( 'dashboard' ) ) {
28 | return;
29 | }
30 |
31 | add_action( 'admin_init', array( $this, 'start' ) );
32 | }
33 |
34 | /**
35 | * Will an Etherpad instance appear on this page?
36 | *
37 | * No need to initialize the API client on every pageload
38 | *
39 | * Must be overridden in a module class
40 | *
41 | * @return bool
42 | */
43 | public function load_on_page() {
44 | $request_uri = $_SERVER['REQUEST_URI'];
45 | $qpos = strpos( $request_uri, '?' );
46 | if ( false !== $qpos ) {
47 | $request_uri = substr( $request_uri, 0, $qpos );
48 | }
49 |
50 | $retval = false;
51 | $filename = substr( $request_uri, strrpos( $request_uri, '/' ) + 1 );
52 |
53 | if ( 'post.php' == $filename || 'post-new.php' == $filename ) {
54 | $retval = true;
55 | }
56 |
57 | return $retval;
58 | }
59 |
60 | public function set_wp_post_id() {
61 | global $post;
62 |
63 | $wp_post_id = 0;
64 |
65 | if ( isset( $_GET['post'] ) ) {
66 | $wp_post_id = $_GET['post'];
67 | } else if ( isset( $_POST['post_ID'] ) ) { // saving post
68 | $wp_post_id = $_POST['post_ID'];
69 | } else if ( !empty( $post->ID ) ) {
70 | $wp_post_id = $post->ID;
71 | }
72 |
73 | // If we still have no post ID, we're probably in the post
74 | // creation process. We have to get weird.
75 | // 1) Create a dummy post for use throughout the process
76 | // 2) Dynamically add a field to the post creation page that
77 | // contains the id of the dummy post
78 | // 3) When the post is finally created, hook in, look for the
79 | // dummy post data, copy it to the new post, and delete the
80 | // dummy
81 | if ( ! $wp_post_id ) {
82 | $wp_post_id = wp_insert_post( array(
83 | 'post_title' => 'Participad_Dummy_Post',
84 | 'post_content' => '',
85 | 'post_status' => 'auto-draft'
86 | ) );
87 |
88 | $this->localize_script['dummy_post_ID'] = $wp_post_id;
89 | }
90 |
91 | $this->wp_post_id = (int) $wp_post_id;
92 |
93 | }
94 |
95 | public function post_ep_setup() {
96 | add_action( 'get_post_metadata', array( $this, 'prevent_check_edit_lock' ), 10, 4 );
97 | add_action( 'admin_enqueue_scripts', array( $this, 'disable_autosave' ) );
98 | add_filter( 'wp_insert_post_data', array( $this, 'sync_etherpad_content_to_wp' ), 10, 2 );
99 | add_filter( 'wp_insert_post', array( $this, 'catch_dummy_post' ), 10, 2 );
100 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
101 | }
102 |
103 | /**
104 | * Prevents setting WP's _edit_lock when on an Etherpad page
105 | *
106 | * Works a little funky because of the filters available in WP.
107 | * Summary:
108 | * 1) Filter get_post_metadata
109 | * 2) If the key is '_edit_lock', and if the $object_id is the
110 | * current post, return an empty value
111 | * 3) The "empty value" must be cast as an array if $single is false
112 | * 4) When get_metadata() sees the empty string come back, it returns
113 | * it, thus tricking the checker into thinking that there's no lock
114 | * 5) A side effect is that edit locks can't be set either, because
115 | * this filter kills the duplicate check in update_metadata()
116 | */
117 | public function prevent_check_edit_lock( $retval, $object_id, $meta_key, $single ) {
118 | if ( '_edit_lock' == $meta_key && ! empty( $this->wp_post_id ) && $this->wp_post_id == $object_id ) {
119 | $retval = $single ? '' : array( '' );
120 | }
121 |
122 | return $retval;
123 | }
124 |
125 | /**
126 | * Dequeues WP's autosave script, thereby disabling the feature
127 | *
128 | * No need for autosave here
129 | */
130 | public function disable_autosave() {
131 | wp_dequeue_script( 'autosave' );
132 | }
133 |
134 | /**
135 | * On WP post save, look to see whether there's a corresponding EP post,
136 | * and if found, sync the EP content into the WP post
137 | *
138 | * Note that this will overwrite local modifications.
139 | * @todo Refactor to use the correct sync mechanism
140 | */
141 | public function sync_etherpad_content_to_wp( $postdata ) {
142 | try {
143 | // We have to concatenaty the getText source
144 | // differently depending on whether this is a new or
145 | // existing post
146 | if ( isset( $_POST['participad_dummy_post_ID'] ) ) {
147 | $post_id = (int) $_POST['participad_dummy_post_ID'];
148 | $ep_post_id = get_post_meta( $post_id, 'ep_post_group_id', true ) . '$' . get_post_meta( $post_id, 'ep_post_id', true );
149 | } else {
150 | $ep_post_id = $this->current_post->ep_post_id_concat;
151 | }
152 |
153 | $text = participad_client()->getHTML( $ep_post_id );
154 | $postdata['post_content'] = $text->html;
155 | } catch ( Exception $e ) {}
156 |
157 | return $postdata;
158 | }
159 |
160 | /**
161 | * When creating a new post, we need to copy over the metadata from
162 | * the dummy WP post into the actual WP post
163 | */
164 | function catch_dummy_post( $post_ID, $post ) {
165 | if ( isset( $_POST['participad_dummy_post_ID'] ) ) {
166 | $dummy_post = get_post( $_POST['participad_dummy_post_ID'] );
167 | update_post_meta( $post_ID, 'ep_post_id', get_post_meta( $dummy_post->ID, 'ep_post_id', true ) );
168 | update_post_meta( $post_ID, 'ep_post_group_id', get_post_meta( $dummy_post->ID, 'ep_post_group_id', true ) );
169 |
170 | $dummy_session_key = 'ep_group_session_id-post_' . $dummy_post->ID;
171 | $post_session_key = 'ep_group_session_id-post_' . $post_ID;
172 | update_user_meta( $this->wp_user_id, $post_session_key, get_user_meta( $this->wp_user_id, $dummy_session_key, true ) );
173 | }
174 | }
175 |
176 | public function enqueue_scripts() {
177 | wp_enqueue_style( 'participad_editor', $this->module_url . 'css/dashboard.css' );
178 | wp_enqueue_script( 'participad_editor', $this->module_url . 'js/dashboard.js', array( 'jquery', 'editor' ) );
179 |
180 | $this->localize_script['url'] = $this->ep_iframe_url;
181 |
182 | wp_localize_script( 'participad_editor', 'Participad_Editor', $this->localize_script );
183 | }
184 |
185 | //////////////////
186 | // SETTINGS //
187 | //////////////////
188 |
189 | public function admin_page() {
190 | $enabled = participad_is_module_enabled( 'dashboard' );
191 |
192 | ?>
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
209 |
210 |
211 |
212 | set_module_path();
91 | $this->set_module_url();
92 |
93 | if ( ! participad_is_installed_correctly() ) {
94 | return new WP_Error( 'not_installed_correctly', 'Participad is not installed correctly.' );
95 | }
96 |
97 | // Set up the admin panels and save methods
98 | add_action( 'participad_admin_page', array( $this, 'admin_page' ) );
99 | add_action( 'participad_admin_page_save', array( $this, 'admin_page_save' ) );
100 | }
101 |
102 | public function start() {
103 | if ( ! $this->load_on_page() ) {
104 | return;
105 | }
106 |
107 | /**
108 | * Top-level overview:
109 | * 1) Figure out the current WP user
110 | * 2) Translate that into an EP user
111 | * 3) Figure out the current WP post ID
112 | * 4) Make sure we've got an EP group corresponding to the WP post
113 | * 5) Based on the EP user and group IDs, create a session
114 | * 6) Attempt connecting to the EP post with the session
115 | */
116 | $this->set_wp_user_id();
117 | $this->set_ep_user_id();
118 | $this->set_wp_post_id();
119 | $this->set_ep_post_group_id();
120 | $this->create_session();
121 | $this->set_ep_post_id();
122 | $this->set_ep_iframe_url();
123 |
124 | if ( isset( $this->ep_post_id ) ) {
125 | $this->post_ep_setup();
126 | }
127 | }
128 |
129 | public function set_module_path() {
130 | $this->module_path = trailingslashit( PARTICIPAD_PLUGIN_DIR . 'modules/' . $this->id );
131 | }
132 |
133 | public function set_module_url() {
134 | $this->module_url = trailingslashit( PARTICIPAD_PLUGIN_URL . 'modules/' . $this->id );
135 | }
136 |
137 | /**
138 | * Will an Etherpad instance appear on this page?
139 | *
140 | * No need to initialize the API client on every pageload
141 | *
142 | * Must be overridden in a module class
143 | *
144 | * @return bool
145 | */
146 | abstract public function load_on_page();
147 |
148 | /**
149 | * This is the method run after the EP post id is successfully set up
150 | */
151 | abstract public function post_ep_setup();
152 |
153 | /**
154 | * Set the current user WP user id property
155 | *
156 | * @since 1.0
157 | * @param bool|int $user_id If false, falls back on logged in user
158 | */
159 | public function set_wp_user_id( $wp_user_id = false ) {
160 | if ( false === $wp_user_id ) {
161 | $wp_user_id = get_current_user_id();
162 | }
163 |
164 | $this->wp_user_id = (int) $wp_user_id;
165 | }
166 |
167 | /**
168 | * Get the EP user id for a given WP user ID
169 | *
170 | * @since 1.0
171 | */
172 | public function set_ep_user_id() {
173 | if ( ! empty( $this->wp_user_id ) ) {
174 | $this->loggedin_user = new Participad_User( 'wp_user_id=' . $this->wp_user_id );
175 | $this->ep_user_id = $this->loggedin_user->ep_user_id;
176 | }
177 | }
178 |
179 | /**
180 | * Set the numeric ID of the current WP post
181 | *
182 | * There's no generic way to make this work. Your module must provide
183 | * an override for this method.
184 | */
185 | abstract public function set_wp_post_id();
186 |
187 | /**
188 | * Get the post group id
189 | *
190 | * Etherpad Lite's 'group' model does not map well onto WP's
191 | * approximation of ACL. So we create an EP group for each individual
192 | * post, and manage sessions dynamically
193 | */
194 | public function set_ep_post_group_id() {
195 | if ( ! empty( $this->wp_post_id ) ) {
196 | $this->current_post = new Participad_Post( 'wp_post_id=' . $this->wp_post_id );
197 | $this->ep_post_group_id = $this->current_post->ep_post_group_id;
198 | }
199 | }
200 |
201 | /**
202 | * Create a session that gives the current user access to this EP post
203 | */
204 | public function create_session() {
205 | if ( is_a( $this->loggedin_user, 'Participad_User' ) ) {
206 | $this->loggedin_user->create_session( $this->wp_post_id, $this->ep_post_group_id );
207 | }
208 | }
209 |
210 | /**
211 | * Look up the EP post id for the current WP post
212 | */
213 | public function set_ep_post_id() {
214 | if ( ! empty( $this->current_post ) ) {
215 | $this->ep_post_id = $this->current_post->ep_post_id;
216 | }
217 | }
218 |
219 | /**
220 | * Calculate the URL for the EP iframe
221 | */
222 | public function set_ep_iframe_url() {
223 | if ( $this->ep_post_group_id && $this->ep_post_id ) {
224 | $this->ep_iframe_url = add_query_arg( array(
225 | 'showControls' => 'true',
226 | 'showChat' => 'false',
227 | 'showLineNumbers' => 'false',
228 | 'useMonospaceFont' => 'false',
229 | ), participad_api_endpoint() . 'p/' . $this->ep_post_group_id . '%24' . $this->ep_post_id );
230 | }
231 | }
232 |
233 | /**
234 | * Replaces the content of the post with the EP iframe, plus other goodies
235 | *
236 | * To use this in your own Participad module, filter 'the_content'. Eg:
237 | *
238 | * add_filter( 'the_content', array( &$this, 'filter_content' ) );
239 | */
240 | public function filter_content( $content ) {
241 | $content = '';
242 | $content .= '';
243 | $content .= wp_nonce_field( 'participad_frontend_nonce', 'participad-frontend-nonce', true, false );
244 | return $content;
245 | }
246 |
247 | /**
248 | * Catches and process AJAX save requests
249 | *
250 | * @since 1.0
251 | */
252 | public function save_ajax_callback() {
253 | check_admin_referer( 'participad_frontend_nonce' );
254 |
255 | $p_post = new Participad_Post( 'wp_post_id=' . $_POST['post_id'] );
256 | $p_post->sync_wp_ep_content();
257 |
258 | die();
259 | }
260 |
261 | /**
262 | * Markup for the admin page
263 | *
264 | * Create the markup that'll appears on your module's section of the admin page
265 | *
266 | * This method is called automatically at the right time. You just need
267 | * to override it in your class.
268 | */
269 | public function admin_page() {}
270 |
271 | /**
272 | * Save changes on your admin page
273 | *
274 | * This method is hooked to participad_admin_page_save. Just catch the
275 | * $_POST global and do what you need to do
276 | */
277 | public function admin_page_save() {}
278 | }
279 |
280 | ?>
281 |
--------------------------------------------------------------------------------
/lib/etherpad-lite-client.php:
--------------------------------------------------------------------------------
1 | apiKey = $apiKey;
17 | if (isset($baseUrl)){
18 | $this->baseUrl = $baseUrl;
19 | }
20 | if (!filter_var($this->baseUrl, FILTER_VALIDATE_URL)){
21 | throw new InvalidArgumentException("[{$this->baseUrl}] is not a valid URL");
22 | }
23 | }
24 |
25 | protected function call($function, array $arguments = array()){
26 | $query = array_merge(
27 | array('apikey' => $this->apiKey),
28 | $arguments
29 | );
30 | $url = $this->baseUrl."/api/".self::API_VERSION."/".$function."?".http_build_query($query);
31 |
32 | if (function_exists('curl_init')){
33 | $c = curl_init($url);
34 | curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
35 | curl_setopt($c, CURLOPT_TIMEOUT, 20);
36 | $result = curl_exec($c);
37 | curl_close($c);
38 | } else {
39 | $result = file_get_contents($url);
40 | }
41 |
42 | if($result == ""){
43 | throw new UnexpectedValueException("Empty or No Response from the server");
44 | }
45 |
46 | $result = json_decode($result);
47 | if ($result === null){
48 | throw new UnexpectedValueException("JSON response could not be decoded");
49 | }
50 | return $this->handleResult($result);
51 | }
52 |
53 | protected function handleResult($result){
54 | if (!isset($result->code)){
55 | throw new RuntimeException("API response has no code");
56 | }
57 | if (!isset($result->message)){
58 | throw new RuntimeException("API response has no message");
59 | }
60 | if (!isset($result->data)){
61 | $result->data = null;
62 | }
63 |
64 | switch ($result->code){
65 | case self::CODE_OK:
66 | return $result->data;
67 | case self::CODE_INVALID_PARAMETERS:
68 | case self::CODE_INVALID_API_KEY:
69 | throw new InvalidArgumentException($result->message);
70 | case self::CODE_INTERNAL_ERROR:
71 | throw new RuntimeException($result->message);
72 | case self::CODE_INVALID_FUNCTION:
73 | throw new BadFunctionCallException($result->message);
74 | default:
75 | throw new RuntimeException("An unexpected error occurred whilst handling the response");
76 | }
77 | }
78 |
79 | // GROUPS
80 | // Pads can belong to a group. There will always be public pads that doesnt belong to a group (or we give this group the id 0)
81 |
82 | // creates a new group
83 | public function createGroup(){
84 | return $this->call("createGroup");
85 | }
86 |
87 | // this functions helps you to map your application group ids to etherpad lite group ids
88 | public function createGroupIfNotExistsFor($groupMapper){
89 | return $this->call("createGroupIfNotExistsFor", array(
90 | "groupMapper" => $groupMapper
91 | ));
92 | }
93 |
94 | // deletes a group
95 | public function deleteGroup($groupID){
96 | return $this->call("deleteGroup", array(
97 | "groupID" => $groupID
98 | ));
99 | }
100 |
101 | // returns all pads of this group
102 | public function listPads($groupID){
103 | return $this->call("listPads", array(
104 | "groupID" => $groupID
105 | ));
106 | }
107 |
108 | // creates a new pad in this group
109 | public function createGroupPad($groupID, $padName, $text){
110 | return $this->call("createGroupPad", array(
111 | "groupID" => $groupID,
112 | "padName" => $padName,
113 | "text" => $text
114 | ));
115 | }
116 |
117 | // AUTHORS
118 | // Theses authors are bind to the attributes the users choose (color and name).
119 |
120 | // creates a new author
121 | public function createAuthor($name){
122 | return $this->call("createAuthor", array(
123 | "name" => $name
124 | ));
125 | }
126 |
127 | // this functions helps you to map your application author ids to etherpad lite author ids
128 | public function createAuthorIfNotExistsFor($authorMapper, $name){
129 | return $this->call("createAuthorIfNotExistsFor", array(
130 | "authorMapper" => $authorMapper,
131 | "name" => $name
132 | ));
133 | }
134 |
135 | // SESSIONS
136 | // Sessions can be created between a group and a author. This allows
137 | // an author to access more than one group. The sessionID will be set as
138 | // a cookie to the client and is valid until a certian date.
139 |
140 | // creates a new session
141 | public function createSession($groupID, $authorID, $validUntil){
142 | return $this->call("createSession", array(
143 | "groupID" => $groupID,
144 | "authorID" => $authorID,
145 | "validUntil" => $validUntil
146 | ));
147 | }
148 |
149 | // deletes a session
150 | public function deleteSession($sessionID){
151 | return $this->call("deleteSession", array(
152 | "sessionID" => $sessionID
153 | ));
154 | }
155 |
156 | // returns informations about a session
157 | public function getSessionInfo($sessionID){
158 | return $this->call("getSessionInfo", array(
159 | "sessionID" => $sessionID
160 | ));
161 | }
162 |
163 | // returns all sessions of a group
164 | public function listSessionsOfGroup($groupID){
165 | return $this->call("listSessionsOfGroup", array(
166 | "groupID" => $groupID
167 | ));
168 | }
169 |
170 | // returns all sessions of an author
171 | public function listSessionsOfAuthor($authorID){
172 | return $this->call("listSessionsOfAuthor", array(
173 | "authorID" => $authorID
174 | ));
175 | }
176 |
177 | // PAD CONTENT
178 | // Pad content can be updated and retrieved through the API
179 |
180 | // returns the text of a pad
181 | // should take optional $rev
182 | public function getText($padID){
183 | return $this->call("getText", array(
184 | "padID" => $padID
185 | ));
186 | }
187 |
188 | // sets the text of a pad
189 | public function setText($padID, $text){
190 | return $this->call("setText", array(
191 | "padID" => $padID,
192 | "text" => $text
193 | ));
194 | }
195 |
196 | // PAD
197 | // Group pads are normal pads, but with the name schema
198 | // GROUPID$PADNAME. A security manager controls access of them and its
199 | // forbidden for normal pads to include a $ in the name.
200 |
201 | // creates a new pad
202 | public function createPad($padID, $text){
203 | return $this->call("createPad", array(
204 | "padID" => $padID,
205 | "text" => $text
206 | ));
207 | }
208 |
209 | // returns the number of revisions of this pad
210 | public function getRevisionsCount($padID){
211 | return $this->call("getRevisionsCount", array(
212 | "padID" => $padID
213 | ));
214 | }
215 |
216 | // deletes a pad
217 | public function deletePad($padID){
218 | return $this->call("deletePad", array(
219 | "padID" => $padID
220 | ));
221 | }
222 |
223 | // returns the read only link of a pad
224 | public function getReadOnlyID($padID){
225 | return $this->call("getReadOnlyID", array(
226 | "padID" => $padID
227 | ));
228 | }
229 |
230 | // sets a boolean for the public status of a pad
231 | public function setPublicStatus($padID, $publicStatus){
232 | return $this->call("setPublicStatus", array(
233 | "padID" => $padID,
234 | "publicStatus" => $publicStatus
235 | ));
236 | }
237 |
238 | // return true of false
239 | public function getPublicStatus($padID){
240 | return $this->call("getPublicStatus", array(
241 | "padID" => $padID
242 | ));
243 | }
244 |
245 | // returns ok or a error message
246 | public function setPassword($padID, $password){
247 | return $this->call("setPassword", array(
248 | "padID" => $padID,
249 | "password" => $password
250 | ));
251 | }
252 |
253 | // returns true or false
254 | public function isPasswordProtected($padID){
255 | return $this->call("isPasswordProtected", array(
256 | "padID" => $padID
257 | ));
258 | }
259 | }
260 |
261 |
--------------------------------------------------------------------------------
/includes/class-participad-post.php:
--------------------------------------------------------------------------------
1 | 0,
18 | 'ep_post_id' => 0,
19 | );
20 | $r = wp_parse_args( $args, $defaults );
21 |
22 | // WP post id always takes precedence
23 | if ( $r['wp_post_id'] ) {
24 | $this->wp_post_id = $r['wp_post_id'];
25 | $this->setup_postdata_from_wp_post_id();
26 | } else if ( $r['ep_post_id'] ) {
27 | $this->ep_post_id = $r['ep_post_id'];
28 | $this->setup_postdata_from_ep_post_id();
29 | }
30 | }
31 |
32 | /**
33 | * Given WP post id, set up the EP post
34 | *
35 | * We store this locally for efficiency's sake. When not found, query
36 | * the EP instance.
37 | *
38 | * @todo Need to create the function that goes in the other direction
39 | */
40 | protected function setup_postdata_from_wp_post_id() {
41 | // Set up a group for this post first
42 | $this->ep_post_group_id = get_post_meta( $this->wp_post_id, 'ep_post_group_id', true );
43 |
44 | if ( ! $this->ep_post_group_id ) {
45 | $post_group_id = self::create_ep_group( $this->wp_post_id, 'post' );
46 |
47 | if ( ! is_wp_error( $post_group_id ) ) {
48 | $this->ep_post_group_id = $post_group_id;
49 | update_post_meta( $this->wp_post_id, 'ep_post_group_id', $this->ep_post_group_id );
50 | }
51 | }
52 |
53 | // Now set up the post
54 | $this->ep_post_id = get_post_meta( $this->wp_post_id, 'ep_post_id', true );
55 |
56 | if ( ! $this->ep_post_id ) {
57 | $post_id = self::create_ep_post( $this->wp_post_id, $this->ep_post_group_id );
58 |
59 | if ( ! is_wp_error( $post_id ) ) {
60 | $this->ep_post_id = $post_id;
61 | update_post_meta( $this->wp_post_id, 'ep_post_id', $this->ep_post_id );
62 | }
63 | }
64 |
65 | // We need a concatenated id for API queries
66 | $this->ep_post_id_concat = $this->ep_post_group_id . '$' . $this->ep_post_id;
67 | }
68 |
69 | /**
70 | * Get the WP post object
71 | *
72 | * Handled separately because it's not needed for most uses, only at sync
73 | */
74 | function setup_wp_post() {
75 | if ( $this->wp_post_id ) {
76 | $wp_post = get_post( $this->wp_post_id );
77 | if ( ! empty( $wp_post ) && ! is_wp_error( $wp_post ) ) {
78 | $this->wp_post = $wp_post;
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * Create an EP group
85 | *
86 | * We use a mapper_type prefix to allow for future iterations of this
87 | * plugin where there are different kinds of mappers than 'type' (such
88 | * as BuddyPress groups)
89 | *
90 | * @param int $mapper_id The numeric ID of the mapped object (eg post)
91 | * @param string $mapper_type Eg 'post'
92 | * @return string|object The group id on success, or a WP_Error object
93 | * on failure
94 | */
95 | public static function create_ep_group( $mapper_id, $mapper_type ) {
96 | $group_mapper = $mapper_type . '_' . $mapper_id;
97 |
98 | try {
99 | $ep_post_group = participad_client()->createGroupIfNotExistsFor( $group_mapper );
100 | return $ep_post_group->groupID;
101 | } catch ( Exception $e ) {
102 | return new WP_Error( 'create_ep_post_group', __( 'Could not create the Etherpad Lite group.', 'participad' ) );
103 | }
104 | }
105 |
106 | public static function create_ep_post( $wp_post_id, $ep_post_group_id ) {
107 |
108 | $ep_post_id = self::generate_random_name();
109 | $pad_created = false;
110 |
111 | while ( !$pad_created ) {
112 | try {
113 | $wp_post = get_post( $wp_post_id );
114 | $wp_post_content = isset( $wp_post->post_content ) ? $wp_post->post_content : '';
115 | $ep_post = participad_client()->createGroupPad( $ep_post_group_id, $ep_post_id, $wp_post_content );
116 | $pad_created = true;
117 | } catch ( Exception $e ) {
118 |
119 | $error_message = $e->getMessage();
120 | $error_code = substr( $error_message, strrpos( $error_message, ':' ) + 2 );
121 |
122 | switch ( $error_code ) {
123 |
124 | // Request URI too long
125 | // @see https://github.com/boonebgorges/participad/issues/23
126 | case '414' :
127 | return new WP_Error( 'create_ep_post', __( 'Could not create the Etherpad Lite post', 'participad' ) );
128 | break;
129 |
130 | // Assume that there's a conflict, and try again
131 | default :
132 | $ep_post_id = self::generate_random_name();
133 | break;
134 |
135 | }
136 | }
137 | }
138 |
139 | return $ep_post_id;
140 | }
141 |
142 | /**
143 | * Gets a random ID. Hashed and salted so it can't be easily reverse engineered
144 | */
145 | public static function generate_random_name() {
146 | return wp_hash( uniqid() );
147 | }
148 |
149 | /**
150 | * Steps:
151 | * - Get lastEdited from EP and WP post_modified_gmt
152 | * - Get last_synced meta from WP postmeta
153 | * - If last_synced does not exist, set to date_created. Then, in the case of new posts, EP
154 | * content will copy over normally. In the case where there have been edits on the WP side,
155 | * reconciliation will proceed as expected
156 | * - If both WP and EP last_edited match last_synced, there's nothing to do
157 | * - If one of the last_edited matches last_synced, the other should be later. Overwrite the older
158 | * content with the new
159 | * - If neither matches last_synced, check to see whether the contents are different. If so, go
160 | * to reconciliation mode
161 | *
162 | */
163 | public function sync_wp_ep_content() {
164 | if ( $this->wp_post_id && $this->ep_post_id_concat ) {
165 |
166 | $last_synced = get_post_meta( $this->wp_post_id, 'ep_last_synced', true );
167 |
168 | if ( $sync_time = get_post_meta( $this->wp_post_id, '_ep_doing_sync', true ) ) {
169 | // If a sync has been running for more than 10 seconds,
170 | // assume it's failed
171 | if ( time() - $sync_time >= 10 ) {
172 | delete_post_meta( $this->wp_post_id, '_ep_doing_sync' );
173 | } else {
174 | // We're mid-sync, so bail
175 | return false;
176 | }
177 | }
178 |
179 | $this->setup_wp_post();
180 |
181 | // Unknown failure looking up post
182 | if ( ! $this->wp_post ) {
183 | return false;
184 | }
185 |
186 | update_post_meta( $this->wp_post_id, '_ep_doing_sync', time() );
187 |
188 | $wp_last_edited = strtotime( $this->wp_post->post_modified_gmt );
189 |
190 | // getLastEdited doesn't exist on older versions of EPL
191 | //$ep_last_edited = self::get_ep_post_last_edited( $this->ep_post_id_concat );
192 |
193 | // @todo There are issues with the way that EPL's API allows for pad text
194 | // to be set - stuff like HTML breaks the pad, and ruins user highlighting.
195 | // For the time being, EP content will never be overwritten. May revisit in
196 | // the future (see logic below)
197 | wp_update_post( array(
198 | 'ID' => $this->wp_post_id,
199 | 'post_content' => self::get_ep_post_content( $this->ep_post_id_concat ),
200 | ) );
201 |
202 | // It's possible that there will be a second or two lag, which will mean
203 | // that $ep_last_edited and post_modified_gmt will not match. To make
204 | // sure this doesn't break the next sync, set ep_last_synced to the
205 | // post_modified_gmt of the queried post. This way, if there's a mismatch,
206 | // it'll simply trigger a new sync
207 | $updated_post = get_post( $this->wp_post_id );
208 | $new_last_synced = strtotime( $updated_post->post_modified_gmt );
209 |
210 | /*
211 | // If there's no last_synced key, set it to the older of the edited dates
212 | if ( ! $last_synced ) {
213 | $last_synced = $wp_last_edited > $ep_last_edited ? $ep_last_edited : $wp_last_edited;
214 | }
215 |
216 | // Both last_edited stamps match last_synced. Nothing to do
217 | if ( $last_synced == $wp_last_edited && $last_synced == $ep_last_edited ) {
218 | return true;
219 |
220 | // WP matches, and EP is newer. Sync EP content to WP
221 | // This is the case with normal syncs
222 | } else if ( $last_synced == $wp_last_edited && $ep_last_edited > $wp_last_edited ) {
223 | wp_update_post( array(
224 | 'ID' => $this->wp_post_id,
225 | 'post_content' => self::get_ep_post_content( $this->ep_post_id_concat ),
226 | ) );
227 |
228 | // It's possible that there will be a second or two lag, which will mean
229 | // that $ep_last_edited and post_modified_gmt will not match. To make
230 | // sure this doesn't break the next sync, set ep_last_synced to the
231 | // post_modified_gmt of the queried post. This way, if there's a mismatch,
232 | // it'll simply trigger a new sync
233 | $updated_post = get_post( $this->wp_post_id );
234 | $new_last_synced = strtotime( $updated_post->post_modified_gmt );
235 |
236 | // EP matches, and WP is newer. Sync WP content to EP
237 | // This happens when you've made local, non-EP edits to the WP content
238 | } else if ( $last_synced == $ep_last_edited && $ep_last_edited < $wp_last_edited ) {
239 | self::set_ep_post_content( $this->ep_post_id_concat, $this->wp_post->post_content );
240 | $new_last_synced = self::get_ep_post_last_edited( $this->ep_post_id_concat );
241 |
242 | // Any other result means that there's been a mismatch of some sort -
243 | // there are unsynced EP and WP edits, or a local WP draft has been deleted,
244 | // or some other unknown issue. Send to manual mode
245 | } else {
246 | // @todo
247 | }
248 | */
249 |
250 | if ( isset( $new_last_synced ) ) {
251 | update_post_meta( $this->wp_post_id, 'ep_last_synced', $new_last_synced );
252 | }
253 |
254 | delete_post_meta( $this->wp_post_id, '_ep_doing_sync' );
255 | }
256 | }
257 |
258 | /**
259 | * Create an EP group session
260 | *
261 | * @param string Etherpad group id
262 | * @param string Etherpad user id
263 | * @return string|object The session id on success, or a WP_Error
264 | * object on failure
265 | */
266 | public static function create_ep_group_session( $ep_group_id, $ep_user_id ) {
267 | try {
268 | // @todo Do we need shorter expirations?
269 | $expiration = time() + ( 60 * 60 * 24 * 365 * 100 );
270 | $ep_session = participad_client()->createSession( $ep_group_id, $ep_user_id, $expiration );
271 | return $ep_session->sessionID;
272 | } catch ( Exception $e ) {
273 | return new WP_Error( 'create_ep_group_session', __( 'Could not create the Etherpad Lite session.', 'participad' ) );
274 | }
275 | }
276 |
277 | /**
278 | * Get the last edited date for a post, and return in standard UNIX format (minus microseconds)
279 | */
280 | public static function get_ep_post_last_edited( $ep_post_id ) {
281 | try {
282 | $last_edited = participad_client()->getLastEdited( $ep_post_id );
283 |
284 | // WP doesn't keep track of microseconds, so we have to strip them
285 | return (int) substr( $last_edited->lastEdited, 0, -3 );
286 | } catch ( Exception $e ) {
287 | return new WP_Error( 'get_ep_post_last_edited', __( 'Could not get the last edited date of this Etherpad Lite post', 'participad' ) );
288 | }
289 | }
290 |
291 | public static function get_ep_post_content( $ep_post_id ) {
292 | try {
293 | $content = participad_client()->getHTML( $ep_post_id );
294 | return $content->html;
295 | } catch ( Exception $e ) {
296 | return new WP_Error( 'get_ep_post_last_edited', __( 'Could not get the last edited date of this Etherpad Lite post', 'participad' ) );
297 | }
298 | }
299 |
300 | public static function set_ep_post_content( $ep_post_id, $post_content ) {
301 | try {
302 | $content = participad_client()->setText( $ep_post_id, $post_content );
303 | return $content->message;
304 | } catch ( Exception $e ) {
305 | return new WP_Error( 'get_ep_post_last_edited', __( 'Could not get the last edited date of this Etherpad Lite post', 'participad' ) );
306 | }
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/modules/notepad/notepad.php:
--------------------------------------------------------------------------------
1 | id = 'notepad';
19 |
20 | if ( is_wp_error( $this->init() ) ) {
21 | return;
22 | }
23 |
24 | if ( 'no' === participad_is_module_enabled( 'notepad' ) ) {
25 | return;
26 | }
27 |
28 | add_action( 'init', array( $this, 'set_post_type_name' ), 20 );
29 | add_action( 'init', array( $this, 'register_post_type' ), 30 );
30 |
31 | // Required files
32 | require( $this->module_path . 'widgets.php' );
33 |
34 | // BuddyPress integration should load at bp_init
35 | add_action( 'bp_init', array( $this, 'bp_integration' ) );
36 |
37 | // Load at 'wp', at which point the $wp_query global has been populated
38 | add_action( 'wp', array( $this, 'start' ), 1 );
39 | }
40 |
41 | /**
42 | * Post type name is abstracted out so it can be overridden as necessary
43 | *
44 | * @since 1.0
45 | */
46 | function set_post_type_name() {
47 | $this->post_type_name = apply_filters( 'participad_notepad_post_type_name', 'participad_notepad' );
48 | }
49 |
50 | /**
51 | * Registers the Notepad post type
52 | *
53 | * @since 1.0
54 | */
55 | function register_post_type() {
56 |
57 | $post_type_labels = apply_filters( 'participad_notepad_post_type_labels', array(
58 | 'name' => _x( 'Notepads', 'post type general name', 'participad' ),
59 | 'singular_name' => _x( 'Notepad', 'post type singular name', 'participad' ),
60 | 'add_new' => _x( 'Add New', 'add new', 'participad' ),
61 | 'add_new_item' => __( 'Add New Notepad', 'participad' ),
62 | 'edit_item' => __( 'Edit Notepad', 'participad' ),
63 | 'new_item' => __( 'New Notepad', 'participad' ),
64 | 'view_item' => __( 'View Notepad', 'participad' ),
65 | 'search_items' => __( 'Search Notepads', 'participad' ),
66 | 'not_found' => __( 'No Notepads found', 'participad' ),
67 | 'not_found_in_trash' => __( 'No Notepads found in Trash', 'participad' ),
68 | 'parent_item_colon' => ''
69 | ), $this );
70 |
71 | // Register the invitation post type
72 | register_post_type( $this->post_type_name, apply_filters( 'participad_notepad_post_type_args', array(
73 | 'label' => __( 'Notepads', 'participad' ),
74 | 'labels' => $post_type_labels,
75 | 'public' => true,
76 | 'show_ui' => current_user_can( 'manage_options' ),
77 | 'hierarchical' => false,
78 | 'supports' => array( 'title', 'editor', 'custom-fields' ),
79 | 'has_archive' => true,
80 | 'rewrite' => array(
81 | 'with_front' => false,
82 | 'slug' => 'notepads'
83 | ),
84 | ), $this ) );
85 | }
86 |
87 | /**
88 | * Will an Etherpad instance appear on this page?
89 | *
90 | * @return bool
91 | */
92 | public function load_on_page() {
93 | $queried_object = get_queried_object();
94 | return isset( $queried_object->post_type ) && $this->post_type_name == $queried_object->post_type;
95 | }
96 |
97 | /**
98 | * The WP post ID is easy to set in this case
99 | *
100 | * @since 1.0
101 | */
102 | public function set_wp_post_id() {
103 | $this->wp_post_id = get_the_ID();
104 | }
105 |
106 | /**
107 | * The setup functions that happen after the EP id has been determined:
108 | * - Enqueue styles/scripts
109 | * - Remove the Edit link
110 | * - Filter the_content to put the EP instance on the page
111 | *
112 | * @since 1.0
113 | */
114 | public function post_ep_setup() {
115 | if ( is_user_logged_in() && ! empty( $this->loggedin_user->ep_session_id ) ) {
116 | $this->enqueue_styles();
117 | $this->enqueue_scripts();
118 | add_filter( 'edit_post_link', array( &$this, 'edit_post_link' ), 10, 2 );
119 | add_action( 'the_content', array( $this, 'filter_content' ) );
120 | }
121 | }
122 |
123 | /**
124 | * Load the BuddyPress integration piece
125 | *
126 | * @since 1.0
127 | */
128 | public function bp_integration() {
129 | require( $this->module_path . 'bp-integration.php' );
130 | }
131 |
132 | /**
133 | * We don't need an Edit link on Notepads
134 | *
135 | * @since 1.0
136 | */
137 | public function edit_post_link( $link, $post_id ) {
138 | return '';
139 | }
140 |
141 | /**
142 | * When creating a new post, we need to copy over the metadata from
143 | * the dummy WP post into the actual WP post
144 | */
145 | function catch_dummy_post( $post_ID, $post ) {
146 | if ( isset( $_POST['participad_dummy_post_ID'] ) ) {
147 | $dummy_post = get_post( $_POST['participad_dummy_post_ID'] );
148 | update_post_meta( $post_ID, 'ep_post_id', get_post_meta( $dummy_post->ID, 'ep_post_id', true ) );
149 | update_post_meta( $post_ID, 'ep_post_group_id', get_post_meta( $dummy_post->ID, 'ep_post_group_id', true ) );
150 |
151 | $dummy_session_key = 'ep_group_session_id-post_' . $dummy_post->ID;
152 | $post_session_key = 'ep_group_session_id-post_' . $post_ID;
153 | update_user_meta( $this->wp_user_id, $post_session_key, get_user_meta( $this->wp_user_id, $dummy_session_key, true ) );
154 | }
155 | }
156 |
157 | public function enqueue_styles() {
158 | wp_enqueue_style( 'participad_notepad', $this->module_url . 'css/notepad.css' );
159 | }
160 |
161 | public function enqueue_scripts() {
162 | wp_enqueue_script( 'jquery' );
163 | wp_enqueue_script( 'schedule' );
164 | wp_enqueue_script( 'participad_frontend', PARTICIPAD_PLUGIN_URL . 'modules/frontend/js/frontend.js', array( 'jquery' ) );
165 | wp_enqueue_script( 'participad_notepad', $this->module_url . 'js/notepad.js', array( 'jquery', 'participad_frontend', 'schedule' ) );
166 | wp_localize_script( 'participad_notepad', 'Participad_Notepad', array(
167 | 'autosave_interval' => participad_notepad_autosave_interval(),
168 | ) );
169 | }
170 |
171 | //////////////////
172 | // SETTINGS //
173 | //////////////////
174 |
175 | public function admin_page() {
176 | $enabled = participad_is_module_enabled( 'notepad' );
177 |
178 | ?>
179 |
180 |
181 |
182 |
183 |
184 |