├── modules ├── frontend │ ├── css │ │ └── frontend.css │ ├── js │ │ └── frontend.js │ └── frontend.php ├── notepad │ ├── css │ │ └── notepad.css │ ├── js │ │ └── notepad.js │ ├── bp-integration.php │ ├── widgets.php │ └── notepad.php └── dashboard │ ├── css │ └── dashboard.css │ ├── js │ └── dashboard.js │ └── dashboard.php ├── readme.md ├── README.textile ├── readme.txt ├── includes ├── functions.php ├── class-participad-client.php ├── class-participad-user.php ├── admin.php ├── class-participad-integration.php └── class-participad-post.php ├── participad.php └── lib └── etherpad-lite-client.php /modules/frontend/css/frontend.css: -------------------------------------------------------------------------------- 1 | #participad-ep-iframe { 2 | width: 100%; 3 | min-height: 400px; 4 | border: 1px solid #ccc; 5 | } 6 | -------------------------------------------------------------------------------- /modules/notepad/css/notepad.css: -------------------------------------------------------------------------------- 1 | #participad-ep-iframe { 2 | width: 100%; 3 | min-height: 400px; 4 | border: 1px solid #ccc; 5 | } 6 | -------------------------------------------------------------------------------- /modules/dashboard/css/dashboard.css: -------------------------------------------------------------------------------- 1 | .participad-active .switch-participad { 2 | background-color: #E9E9E9; 3 | border-color: #CCCCCC #CCCCCC #E9E9E9; 4 | color: #333333; 5 | } 6 | 7 | #wp-content-editor-container iframe { 8 | width: 100%; height: 400px; 9 | } 10 | -------------------------------------------------------------------------------- /modules/notepad/js/notepad.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready( function($) { 2 | var post_id = $('#participad-frontend-post-id').val(); 3 | var wpnonce = $('#participad-frontend-nonce').val(); 4 | 5 | autosavePeriodical = $.schedule({ 6 | time: Participad_Notepad.autosave_interval * 1000, 7 | func: function() { 8 | participad_frontend_save( post_id, wpnonce ); 9 | }, 10 | repeat: true, 11 | protect: true 12 | }); 13 | },(jQuery)); 14 | 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Participad 2 | --------------------------------- 3 | 4 | * Contributors: boonebgorges 5 | * Tags: etherpad, etherpad lite, live, colaboration, lite 6 | * Tested up to: __3.5__ 7 | * Stable tag: coming soon 8 | * Requires at least: __3.0__ 9 | 10 | ## Description 11 | 12 | Collaborate in real time on WordPress content 13 | 14 | ## Installation 15 | 16 | 1. Copy the `participad` directory into the plugins folder of your WP installation 17 | 1. Activate the plugin 18 | 1. Navigate to Settings > Participad and enter the API details for your Etherpad Lite installation 19 | 20 | ## Credits 21 | 22 | Participad was originally developed for [THATCamp](http://thatcamp.org), with the support of the [Roy Rosenzweig Center for History and New Media](http://chnm.gmu.edu). 23 | 24 | ## Links 25 | 26 | [ETHERPAD](http://etherpad.org). 27 | 28 | -------------------------------------------------------------------------------- /modules/frontend/js/frontend.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready( function($) { 2 | var post_id = $('#participad-frontend-post-id').val(); 3 | var wpnonce = $('#participad-frontend-nonce').val(); 4 | 5 | $(window).bind('beforeunload', function() { 6 | participad_frontend_save( post_id, wpnonce ); 7 | }); 8 | 9 | $('body').on('click', 'a', function(e) { 10 | e.preventDefault(); 11 | var goto_href = this.href; 12 | participad_frontend_save( post_id, wpnonce, function() { window.location = goto_href; } ); 13 | }); 14 | }, (jQuery)); 15 | 16 | function participad_frontend_save( post_id, wpnonce, callback ) { 17 | if ( ! callback ) { 18 | callback = function() { return; } 19 | } 20 | 21 | jQuery.ajax({ 22 | type: 'POST', 23 | url: Participad_Frontend.ajaxurl, 24 | data: { 25 | action: 'participad_frontend_save', 26 | _wpnonce: wpnonce, 27 | post_id: post_id 28 | }, 29 | success: function(r) { 30 | callback(); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | === WP Super Cache === 2 | Contributors: *waiting* 3 | Tags: etherpad, live, colaboration, lite 4 | Tested up to: 3.2 5 | Stable tag: *coming soon* 6 | Requires at least: 3.* 7 | 8 | Etherpad Lite integration for blogs. 9 | 10 | == Description == 11 | 12 | = Recommended Settings = 13 | 14 | == Upgrade Notice == 15 | 16 | == Changelog == 17 | 18 | == 0.0.1 == 19 | * Project initiated 20 | * Waiting code input 21 | 22 | == Installation == 23 | 1. Copy the files into your plugins forlder ... 24 | 25 | == How to uninstall WP Super Cache == 26 | 27 | 28 | == Frequently Asked Questions == 29 | 30 | = Does this really work? = 31 | 32 | Will do sometime soon. 33 | 34 | = Troubleshooting = 35 | 36 | == Self-Host == 37 | 38 | It's complicated. 39 | 40 | 41 | == Links == 42 | [ETHERPAD](http://etherpad.org). 43 | 44 | == Updates == 45 | Updates to the plugin will be posted here, to [Robert Zimtea](http://robert.zinte.ie/) and the [EtherPad Foundation](http://etherpad.org) 46 | 47 | == Thanks == 48 | -------------------------------------------------------------------------------- /modules/dashboard/js/dashboard.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function($){ 2 | // Add our tab and remove the others 3 | var ed_tools = $('#wp-content-editor-tools'); 4 | $(ed_tools).children('a.wp-switch-editor').remove(); 5 | $(ed_tools).prepend('Participad'); 6 | 7 | // Change the CSS selector 8 | $('#wp-content-wrap').removeClass('html-active'); 9 | $('#wp-content-wrap').removeClass('tmce-active'); 10 | $('#wp-content-wrap').addClass('participad-active'); 11 | 12 | // Swap out the editor 13 | var ed_cont = $('#wp-content-editor-container'); 14 | $(ed_cont).children().remove(); 15 | $(ed_cont).append(''); 16 | 17 | if ( Participad_Editor.dummy_post_ID ) { 18 | $(ed_cont).after(''); 19 | } 20 | },(jQuery)); 21 | 22 | /** 23 | * This code is not currently in use. I have to find a way to toss content 24 | * between tabs in a way that's reliable. 25 | */ 26 | var participad = { 27 | switch_to_ep: function() { 28 | var ed, from_mode, wrap_id, textarea_el, dom; 29 | 30 | ed = tinyMCE.get('content'); 31 | dom = tinymce.DOM; 32 | wrap_id = 'wp-content-wrap'; 33 | textarea_el = dom.get('content'); 34 | 35 | if ( ed && !ed.isHidden() ) { 36 | from_mode = 'tmce'; 37 | } else if ( textarea_el.style.display != 'none' ) { 38 | from_mode = 'html'; 39 | } else { 40 | from_mode = 'participad'; 41 | } 42 | 43 | if ( 'participad' != from_mode ) { 44 | dom.removeClass(wrap_id, from_mode + '-active'); 45 | dom.addClass(wrap_id, 'participad-active'); 46 | setUserSetting('editor', 'participad'); 47 | 48 | } 49 | 50 | console.log(from_mode); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Participad === 2 | Contributors: boonebgorges 3 | Donate link: http://teleogistic.net/donate 4 | Tags: collaboration, collaborate, realtime, Google Docs, real time, synchronous, editor, Etherpad, Etherpad Lite, live 5 | Requires at least: 3.4 6 | Tested up to: 3.6 7 | Stable tag: 1.0.3 8 | License: GPLv3 9 | License URI: http://www.gnu.org/licenses/gpl-3.0.html 10 | 11 | Realtime collaborative editing for WordPress content, powered by Etherpad Lite. 12 | 13 | == Description == 14 | 15 | [Participad](http://participad.org) enables realtime, collaborative editing on WordPress content. Co-write and co-edit content, totally synchronously, and watch as the other people's text appears instantly on the screen. 16 | 17 | Powered by [Etherpad Lite](http://etherpad.org). 18 | 19 | Visit [http://participad.org/features/](http://participad.org/features/) to learn more about Participad's features, or watch the following video to see Participad in action. 20 | 21 | http://vimeo.com/52354978 22 | 23 | == Installation == 24 | 25 | Visit [http://participad.org/faqs/](http://participad.org/faqs/) for installation instructions. 26 | 27 | == Frequently Asked Questions == 28 | 29 | Visit [http://participad.org/faqs/](http://participad.org/faqs/) for FAQs. 30 | 31 | Take Participad for a test drive at the [Participad Demo site](http://participad.org/demo/). 32 | 33 | == Screenshots == 34 | 35 | 1. Participad in action 36 | 37 | == Changelog == 38 | 39 | = 1.0.3 = 40 | * Fixed bug that caused fatal errors on activation if no previous endpoint was defined 41 | * Added "(Participad)" to the beginning of Notepad widget names, for greater clarity 42 | * Block Dashboard edit access to Notepads for non-admins, to reduce confusion about their status 43 | * Added ability for Multisite admins to create a filter on default module settings 44 | * Improved handling of the way rewrite rules are flushed 45 | * Filters cookie domain, for better performance on subdirectory setups 46 | 47 | = 1.0.2 = 48 | * Fixed problem that may have caused out-of-control loops when API calls resulted in 40x codes 49 | * Fixed bug that created malformed API endpoint URIs in some cases 50 | 51 | = 1.0.1 = 52 | * Fixed some problems in plugin header and in readme 53 | 54 | = 1.0 = 55 | * Initial release 56 | -------------------------------------------------------------------------------- /includes/functions.php: -------------------------------------------------------------------------------- 1 | getText( $ep_post_id ); 9 | * 10 | * @since 1.0 11 | */ 12 | function participad_client() { 13 | return participad_Client::instance(); 14 | } 15 | 16 | /** 17 | * Returns the API key, as set either in the DB or in a constant 18 | * 19 | * At the moment, the plugin saves its API info on a blog-by-blog basis. For 20 | * use on a WP network, you should define the constants in wp-config.php to 21 | * avoid having to set the values manually on each blog. 22 | * 23 | * @return string $api_key 24 | */ 25 | function participad_api_key() { 26 | $api_key = ''; 27 | 28 | if ( defined( 'PARTICIPAD_API_KEY' ) ) { 29 | $api_key = PARTICIPAD_API_KEY; 30 | } else { 31 | $api_key = get_option( 'ep_api_key' ); 32 | } 33 | 34 | return $api_key; 35 | } 36 | 37 | /** 38 | * Returns the API endpoint, as set either in the DB or in a constant 39 | * 40 | * At the moment, the plugin saves its API info on a blog-by-blog basis. For 41 | * use on a WP network, you should define the constants in wp-config.php to 42 | * avoid having to set the values manually on each blog. 43 | * 44 | * @return string $api_endpoint 45 | */ 46 | function participad_api_endpoint() { 47 | $api_endpoint = ''; 48 | 49 | if ( defined( 'PARTICIPAD_API_ENDPOINT' ) ) { 50 | $api_endpoint = PARTICIPAD_API_ENDPOINT; 51 | } else { 52 | $api_endpoint = get_option( 'ep_api_endpoint' ); 53 | } 54 | 55 | if ( ! empty( $api_endpoint ) ) { 56 | $api_endpoint = trailingslashit( $api_endpoint ); 57 | } 58 | 59 | return $api_endpoint; 60 | } 61 | 62 | /** 63 | * Is Participad installed correctly? 64 | */ 65 | function participad_is_installed_correctly() { 66 | $is_installed_correctly = true; 67 | 68 | $api_endpoint = participad_api_endpoint(); 69 | $api_key = participad_api_key(); 70 | 71 | if ( ! $api_endpoint || ! $api_key ) { 72 | $is_installed_correctly = false; 73 | } 74 | 75 | if ( ! method_exists( participad_client(), 'is_connected' ) || ! participad_client()->is_connected() ) { 76 | $is_installed_correctly = false; 77 | } 78 | 79 | return $is_installed_correctly; 80 | } 81 | 82 | /** 83 | * Determine whether a given module is enabled 84 | * 85 | * Note that this returns 'yes' or 'no', NOT a bool, for boring reasons 86 | */ 87 | function participad_is_module_enabled( $module_name = '' ) { 88 | $setting = get_option( 'participad_' . $module_name . '_enable' ); 89 | 90 | if ( ! in_array( $setting, array( 'yes', 'no' ) ) ) { 91 | $setting = apply_filters( 'participad_is_module_enabled_default', 'yes', $module_name ); 92 | } 93 | 94 | return $setting; 95 | } 96 | -------------------------------------------------------------------------------- /participad.php: -------------------------------------------------------------------------------- 1 | setup_constants(); 24 | $this->includes(); 25 | $this->load_modules(); 26 | 27 | // Plugins should load themselves here 28 | do_action( 'participad_init', $this ); 29 | } 30 | 31 | /** 32 | * Define some constants 33 | * 34 | * These are provided primarily because it's necessary on some setups 35 | * to override WP's stupid plugin_dir_path() etc because of symlinks. 36 | */ 37 | function setup_constants() { 38 | if ( ! defined( 'PARTICIPAD_PLUGIN_DIR' ) ) { 39 | define( 'PARTICIPAD_PLUGIN_DIR', trailingslashit( dirname(__FILE__) ) ); 40 | } 41 | 42 | if ( ! defined( 'PARTICIPAD_PLUGIN_URL' ) ) { 43 | define( 'PARTICIPAD_PLUGIN_URL', WP_PLUGIN_URL . '/participad/' ); 44 | } 45 | } 46 | 47 | /** 48 | * Require necessary files 49 | */ 50 | function includes() { 51 | require PARTICIPAD_PLUGIN_DIR . 'includes/functions.php'; 52 | require PARTICIPAD_PLUGIN_DIR . 'includes/class-participad-integration.php'; 53 | require PARTICIPAD_PLUGIN_DIR . 'includes/class-participad-user.php'; 54 | require PARTICIPAD_PLUGIN_DIR . 'includes/class-participad-post.php'; 55 | require PARTICIPAD_PLUGIN_DIR . 'includes/class-participad-client.php'; 56 | 57 | if ( is_admin() ) { 58 | require PARTICIPAD_PLUGIN_DIR . 'includes/admin.php'; 59 | } 60 | } 61 | 62 | /** 63 | * Load the modules packaged with Participad 64 | * 65 | * @todo How will these be toggled? In the modules themselves? 66 | */ 67 | function load_modules() { 68 | if ( $modules_dir = opendir( PARTICIPAD_PLUGIN_DIR . 'modules' ) ) { 69 | while ( ( $file = readdir( $modules_dir ) ) !== false ) { 70 | if ( '.' == substr( $file, 0, 1 ) ) { 71 | continue; 72 | } 73 | 74 | if ( ! include( PARTICIPAD_PLUGIN_DIR . 'modules/' . $file . '/' . $file . '.php' ) ) { 75 | continue; 76 | } 77 | 78 | // Build class name 79 | $class_name = 'Participad_Integration_' . ucwords( $file ); 80 | 81 | if ( class_exists( $class_name ) ) { 82 | $this->modules[ $file ] = new $class_name; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Plugin bootstrap 91 | */ 92 | function participad() { 93 | return Participad::instance(); 94 | } 95 | add_action( 'plugins_loaded', 'participad' ); 96 | 97 | register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); 98 | -------------------------------------------------------------------------------- /modules/notepad/bp-integration.php: -------------------------------------------------------------------------------- 1 | post_type ) || participad_notepad_post_type_name() != $post->post_type ) { 24 | return; 25 | } 26 | 27 | // Throttle activity updates: No duplicate posts (same user, same 28 | // notepad) within 60 minutes 29 | $already_args = array( 30 | 'max' => 1, 31 | 'sort' => 'DESC', 32 | 'show_hidden' => 1, // We need to compare against all activity 33 | 'filter' => array( 34 | 'user_id' => get_current_user_id(), 35 | 'action' => 'participad_notepad_edited', 36 | 'secondary_id' => $post_id // We don't really care about the item_id for these purposes (it could have been changed) 37 | ), 38 | ); 39 | 40 | $already_activity = bp_activity_get( $already_args ); 41 | 42 | // If any activity items are found, compare its date_recorded with time() to 43 | // see if it's within the allotted throttle time. If so, don't record the 44 | // activity item 45 | if ( !empty( $already_activity['activities'] ) ) { 46 | $date_recorded = $already_activity['activities'][0]->date_recorded; 47 | $drunix = strtotime( $date_recorded ); 48 | if ( time() - $drunix <= apply_filters( 'participad_notepad_edit_activity_throttle_time', 60*60 ) ) 49 | return; 50 | } 51 | 52 | $post_permalink = get_permalink( $post_id ); 53 | $action = sprintf( __( '%1$s edited a notepad %2$s on the site %3$s', 'participad' ), bp_core_get_userlink( get_current_user_id() ), '' . esc_html( $post->post_title ) . '', '' . get_option( 'blogname' ) . '' ); 54 | 55 | $activity_id = bp_activity_add( array( 56 | 'user_id' => get_current_user_id(), 57 | 'component' => bp_is_active( 'blogs' ) ? $bp->blogs->id : 'blogs', 58 | 'action' => $action, 59 | 'primary_link' => $post_permalink, 60 | 'type' => 'participad_notepad_edited', 61 | 'item_id' => get_current_blog_id(), 62 | 'secondary_item_id' => $post_id, 63 | 'recorded_time' => $post->post_modified_gmt, 64 | 'hide_sitewide' => get_option( 'blog_public' ) <= 0, 65 | )); 66 | 67 | if ( function_exists( 'bp_blogs_update_blogmeta' ) ) { 68 | bp_blogs_update_blogmeta( get_current_blog_id(), 'last_activity', bp_core_current_time() ); 69 | } 70 | 71 | return $activity_id; 72 | } 73 | add_action( 'save_post', 'participad_notepad_record_notepad_activity', 10, 2 ); 74 | -------------------------------------------------------------------------------- /includes/class-participad-client.php: -------------------------------------------------------------------------------- 1 | $this->apiKey ), 44 | $arguments 45 | ); 46 | 47 | $url = $this->baseUrl . "api/" . self::API_VERSION . "/" . $function . "?" . http_build_query( $query ); 48 | 49 | $request = wp_remote_get( $url ); 50 | 51 | if ( is_wp_error( $request ) || empty( $request['body'] ) ) { 52 | $e = new UnexpectedValueException( "Empty or No Response from the server" ); 53 | $e->status = 'no_response'; 54 | throw $e; 55 | } 56 | 57 | if ( 200 !== $request['response']['code'] ) { 58 | throw new UnexpectedValueException( "Unknown error: " . $request['response']['code'] ); 59 | } 60 | 61 | $result = json_decode( $request['body'] ); 62 | 63 | if ( $result === null ) { 64 | throw new UnexpectedValueException( "JSON response could not be decoded" ); 65 | } 66 | 67 | return $this->handleResult($result); 68 | } 69 | 70 | /** 71 | * Only have to check connection quality once per load 72 | */ 73 | public function is_connected() { 74 | if ( isset( $this->connected ) && false !== $this->connected ) { 75 | return (bool) $this->connected; 76 | } 77 | 78 | try { 79 | $api_test = $this->getText( 1 ); 80 | if ( $api_test ) { 81 | $this->connected = 1; 82 | return true; 83 | } 84 | } catch ( Exception $e ) { 85 | if ( ! isset( $e->status ) || 'no_response' != $e->status ) { 86 | $this->connected = 1; 87 | return true; 88 | } else { 89 | $this->connected = 0; 90 | return false; 91 | } 92 | } 93 | } 94 | 95 | //////////////////////////// 96 | // ADDITIONAL API METHODS // 97 | //////////////////////////// 98 | 99 | /** 100 | * Get the last edited date for a pad 101 | */ 102 | public function getLastEdited( $padID ){ 103 | return $this->call("getLastEdited", array( 104 | "padID" => $padID 105 | )); 106 | } 107 | 108 | /** 109 | * Get the HTML content of a pad 110 | */ 111 | public function getHTML( $padID ){ 112 | return $this->call("getHTML", array( 113 | "padID" => $padID 114 | )); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /includes/class-participad-user.php: -------------------------------------------------------------------------------- 1 | 0, 17 | 'ep_user_id' => 0, 18 | ); 19 | $r = wp_parse_args( $args, $defaults ); 20 | 21 | // WP user id always takes precedence 22 | if ( $r['wp_user_id'] ) { 23 | $this->wp_user_id = $r['wp_user_id']; 24 | $this->setup_userdata_from_wp_user_id(); 25 | } else if ( $r['ep_user_id'] ) { 26 | $this->ep_user_id = $r['ep_user_id']; 27 | $this->setup_userdata_from_ep_user_id(); 28 | } 29 | } 30 | 31 | /** 32 | * Given WP user id, set up the user 33 | * 34 | * We store this locally for efficiency's sake. When not found, query 35 | * the EP instance. 36 | * 37 | * @todo Need to create the function that goes in the other direction 38 | */ 39 | protected function setup_userdata_from_wp_user_id() { 40 | $this->ep_user_id = get_user_meta( $this->wp_user_id, 'ep_user_id', true ); 41 | 42 | if ( ! $this->ep_user_id ) { 43 | $this->ep_user_id = self::create_ep_user( $this->wp_user_id ); 44 | } 45 | } 46 | 47 | /** 48 | * Create a session between this user and a given EP group 49 | * 50 | * @param int $wp_post_id This is used for setting the session key 51 | * @param string $ep_group_id 52 | */ 53 | public function create_session( $wp_post_id, $ep_group_id ) { 54 | // Sessions are user-post specific 55 | $session_key = 'ep_group_session_id-post_' . $wp_post_id; 56 | $this->ep_session_id = get_user_meta( $this->wp_user_id, $session_key, true ); 57 | 58 | if ( empty( $this->ep_session_id ) ) { 59 | $ep_session_id = Participad_Post::create_ep_group_session( $ep_group_id, $this->ep_user_id ); 60 | 61 | if ( ! is_wp_error( $ep_session_id ) ) { 62 | $this->ep_session_id = $ep_session_id; 63 | update_user_meta( $this->wp_user_id, $session_key, $this->ep_session_id ); 64 | } 65 | } 66 | 67 | if ( ! empty( $this->ep_session_id ) ) { 68 | // @todo Better expiration? 69 | 70 | // It's not possible to set cross-domain cookies. But 71 | // this filter will let you adjust for EP on a separate 72 | // subdomain 73 | $cookie_domain = apply_filters( 'participad_cookie_domain', '' ); 74 | setcookie( "sessionID", $this->ep_session_id, time() + ( 60*60*24*365*100 ), '/', $cookie_domain ); 75 | } 76 | } 77 | 78 | ///////////////////// 79 | // STATIC METHODS // 80 | ///////////////////// 81 | 82 | /** 83 | * Given a WP user ID, create a new EP user 84 | * 85 | * @param int $wp_user_id 86 | * @return string $ep_user_id 87 | */ 88 | public static function create_ep_user( $wp_user_id ) { 89 | $ep_user_id = ''; 90 | 91 | // Use display_name for the WP user 92 | $wp_user = new WP_User( $wp_user_id ); 93 | 94 | if ( is_a( $wp_user, 'WP_User' ) ) { 95 | 96 | try { 97 | $ep_user = participad_client()->createAuthorIfNotExistsFor( $wp_user->ID, $wp_user->display_name ); 98 | $ep_user_id = $ep_user->authorID; 99 | update_user_meta( $wp_user_id, 'ep_user_id', $ep_user_id ); 100 | return $ep_user_id; 101 | } catch ( Exception $e ) { 102 | return new WP_Error( 'create_ep_user', __( 'Could not create the Etherpad Lite user.', 'participad' ) ); 103 | } 104 | } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /modules/frontend/frontend.php: -------------------------------------------------------------------------------- 1 | id = 'frontend'; 19 | 20 | if ( is_wp_error( $this->init() ) ) { 21 | return; 22 | } 23 | 24 | if ( 'no' === participad_is_module_enabled( 'frontend' ) ) { 25 | return; 26 | } 27 | 28 | add_action( 'wp_ajax_participad_frontend_save', array( $this, 'save_ajax_callback' ) ); // @todo nopriv? 29 | 30 | // Load at 'wp', at which point the $wp_query global has been populated 31 | add_action( 'wp', array( $this, 'start' ), 1 ); 32 | } 33 | 34 | /** 35 | * Will an Etherpad instance appear on this page? 36 | * 37 | * @todo How to make this more fine-grained through user settings? 38 | * @return bool 39 | */ 40 | public function load_on_page() { 41 | return apply_filters( 'participad_frontend_load_on_page', true ); 42 | } 43 | 44 | /** 45 | * The setup functions that happen after the EP id has been determined: 46 | * - Filter the Edit link to point to the correct frontend URL 47 | * - Set up the filter on the_content, where necessary 48 | * - Enqueue styles/scripts 49 | * 50 | * @since 1.0 51 | */ 52 | public function post_ep_setup() { 53 | if ( is_user_logged_in() && ! empty( $this->loggedin_user->ep_session_id ) ) { 54 | add_filter( 'edit_post_link', array( &$this, 'edit_post_link' ), 10, 2 ); 55 | $this->maybe_filter_content(); 56 | $this->enqueue_scripts(); 57 | $this->enqueue_styles(); 58 | } 59 | } 60 | 61 | /** 62 | * The WP post ID is easy to set in this case 63 | */ 64 | public function set_wp_post_id() { 65 | $this->wp_post_id = get_the_ID(); 66 | } 67 | 68 | /** 69 | * Filter the Edit button on the front end of the post 70 | * 71 | * @since 1.0 72 | * @param string $link The original edit link 73 | * @param int $post_id The id of the post in question 74 | * @return string $link The new HTML link element 75 | */ 76 | public function edit_post_link( $link, $post_id ) { 77 | if ( empty( $_GET['participad_edit'] ) ) { 78 | $new_link = add_query_arg( 'participad_edit', '1', get_permalink( $post_id ) ); 79 | $link = preg_replace( '/href=".*?"/', 'href="' . $new_link . '"', $link ); 80 | } else { 81 | $new_link = remove_query_arg( 'participad_edit', get_permalink( $post_id ) ); 82 | $link = '' . __( 'Exit Edit Mode', 'participad' ) . ''; 83 | } 84 | 85 | return $link; 86 | } 87 | 88 | /** 89 | * Set up the filter_content filter, if necessary 90 | * 91 | * We don't want to create the Etherpad in every case. We must check 92 | * permissions first. 93 | * 94 | * @since 1.0 95 | */ 96 | public function maybe_filter_content() { 97 | global $post; 98 | 99 | if ( empty( $_GET['participad_edit'] ) ) { 100 | return; 101 | } 102 | 103 | $post_type_object = get_post_type_object( $post->post_type ); 104 | if ( empty( $post_type_object->cap->edit_post ) || ! current_user_can( $post_type_object->cap->edit_post, $post->ID ) ) { 105 | return; 106 | } 107 | 108 | // This is the main content filter that adds the Etherpad interface 109 | add_action( 'the_content', array( $this, 'filter_content' ) ); 110 | 111 | // Adds some additional text to the content box 112 | add_action( 'the_content', array( $this, 'filter_content_helptext' ), 20 ); 113 | } 114 | 115 | /** 116 | * Adds help text underneath Etherpad instances on the front end 117 | * 118 | * @since 1.0 119 | * 120 | * @param string $content The content, which should already have the 121 | * WP content swapped out with the iframe (see maybe_filter_content()) 122 | * @return string $content The content with our text appended 123 | */ 124 | public function filter_content_helptext( $content ) { 125 | $content .= '

' . sprintf( __( "You're in collaborative edit mode. Return to standard mode.", 'participad' ), remove_query_arg( 'participad_edit', get_permalink() ) ) . '

'; 126 | return $content; 127 | } 128 | 129 | /** 130 | * Enqueue necessary scripts 131 | * 132 | * @since 1.0 133 | */ 134 | public function enqueue_scripts() { 135 | wp_enqueue_script( 'jquery' ); 136 | wp_enqueue_script( 'participad_frontend', $this->module_url . 'js/frontend.js', array( 'jquery' ) ); 137 | wp_localize_script( 'participad_frontend', 'Participad_Frontend', array( 138 | 'ajaxurl' => admin_url( 'admin-ajax.php' ), 139 | ) ); 140 | } 141 | 142 | /** 143 | * Enqueue necessary styles 144 | * 145 | * @since 1.0 146 | */ 147 | public function enqueue_styles() { 148 | wp_enqueue_style( 'participad_frontend', $this->module_url . 'css/frontend.css' ); 149 | } 150 | 151 | public function admin_page() { 152 | $enabled = participad_is_module_enabled( 'frontend' ); 153 | 154 | ?> 155 | 156 |

157 | 158 |

159 | 160 | 161 | 162 | 165 | 166 | 172 | 173 |
163 | 164 | 167 | 171 |
174 | __( 'An easy interface for creating new Participad Notepads.', 'participad' ) 26 | ) 27 | ); 28 | } 29 | 30 | public function form( $instance ) { 31 | $title = isset( $instance['title'] ) ? $instance['title'] : __( 'Create a Notepad', 'participad' ); 32 | $use_packaged_css = isset( $instance['use_packaged_css'] ) && 'no' == $instance['use_packaged_css'] ? 'no' : 'yes'; 33 | 34 | ?> 35 | 36 |

37 | 38 | 39 |

40 | 41 |

42 | 43 | 47 | 48 | __( 'Displays Notepad info. When you\'re viewing a Notepad, shows info about associated posts. When you\'re viewing a post, shows info about associated notepads.', 'participad' ) 94 | ) 95 | ); 96 | } 97 | 98 | public function form( $instance ) { 99 | $title = isset( $instance['title'] ) ? $instance['title'] : __( 'Notepad Info', 'participad' ); 100 | $use_packaged_css = isset( $instance['use_packaged_css'] ) && 'no' == $instance['use_packaged_css'] ? 'no' : 'yes'; 101 | 102 | ?> 103 | 104 |

105 | 106 | 107 |

108 | 109 | 111 | 112 | 116 |

117 | 118 | 119 | ' . __( 'This Notepad is associated with the following post:', 'participad' ) . '

'; 141 | $associated_post = get_post( $associated_post_id ); 142 | $content .= ''; 143 | } else if ( ! participad_notepad_is_notepad() && $notepads = participad_notepad_post_has_notepad() ) { 144 | $content .= '

' . __( 'This post is associated with the following Notepads:', 'participad' ) . '

'; 145 | $content .= ''; 150 | } 151 | 152 | if ( $content ) { 153 | $title = $instance['title']; 154 | $use_packaged_css = ! isset( $instance['use_packaged_css'] ) || 'no' == $instance['use_packaged_css'] ? 'no' : 'yes'; 155 | 156 | if ( 'yes' == $use_packaged_css ) { 157 | echo ''; 160 | } 161 | 162 | echo $before_widget; 163 | 164 | if ( ! empty( $title ) ) { 165 | echo $before_title . $title . $after_title; 166 | } 167 | 168 | echo $content; 169 | echo $after_widget; 170 | } 171 | } 172 | } 173 | 174 | -------------------------------------------------------------------------------- /includes/admin.php: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 | 32 |
33 |

34 | 35 |

36 | 37 |

Participad is a bridge between your WordPress installation and an Etherpad Lite installation. Please enter the API authentication details for your Etherpad Lite installation below.', 'participad' ) ?>

38 | 39 |

Learn more about Etherpad Lite', 'participad' ) ?>

40 | 41 | 42 | 43 | 46 | 47 | 54 | 55 | 56 | 57 | 60 | 61 | 69 | 70 |
44 | 45 | 48 | disabled="disabled" name="participad_api_endpoint" value="" /> 49 | 50 | 51 |

PARTICIPAD_API_ENDPOINT is defined in wp-config.php.", 'participad' ) ?>

52 | 53 |
58 | 59 | 62 | disabled="disabled" name="participad_api_key" value="" /> 63 | 64 |

65 | 66 |

PARTICIPAD_API_KEY is defined in wp-config.php.", 'participad' ) ?>

67 | 68 |
71 | 72 | 73 |

74 | 75 |

Participad Modules are different ways of enabling your users to collaborate on WordPress content using Etherpad Lite.', 'participad' ) ?>

76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | " /> 84 |
85 | 86 |
87 | '; 136 | $html .= '

'; 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 | 203 | 204 | 210 | 211 |
201 | 202 | 205 | 209 |
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 | 185 | 186 | 189 | 190 | 196 | 197 |
187 | 188 | 191 | 195 |
198 | modules['notepad']->post_type_name; 218 | } 219 | 220 | /** 221 | * Is this a Notepad object? 222 | * 223 | * @since 1.0 224 | * @return bool 225 | */ 226 | function participad_notepad_is_notepad() { 227 | $queried_object = get_queried_object(); 228 | return isset( $queried_object->post_type ) && participad_notepad_post_type_name() == $queried_object->post_type; 229 | } 230 | 231 | /** 232 | * Get the autosave interval 233 | * 234 | * Falls back on WP's main AUTOSAVE_INTERVAL 235 | * 236 | * Set your own by defining PARTICIPAD_NOTEPAD_AUTOSAVE_INTERVAL, or by 237 | * filtering participad_notepad_autosave_interval. Note that the filter 238 | * always takes precedence. 239 | * 240 | * @since 1.0 241 | * @return int 242 | */ 243 | function participad_notepad_autosave_interval() { 244 | if ( defined( 'PARTICIPAD_NOTEPAD_AUTOSAVE_INTERVAL' ) ) { 245 | $interval = (int) PARTICIPAD_NOTEPAD_AUTOSAVE_INTERVAL; 246 | } 247 | 248 | if ( empty( $interval ) ) { 249 | $interval = AUTOSAVE_INTERVAL; 250 | } 251 | 252 | return apply_filters( 'participad_notepad_autosave_interval', $interval ); 253 | } 254 | 255 | /** 256 | * If this post has an associated Notepad, return its ids 257 | * 258 | * @since 1.0 259 | * @return array 260 | */ 261 | function participad_notepad_post_has_notepad( $post_id = 0 ) { 262 | $notepads = array(); 263 | 264 | if ( ! $post_id && is_single() ) { 265 | $post_id = get_the_ID(); 266 | } 267 | 268 | if ( ! $post_id ) { 269 | return $notepads; 270 | } 271 | 272 | $posts = get_posts( array( 273 | 'post_type' => participad_notepad_post_type_name(), 274 | 'meta_query' => array( 275 | array( 276 | 'key' => 'notepad_associated_post', 277 | 'value' => $post_id, 278 | ), 279 | ), 280 | 'post_status' => 'publish', 281 | 'posts_per_page' => -1, 282 | ) ); 283 | 284 | return $posts; 285 | } 286 | 287 | /** 288 | * Builds the HTML for the Create a Notepad widget and shortcode 289 | * 290 | * @param array $args See below for values 291 | * @return string $form The HTML form 292 | */ 293 | function participad_notepad_create_render( $args = array() ) { 294 | $r = wp_parse_args( $args, array( 295 | 'default_title' => '', 296 | 'default_associated_post' => '', 297 | 'use_packaged_css' => true, 298 | ) ); 299 | 300 | // Pull up a list of posts to populate the Link To field 301 | $associated_post_args = array( 302 | 'post_type' => array( 'post', 'page' ), 303 | 'post_status' => 'publish', 304 | 'posts_per_page' => -1, 305 | 'update_post_term_cache' => false, 306 | 'update_post_meta_cache' => false, 307 | 'orderby' => 'title', 308 | 'order' => 'ASC', 309 | ); 310 | $associated_posts = get_posts( $associated_post_args ); 311 | $associated_posts_options = ''; 312 | foreach( $associated_posts as $ap ) { 313 | $selected = $ap->ID == $r['default_associated_post'] ? ' selected="selected" ' : ''; 314 | $associated_posts_options .= ''; 315 | } 316 | 317 | $form = ''; 318 | 319 | if ( (bool) $r['use_packaged_css'] ) { 320 | $form .= ''; 329 | } 330 | 331 | // @todo 332 | if ( ! is_user_logged_in() ) { 333 | return sprintf( __( 'You log in to create Notepads.', 'participad' ), add_query_arg( 'redirect_to', wp_guess_url(), wp_login_url() ) ); 334 | } 335 | 336 | $form .= '
'; 337 | $form .= ''; 353 | $form .= ''; 354 | $form .= wp_nonce_field( 'participad_notepad_create', 'participad-notepad-nonce', true, false ); 355 | $form .= '
'; 356 | 357 | return $form; 358 | } 359 | 360 | /** 361 | * Registers our notepad_create shortcode 362 | */ 363 | function participad_notepad_create_shortcode( $atts ) { 364 | return participad_notepad_create_render( $atts ); 365 | } 366 | add_shortcode( 'notepad_create', 'participad_notepad_create_shortcode' ); 367 | 368 | /** 369 | * Catches and processes notepad creation requests 370 | */ 371 | function participad_notepad_create_catch() { 372 | if ( is_user_logged_in() && ! empty( $_POST['participad-create-submit'] ) ) { 373 | 374 | check_admin_referer( 'participad_notepad_create', 'participad-notepad-nonce' ); 375 | 376 | $errors = array(); 377 | 378 | if ( empty( $_POST['notepad-name'] ) ) { 379 | $errors['notepad_noname'] = '1'; 380 | } 381 | 382 | $associated_post = isset( $_POST['notepad-associated-post'] ) ? (int) $_POST['notepad-associated-post'] : 0; 383 | 384 | $notepad_id = participad_notepad_create_notepad( array( 385 | 'name' => $_POST['notepad-name'], 386 | 'associated_post' => $associated_post, 387 | 'author' => get_current_user_id(), 388 | ) ); 389 | 390 | if ( $notepad_id ) { 391 | $redirect = add_query_arg( 'notepad_created', '1', get_permalink( $notepad_id ) ); 392 | } else { 393 | $errors['notepad_misc'] = '1'; 394 | $redirect = add_query_arg( $errors, $_POST['_wp_http_referer'] ); 395 | } 396 | 397 | wp_safe_redirect( $redirect ); 398 | } 399 | } 400 | add_action( 'wp', 'participad_notepad_create_catch', 1 ); 401 | 402 | /** 403 | * Create a new notepad 404 | */ 405 | function participad_notepad_create_notepad( $args = array() ) { 406 | $r = wp_parse_args( $args, array( 407 | 'name' => '', 408 | 'associated_post' => '', 409 | 'author' => '', 410 | ) ); 411 | 412 | $notepad_id = wp_insert_post( array( 413 | 'post_author' => $r['author'], 414 | 'post_title' => $r['name'], 415 | 'post_status' => 'publish', 416 | 'post_type' => participad_notepad_post_type_name(), 417 | ) ); 418 | 419 | if ( $notepad_id ) { 420 | update_post_meta( $notepad_id, 'notepad_associated_post', $r['associated_post'] ); 421 | } 422 | 423 | return $notepad_id; 424 | } 425 | 426 | /** 427 | * Detect when a success/error message should be shown 428 | */ 429 | function participad_notepad_display_error( $content ) { 430 | $message = ''; 431 | 432 | // Admins can override these hardcoded styles 433 | if ( ! apply_filters( 'participad_notepad_suppress_error_styles', false ) ) { 434 | $message .= ' 435 | 440 | '; 441 | } 442 | 443 | if ( participad_notepad_is_notepad() && isset( $_GET['notepad_created'] ) ) { 444 | $message .= '
' . __( 'Notepad created', 'participad' ) . '
'; 445 | } 446 | 447 | if ( isset( $_GET['notepad_noname'] ) ) { 448 | $message .= '
' . __( 'Notepads must have a title', 'participad' ) . '
'; 449 | } 450 | 451 | if ( isset( $_GET['notepad_misc'] ) ) { 452 | $message .= '
' . __( 'Could not create the notepad', 'participad' ) . '
'; 453 | } 454 | 455 | return $message . $content; 456 | } 457 | add_filter( 'the_content', 'participad_notepad_display_error', 100 ); 458 | --------------------------------------------------------------------------------