diff --git a/README.md b/README.md new file mode 100644 index 0000000..6653993 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# DocumentCloud WordPress plugin + +The DocumentCloud WordPress plugin lets you embed [DocumentCloud](https://www.documentcloud.org/) resources into WordPress content using [shortcodes](https://codex.wordpress.org/Shortcode_API). + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html"] + +## Installation + +1. Upload the contents of the plugin to `wp-content/plugins/documentcloud` +2. Activate the plugin through the "Plugins" menu +3. In your posts, embed documents or notes using the DocumentCloud button or the `[documentcloud]` shortcode +4. Optional: Set a default width/height for all DocumentCloud embeds (which can be overridden on a per-embed basis with the `height/width` attributes) at Settings > DocumentCloud. (This default width will only be used if you set `responsive="false"` on an embed.) + +## Usage + +This plugin allows you embed DocumentCloud resources using a custom shortcode: + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html"] + +When you save, WordPress fetches and stores the actual embed code HTML from the DocumentCloud servers using oEmbed. You can freely toggle between visual and HTML mode without mangling embed code, and your embed will always be up to date with the latest embed code. + +By default, documents will have a responsive width (it will narrow and widen as necessary to fill available content area) and use the theme's default height. If you want to override this, you can either set `responsive="false"` or explicitly set a `width`: + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html" width="600"] + +You can set your own defaults in Settings > DocumentCloud, but default widths will be ignored unless `responsive` is disabled: + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html" responsive="false"] + +To embed a note, just use any note-specific URL. Notes ignore `width/height` and always act responsively: + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html#document/p1/a53674"] + +Here's the full list of embed options you can pass via shortcode attributes; some are specific to the type of resource you're embedding. + +### All resources (documents and notes): + +- `url` (**required**, string): Full URL of the DocumentCloud resource. +- `container` (string): ID of element to insert the embed into; if excluded, embedder will create its own container. + +### Documents only: + +- `height` (integer): Height (in pixels) of the embed. +- `width` (integer): Width (in pixels) of the embed. If used, will implicitly set `responsive="false"`. +- `responsive` (boolean): Use responsive layout, which dynamically adjusts width to fill content area. Defaults `true`. +- `responsive_offset` (integer): Distance (in pixels) to vertically offset the viewer for some responsive embeds. +- `default_page` (integer): Page number to have the document scroll to by default. +- `default_note` (integer): ID of the note that the document should highlight by default. +- `notes` (boolean): Show/hide notes: +- `search` (boolean): Hide or show search form. +- `sidebar` (boolean): Hide or show sidebar. Defaults `false`. +- `pdf` (boolean): Hide or show link to download original PDF. Defaults `true`. +- `text` (boolean): Hide or show text tab. Defaults `true`. +- `zoom` (boolean): Hide or show zoom slider. +- `format` (string): Indicate to the theme that this is a wide asset by setting this to `wide`. Defaults `normal`. + +You can read more about publishing and embedding DocumentCloud resources on https://www.documentcloud.org/help/publishing. + +## Caching + +Ideally, when WordPress hits our oEmbed service to fetch the embed code, it would obey the `cache_age` we return. Despite [conversation](https://core.trac.wordpress.org/ticket/14759) around this, it doesn't seem to. + +Instead, it lets us choose between no cache at all (so *every pageload* triggers a call to our oEmbed service to get the embed code) or a supposed 24-hour cache stored in the `postmeta` table. Unfortunately, [our tests](https://github.com/documentcloud/wordpress-documentcloud/issues/20) seem to show this cache is never expired, which means we can choose between no cache (thus possibly DDOSing ourselves) or a permanent cache (thus possibly having stale embed codes). We've chosen the latter; hopefully this cache does eventually expire, and our embed codes shouldn't change that often anyway. + +If you find yourself absolutely needing to expire the cache, though, you have two choices: + +1. Delete the appropriate `_oembed_*` rows from your `postmeta` table. +2. Modify the shortcode attributes for the embed, since this is recognized as a new embed by WordPress. + +## Changelog + +### 0.3 +* Add support for embedding notes. +* Default to responsive. +* Enable caching. + +### 0.2 +* Fetch embed code via oEmbed instead of generating statically. +* Add new options: `container`, `responsive`, `responsive_offset`, `default_page`, `default_note`, `notes`, `search`, and `zoom`. +* Deprecate `id` attribute. It's still usable, but support may drop in the future. Use `url` instead. + +### 0.1 +* Initial release. + +## License and History + +The DocumentCloud WordPress plugin is [GPLv2](http://www.gnu.org/licenses/gpl-2.0.html). Initial development of this plugin by Chris Amico (@eyeseast) supported by [NPR](http://www.npr.org) as part of [StateImpact](http://stateimpact.npr.org) project. Development continued by Justin Reese (@reefdog) at [DocumentCloud](https://www.documentcloud.org/). diff --git a/documentcloud.php b/documentcloud.php index ce2ac1a..793d728 100644 --- a/documentcloud.php +++ b/documentcloud.php @@ -3,7 +3,7 @@ * Plugin Name: DocumentCloud * Plugin URI: https://www.documentcloud.org/ * Description: Embed DocumentCloud resources in WordPress content. - * Version: 0.1.1 + * Version: 0.3 * Authors: Chris Amico, Justin Reese * License: GPLv2 ***/ @@ -26,20 +26,203 @@ */ class WP_DocumentCloud { + + const CACHING_ENABLED = true, + DEFAULT_EMBED_FULL_WIDTH = 940, + OEMBED_RESOURCE_DOMAIN = 'www.documentcloud.org', + OEMBED_PROVIDER = 'https://www.documentcloud.org/api/oembed.{format}', + DOCUMENT_PATTERN = '^(?Phttps?)://www\.documentcloud\.org/documents/(?P[0-9]+-[a-z0-9-]+)'; function __construct() { - add_shortcode('documentcloud', array(&$this, 'embed_shortcode')); - - add_action('init', array(&$this, 'register_tinymce_filters')); - - add_action('save_post', array(&$this, 'save')); - + add_action('init', array(&$this, 'register_dc_oembed_provider')); + add_shortcode('documentcloud', array(&$this, 'handle_dc_shortcode')); + add_filter('oembed_fetch_url', array(&$this, 'add_dc_arguments'), 10, 3); + + // Setup TinyMCE shortcode-generation plugin + // add_action('init', array(&$this, 'register_tinymce_filters')); + + // Setup admin settings add_action('admin_menu', array(&$this, 'add_options_page')); - add_action('admin_init', array(&$this, 'settings_init')); + + // Store metadata upon post save + add_action('save_post', array(&$this, 'save')); } + function register_dc_oembed_provider() { + /* + Hello developer. If you wish to test this plugin against your + local installation of DocumentCloud (with its own testing + domain), set the OEMBED_PROVIDER and OEMBED_RESOURCE_DOMAIN + constants above to your local testing domain. You'll also want + to uncomment the next line to let WordPress connect to local + domains. + */ + // add_filter( 'http_request_host_is_external', '__return_true'); + + wp_oembed_add_provider("http://" . WP_DocumentCloud::OEMBED_RESOURCE_DOMAIN . "/documents/*", WP_DocumentCloud::OEMBED_PROVIDER); + wp_oembed_add_provider("https://" . WP_DocumentCloud::OEMBED_RESOURCE_DOMAIN . "/documents/*", WP_DocumentCloud::OEMBED_PROVIDER); + } + + function get_default_sizes() { + $wp_embed_defaults = wp_embed_defaults(); + + $height = intval(get_option('documentcloud_default_height', $wp_embed_defaults['height'])); + $width = intval(get_option('documentcloud_default_width', $wp_embed_defaults['width'])); + $full_width = intval(get_option('documentcloud_full_width', WP_DocumentCloud::DEFAULT_EMBED_FULL_WIDTH)); + + return array ( + 'height' => $height, + 'width' => $width, + 'full_width' => $full_width, + ); + } + + // TODO: Add admin options to adjust all defaults. + function get_default_atts() { + $default_sizes = $this->get_default_sizes(); + + return array( + 'url' => null, + 'container' => null, + 'notes' => null, + 'responsive_offset' => null, + 'default_page' => null, + 'default_note' => null, + 'zoom' => null, + 'search' => null, + 'responsive' => 'true', + // The following defaults match the existing plugin, except + // `height/width` are prefixed `max*` per the oEmbed spec. + // You can still use `height/width` for backwards + // compatibility, but they'll be mapped to `max*`. + // Precedence (lower number == higher priority): + // 1. `width` on shortcode + // 2. `maxwidth` on shortcode + // 3. Settings > DocumentCloud > "Default embed width" + // 4. `wp_embed_defaults()['width']` + 'maxheight' => $default_sizes['height'], + 'maxwidth' => $default_sizes['width'], + 'format' => 'normal', + 'sidebar' => 'false', + 'text' => 'true', + 'pdf' => 'true', + ); + } + + function add_dc_arguments($provider, $url, $args) { + foreach ($args as $key => $value) { + switch ($key) { + case 'format': + case 'height': + case 'width': + case 'discover': + // Don't pass these attributes to the provider + break; + default: + $provider = add_query_arg( $key, $value, $provider ); + break; + } + } + return $provider; + } + + function handle_dc_shortcode($atts) { + $default_sizes = $this->get_default_sizes(); + $default_atts = $this->get_default_atts(); + + // Smooshes together passed-in shortcode attrs with defaults + // and filters to only those we accept. + $filtered_atts = shortcode_atts($default_atts, $atts); + + // Either the `url` or `id` attributes are required, but `id` + // is only supported for backwards compatibility. If it's used, + // we force this to embed a document. I.e., `id` can't be used + // for embedding notes, pages, or other non-document resources. + if (!$atts['url']) { + if (!$atts['id']) { + return ''; + } + else { + $url = $filtered_atts['url'] = "https://" . WP_DocumentCloud::OEMBED_RESOURCE_DOMAIN . "/documents/{$atts['id']}.html"; + } + } else { + // Some resources (like notes) have multiple possible + // user-facing URLs. We recompose them into a single form. + $url = $filtered_atts['url'] = $this->clean_dc_url($atts['url']); + } + + // `height/width` beat `maxheight/maxwidth`; see full precedence list in `get_default_atts()`. + if (isset($atts['height'])) { + $filtered_atts['maxheight'] = $atts['height']; + } + if (isset($atts['width'])) { + $filtered_atts['maxwidth'] = $atts['width']; + } + + // `responsive` defaults true, but our responsive layout + // ignores width declarations. If a user indicates a width and + // hasn't otherwise specifically indicated `responsive='true'`, + // it's safe to assume they expect us to respect the width, so + // we disable the responsive flag. + if ((isset($atts['width']) || isset($atts['maxwidth'])) && (string) $atts['responsive'] != 'true') { + $filtered_atts['responsive'] = 'false'; + } + + // If the format is set to wide, it blows away all other width + // settings. + if ($filtered_atts['format'] == 'wide') { + $filtered_atts['maxwidth'] = $default_sizes['full_width']; + } + + // For the benefit of some templates, notify template that + // we're requesting an asset wider than the default size. + global $post; + $is_wide = intval($filtered_atts['maxwidth']) > $default_sizes['width']; + + if (WP_DocumentCloud::CACHING_ENABLED) { + // This lets WordPress cache the result of the oEmbed call. + // Thanks to http://bit.ly/1HykA0U for this pattern. + global $wp_embed; + return $wp_embed->shortcode($filtered_atts, $url); + } else { + return wp_oembed_get($url, $filtered_atts); + } + + } + + function parse_dc_url($url) { + $patterns = array( + // Document + '{' . WP_DocumentCloud::DOCUMENT_PATTERN . '\.html$}', + // Notes and note variants + '{' . WP_DocumentCloud::DOCUMENT_PATTERN . '/annotations/(?P[0-9]+)\.(html|js)$}', + '{' . WP_DocumentCloud::DOCUMENT_PATTERN . '.html#document/p([0-9]+)/a(?P[0-9]+)$}', + '{' . WP_DocumentCloud::DOCUMENT_PATTERN . '.html#annotation/a(?P[0-9]+)$}', + ); + + $elements = array(); + foreach ($patterns as $pattern) { + $perfect_match = preg_match($pattern, $url, $elements); + if ($perfect_match) { + break; + } + } + return $elements; + } + + function clean_dc_url($url) { + $elements = $this->parse_dc_url($url); + if ($elements['document_slug']) { + $url = "{$elements['protocol']}://" . WP_DocumentCloud::OEMBED_RESOURCE_DOMAIN . "/documents/{$elements['document_slug']}" . + ($elements['note_id'] ? "/annotations/{$elements['note_id']}" : '') . '.html'; + } + return $url; + } + + // Setup TinyMCE shortcode button + function register_tinymce_filters() { add_filter('mce_external_plugins', array(&$this, 'add_tinymce_plugin') @@ -62,21 +245,8 @@ function register_button($buttons) { return $buttons; } - function get_defaults() { - // add admin options to adjust these defaults - // storing js params as strings instead of real booleans - return array( - 'url' => null, - 'id' => null, - 'height' => get_option('documentcloud_default_height', 600), - 'width' => get_option('documentcloud_default_width', 620), - 'format' => 'normal', - 'sidebar' => 'false', - 'text' => 'true', - 'pdf' => 'true' - ); - } - + // Setup settings for plugin + function add_options_page() { add_options_page('DocumentCloud', 'DocumentCloud', 'manage_options', 'documentcloud', array(&$this, 'render_options_page')); @@ -85,12 +255,14 @@ function add_options_page() { function render_options_page() { ?>

DocumentCloud Options

+ +

Any widths set here will only take effect if you set responsive="false" on an embed.

-
+ "; + $default_sizes = $this->get_default_sizes(); + echo ""; } function default_width_field() { - $option = intval(get_option('documentcloud_default_width', 620)); - echo ""; + $default_sizes = $this->get_default_sizes(); + echo ""; } function full_width_field() { - $option = intval(get_option('documentcloud_full_width', 620)); - echo ""; + $default_sizes = $this->get_default_sizes(); + echo ""; } function settings_section() {} @@ -140,7 +312,8 @@ function save($post_id) { )) ) { return; } - $defaults = $this->get_defaults(); + $default_sizes = $this->get_default_sizes(); + $default_atts = $this->get_default_atts(); $wide_assets = get_post_meta($post_id, 'wide_assets', true); $documents = get_post_meta($post_id, 'documentcloud', true); $matches = array(); @@ -150,98 +323,36 @@ function save($post_id) { $args = $matches[3]; foreach($tags as $i => $tag) { if ($tag == "documentcloud") { - $atts = shortcode_parse_atts($args[$i]); - $atts = shortcode_atts($defaults, $atts); + $parsed_atts = shortcode_parse_atts($args[$i]); + $atts = shortcode_atts($default_atts, $parsed_atts); // get a doc id to keep array keys consistent - if (isset($atts['url']) && !isset($atts['id']) ) { - $atts['id'] = $this->parse_id_from_url($atts['url']); + if (isset($atts['url'])) { + $elements = $this->parse_dc_url($atts['url']); + $meta_key = $elements['document_slug']; + if ($elements['note_id']) { + $meta_key .= "-{$elements['note_id']}"; + } + } else if (isset($atts['id'])) { + $meta_key = $atts['id']; } // if no id, don't bother storing because it's wrong - if ($atts['id'] != null) { - if ($atts['format'] == "wide" || $atts['width'] > $defaults['width']) { - $wide_assets[$atts['id']] = true; + if ($meta_key) { + $width = intval(isset($parsed_atts['width']) ? $parsed_atts['width'] : $atts['maxwidth']); + if ($atts['format'] == "wide" || $width > $default_sizes['width']) { + $wide_assets[$meta_key] = true; } else { - $wide_assets[$atts['id']] = false; + $wide_assets[$meta_key] = false; } - - $documents[$atts['id']] = $atts; - + $documents[$meta_key] = $atts; } } } update_post_meta($post_id, 'documents', $documents); update_post_meta($post_id, 'wide_assets', $wide_assets); - } - function parse_id_from_url($url) { - $regex = '{^https://www\.documentcloud\.org/documents/(?P.+)\.html}'; - $matches = array(); - if (preg_match($regex, $url, $matches)) { - return $matches['id']; - } else { - return null; - } - } - - function embed_shortcode($atts, $content, $code) { - global $post; - $defaults = $this->get_defaults(); - extract(shortcode_atts($defaults, $atts)); - - // we need a document ID or URL, or it's a no op - if ($url && !$id) { - // parse id from url - $id = $this->parse_id_from_url($url); - } - - // still no id? nothin doing - if (!$id) return; - - // we only deal with integers - $height = intval($height); - $width = intval($width); - if ($format == 'wide') { - $width = get_option('documentcloud_full_width', 940); - } - - $is_wide = $width > $defaults['width']; - - // full control in single templates - if (is_single() || is_page()) { - return " -
- - "; - - } else { - // index view is always normal width, no sidebar - return " -
- - "; - } - } } new WP_DocumentCloud; diff --git a/js/window.php b/js/window.php index 4d7bdaa..4c7d27d 100644 --- a/js/window.php +++ b/js/window.php @@ -6,6 +6,9 @@ } $SITEURL .= $_SERVER[ 'HTTP_HOST' ] or $_SERVER[ 'SERVER_NAME' ]; $SITEURL .= $_GET[ 'wpbase' ]; +if (substr($SITEURL, -1) != '/') { + $SITEURL .= '/'; +} ?> diff --git a/readme.txt b/readme.txt index a9a80d4..555ec3b 100644 --- a/readme.txt +++ b/readme.txt @@ -1,47 +1,87 @@ === DocumentCloud === Contributors: chrisamico, reefdog -Tags: documentcloud, documents +Tags: documentcloud, documents, journalism, reporting, research Requires at least: 3.2 -Tested up to: 3.9.1 +Tested up to: 4.2.1 Stable tag: trunk +License: GPLv2 +License URI: http://www.gnu.org/licenses/gpl-2.0.html Embed DocumentCloud resources in WordPress content. == Description == -[DocumentCloud](https://www.documentcloud.org/) is a free service allowing journalists to analyze, annotate and publish documents, funded by the Knight Foundation. Initial development of this plugin supported by [NPR](http://www.npr.org) as part of [StateImpact](http://stateimpact.npr.org) project. +[DocumentCloud](https://www.documentcloud.org/) is a service that allows journalists to analyze, annotate and publish documents, hosted by Investigative Reporters & Editors. Initial development of this plugin supported by [NPR](http://www.npr.org) as part of [StateImpact](http://stateimpact.npr.org) project. -DocumentCloud's normal embed code looks like this: +This plugin allows you embed DocumentCloud resources using a custom shortcode: -
- - - -That works great as long as you edit in HTML mode. Switch to the visual editor, and your container `div` disappears and your JavaScript is broken. + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html"] -To get around this, use this short code: +When you save, WordPress fetches and stores the actual embed code HTML from the DocumentCloud servers using oEmbed. You can freely toggle between visual and HTML mode without mangling embed code, and your embed will always be up to date with the latest embed code. - [documentcloud id="265231-11-07-2011-letter-to-idaho-congressional"] - -Or use the URL from DocumentCloud: +By default, documents will have a responsive width (it will narrow and widen as necessary to fill available content area) and use the theme's default height. If you want to override this, you can either set `responsive="false"` or explicitly set a `width`: - [documentcloud url="http://www.documentcloud.org/documents/265231-11-07-2011-letter-to-idaho-congressional.html"] + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html" width="600"] -This will use default height and width settings, which you can update in the WordPress admin. To override defaults on a specific document, pass them into the shortcode: +You can set your own defaults in Settings > DocumentCloud, but default widths will be ignored unless `responsive` is disabled: - [documentcloud id="265231-11-07-2011-letter-to-idaho-congressional" width="400" height="500" sidebar="true"] + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html" responsive="false"] +To embed a note, just use any note-specific URL. Notes ignore `width/height` and always act responsively: + + [documentcloud url="https://www.documentcloud.org/documents/282753-lefler-thesis.html#document/p1/a53674"] + +Here's the full list of embed options you can pass via shortcode attributes; some are specific to the type of resource you're embedding. + +**All resources (documents and notes):** + +- `url` (**required**, string): Full URL of the DocumentCloud resource. +- `container` (string): ID of element to insert the embed into; if excluded, embedder will create its own container. + +**Documents only:** + +- `height` (integer): Height (in pixels) of the embed. +- `width` (integer): Width (in pixels) of the embed. If used, will implicitly set `responsive="false"`. +- `responsive` (boolean): Use responsive layout, which dynamically adjusts width to fill content area. Defaults `true`. +- `responsive_offset` (integer): Distance (in pixels) to vertically offset the viewer for some responsive embeds. +- `default_page` (integer): Page number to have the document scroll to by default. +- `default_note` (integer): ID of the note that the document should highlight by default. +- `notes` (boolean): Show/hide notes: +- `search` (boolean): Hide or show search form. +- `sidebar` (boolean): Hide or show sidebar. Defaults `false`. +- `pdf` (boolean): Hide or show link to download original PDF. Defaults `true`. +- `text` (boolean): Hide or show text tab. Defaults `true`. +- `zoom` (boolean): Hide or show zoom slider. +- `format` (string): Indicate to the theme that this is a wide asset by setting this to `wide`. Defaults `normal`. + +You can read more about publishing and embedding DocumentCloud resources on https://www.documentcloud.org/help/publishing. == Installation == -1. Upload the documentcloud directory to `wp-content/plugins/documentcloud` -2. Activate the plugin -3. In your posts, add documents using the DocumentCloud button, or the `[documentcloud]` shortcode. +1. Upload the contents of the plugin to `wp-content/plugins/documentcloud` +2. Activate the plugin through the "Plugins" menu +3. In your posts, embed documents or notes using the DocumentCloud button or the `[documentcloud]` shortcode +4. Optional: Set a default width/height for all DocumentCloud embeds (which can be overridden on a per-embed basis with the `height/width` attributes) at Settings > DocumentCloud. (This default width will only be used if you set `responsive="false"` on an embed.) + +== Changelog == + += 0.3 = +* Add support for embedding notes. +* Default to responsive. +* Enable caching. + += 0.2 = +* Fetch embed code via oEmbed instead of generating statically. +* Add new options: `container`, `responsive`, `responsive_offset`, `default_page`, `default_note`, `notes`, `search`, and `zoom`. +* Deprecate `id` attribute. It's still usable, but support may drop in the future. Use `url` instead. + += 0.1 = +* Initial release. + +== Upgrade Notice == + += 0.3 = +Adds support for embedding notes and enables caching. += 0.2 = +Adds oEmbed support for future-proofing embed codes. Provides additional embed options like `default_page`.