<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[2124] sites/trunk/wordcamp.org/public_html/wp-content/plugins: WordCamp Remote CSS: Initial commit.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/2124">2124</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/2124","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-11-24 18:57:05 +0000 (Tue, 24 Nov 2015)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Remote CSS: Initial commit.</pre>
<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/</li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappcommonphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/common.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappoutputcachedcssphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/output-cached-css.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappsynchronizeremotecssphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/synchronize-remote-css.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappuserinterfacephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/user-interface.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappwebhookhandlerphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/webhook-handler.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssbootstrapphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/bootstrap.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/platforms/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssplatformsgithubphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/platforms/github.php</a></li>
<li>sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/</li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpautomatedsynchronizationphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-automated-synchronization.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpbasicsetupphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-basic-setup.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpoverviewphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-overview.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelptipsphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-tips.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewspageremotecssphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/page-remote-css.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappcommonphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/common.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/common.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/common.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,57 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+const SAFE_CSS_POST_SLUG = 'wcrcss_safe_cached_version';
+const OPTION_LAST_UPDATE = 'wcrcss_last_update';
+const AJAX_ACTION = 'wcrcss_webhook';
+const SYNCHRONIZE_ACTION = 'wcrcss_synchronize';
+const WEBHOOK_RATE_LIMIT = 30; // seconds
+const OPTION_REMOTE_CSS_URL = 'wcrcss_remote_css_url';
+const CSS_HANDLE = 'wordcamp_remote_css';
+const GITHUB_API_HOSTNAME = 'api.github.com';
+
+/**
+ * Find the ID of the post we use to store the safe CSS
+ *
+ * @return int|\WP_Error
+ */
+function get_safe_css_post_id() {
+ $post = get_safe_css_post();
+ $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : $post;
+
+ return $post_id;
+}
+
+/**
+ * Find the post we use to store the safe CSS
+ *
+ * @return \WP_Post|\WP_Error
+ */
+function get_safe_css_post() {
+ $safe_css_post = get_posts( array(
+ 'posts_per_page' => 1,
+ 'post_type' => 'safecss',
+ 'post_status' => 'private',
+ 'post_name' => SAFE_CSS_POST_SLUG,
+ ) );
+
+ if ( $safe_css_post ) {
+ $post = $safe_css_post[0];
+ } else {
+ $post = wp_insert_post( array(
+ 'post_type' => 'safecss',
+ 'post_status' => 'private', // Jetpack_Custom_CSS::post_id() only searches for `public` posts, so this prevents Jetpack from fetching our post
+ 'post_title' => SAFE_CSS_POST_SLUG,
+ 'post_name' => SAFE_CSS_POST_SLUG,
+ ), true );
+
+ if ( ! is_wp_error( $post ) ) {
+ $post = get_post( $post );
+ }
+ }
+
+ return $post;
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappoutputcachedcssphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/output-cached-css.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/output-cached-css.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/output-cached-css.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,106 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_cached_css', 11 ); // after the theme's stylesheet, but before before Jetpack Custom CSS's stylesheet
+add_action( 'wp_ajax_' . CSS_HANDLE, __NAMESPACE__ . '\output_cached_css' );
+add_action( 'wp_ajax_nopriv_' . CSS_HANDLE, __NAMESPACE__ . '\output_cached_css' );
+add_filter( 'nocache_headers', __NAMESPACE__ . '\set_cache_headers' );
+
+/**
+ * Enqueue the cached CSS
+ *
+ * An AJAX endpoint is used because the CSS is stored in the database, rather than on the file system.
+ */
+function enqueue_cached_css() {
+ if ( false === get_option( OPTION_REMOTE_CSS_URL ) ) {
+ return;
+ }
+
+ $cachebuster = get_latest_revision_id();
+
+ if ( ! $cachebuster ) {
+ $cachebuster = date( 'Y-m-d' ); // We should always have a revision ID, but this will work as a fallback if we don't for some reason
+ }
+
+ wp_enqueue_style(
+ CSS_HANDLE,
+ add_query_arg( 'action', CSS_HANDLE, admin_url( 'admin-ajax.php' ) ),
+ array(),
+ $cachebuster,
+ 'all'
+ );
+}
+
+/**
+ * Get the ID of the latest revision of the safe CSS post
+ *
+ * @return int|bool
+ */
+function get_latest_revision_id() {
+ $safe_css = get_safe_css_post();
+
+ if ( ! is_a( $safe_css, 'WP_Post' ) || empty( $safe_css->post_content_filtered ) ) {
+ return false;
+ }
+ $latest_revision = wp_get_post_revisions( $safe_css->ID, array( 'posts_per_page' => 1 ) );
+
+ if ( empty( $latest_revision ) ) {
+ return false;
+ }
+
+ $latest_revision = array_shift( $latest_revision );
+
+ return $latest_revision->ID;
+}
+
+/**
+ * Adjust the HTTP response headers so that browsers will cache the CSS we send
+ *
+ * Normally Core prevents caching of all AJAX requests, but we want to make sure the CSS is cached because it's
+ * loaded on every front-end request.
+ *
+ * @param array $cache_headers
+ *
+ * @return array
+ */
+function set_cache_headers( $cache_headers ) {
+ if ( ! defined( 'DOING_AJAX' ) || empty( $_GET['action'] ) || CSS_HANDLE !== $_GET['action'] ) {
+ return $cache_headers;
+ }
+
+ $safe_css = get_safe_css_post();
+
+ if ( ! is_a( $safe_css, 'WP_Post' ) ) {
+ return $cache_headers;
+ }
+
+ $last_modified = date( 'D, d M Y H:i:s', strtotime( $safe_css->post_date_gmt ) ) . ' GMT';
+ $expiration_period = YEAR_IN_SECONDS;
+
+ $cache_headers = array(
+ 'Cache-Control' => 'maxage=' . $expiration_period,
+ 'ETag' => '"' . md5( $last_modified ) . '"',
+ 'Last-Modified' => $last_modified, // Currently Core always strips this out, but we want to send it, and maybe Core will allow that in the future
+ 'Expires' => gmdate( 'D, d M Y H:i:s', time() + $expiration_period ) . ' GMT',
+ );
+
+ return $cache_headers;
+}
+
+/**
+ * Handles the AJAX endpoint to output the local copy of the CSS
+ */
+function output_cached_css() {
+ header( 'Content-Type: text/css; charset=' . get_option( 'blog_charset' ) );
+
+ $safe_css = get_safe_css_post();
+
+ if ( is_a( $safe_css, 'WP_Post' ) ) {
+ echo $safe_css->post_content_filtered;
+ }
+
+ wp_die();
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappsynchronizeremotecssphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/synchronize-remote-css.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/synchronize-remote-css.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/synchronize-remote-css.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,93 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+/**
+ * Synchronizes the local safe/cached copy of the CSS with the canonical, remote source.
+ *
+ * @param string $remote_css_url
+ */
+function synchronize_remote_css( $remote_css_url ) {
+ sanitize_and_save_unsafe_css( fetch_unsafe_remote_css( $remote_css_url ) );
+ update_option( OPTION_LAST_UPDATE, time() );
+}
+
+/**
+ * Fetch the unsafe CSS from the remote server
+ *
+ * @param string $remote_css_url
+ *
+ * @throws \Exception if the response body could not be retrieved for any reason
+ *
+ * @return string
+ */
+function fetch_unsafe_remote_css( $remote_css_url ) {
+ $response = wp_remote_get(
+ $remote_css_url,
+ array(
+ 'user-agent' => 'WordCamp.org Remote CSS', // GitHub's API explicitly requests this, and it could be beneficial for other platforms too
+ 'reject_unsafe_urls' => true,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ throw new \Exception( $response->get_error_message() );
+ }
+
+ $response_code = (int) wp_remote_retrieve_response_code( $response );
+
+ if ( ! in_array( $response_code, array( 200, 301, 302, 303, 307, 308 ), true ) ) {
+ throw new \Exception( sprintf(
+ __( 'The remote server responded with status code <code>%d</code>, which is not valid.', 'wordcamporg' ),
+ $response_code
+ ) );
+ }
+
+ return apply_filters( 'wcrcss_unsafe_remote_css', wp_remote_retrieve_body( $response ), $remote_css_url );
+}
+
+/**
+ * Sanitize unsafe CSS and save the safe version
+ *
+ * Note: If we ever need to decouple from Jetpack Custom CSS, then https://github.com/google/caja might be
+ * a viable alternative. It'd be nice to have a modular solution, but we'd also have to keep it up to date,
+ * and we'd still need to mirror the Output Mode setting.
+ *
+ * @param string $unsafe_css
+ *
+ * @throws \Exception if Jetpack's Custom CSS module isn't available
+ */
+function sanitize_and_save_unsafe_css( $unsafe_css ) {
+ if ( ! is_callable( array( '\Jetpack_Custom_CSS', 'save' ) ) ) {
+ throw new \Exception(
+ __( "<code>Jetpack_Custom_CSS::save()</code> is not available.
+ Please make sure Jetpack's Custom CSS module has been activated.", 'wordcamporg' )
+ );
+ }
+
+ /*
+ * Note: In addition to the sanitization that Jetpack_Custom_CSS::save() does, there's additional sanitization
+ * done by the callbacks in mu-plugins/jetpack-tweaks.php.
+ */
+
+ add_filter( 'jetpack_custom_css_pre_post_id', __NAMESPACE__ . '\get_safe_css_post_id' );
+
+ \Jetpack_Custom_CSS::save( array(
+ 'css' => $unsafe_css,
+ 'is_preview' => false,
+ 'preprocessor' => '', // This should never be changed to allow pre-processing. See note in validate_remote_css_url()
+ 'add_to_existing' => false, // This isn't actually used, see get_output_mode()
+ 'content_width' => false,
+ ) );
+
+ remove_filter( 'jetpack_custom_css_pre_post_id', __NAMESPACE__ . '\get_safe_css_post_id' );
+
+ /*
+ * Jetpack_Custom_CSS::save_revision() caches our post ID because it retrieves the post ID from
+ * Jetpack_Custom_CSS::post_id() while the get_safe_css_post_id() callback is active. We need to clear that
+ * to avoid unintended side-effects.
+ */
+ wp_cache_delete( 'custom_css_post_id' );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappuserinterfacephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/user-interface.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/user-interface.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/user-interface.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,308 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+add_action( 'admin_menu', __NAMESPACE__ . '\add_admin_pages' );
+
+/**
+ * Register new admin pages
+ */
+function add_admin_pages() {
+ $page_hook = \add_submenu_page(
+ 'themes.php',
+ __( 'Remote CSS', 'wordcamporg' ),
+ __( 'Remote CSS', 'wordcamporg' ),
+ 'switch_themes',
+ 'remote-css',
+ __NAMESPACE__ . '\render_options_page'
+ );
+
+ add_action( 'admin_print_styles-' . $page_hook, __NAMESPACE__ . '\print_css' );
+ add_action( 'load-' . $page_hook, __NAMESPACE__ . '\add_contextual_help_tabs' );
+}
+
+/**
+ * Render the view for the options page
+ */
+function render_options_page() {
+ $notice = null;
+
+ if ( isset( $_POST['submit'] ) ) {
+ try {
+ $notice = process_options_page();
+ $notice_class = 'notice-success';
+ } catch( \Exception $exception ) {
+ $notice = $exception->getMessage();
+ $notice_class = 'notice-error';
+ }
+ }
+
+ $output_mode = get_output_mode();
+ $remote_css_url = get_option( OPTION_REMOTE_CSS_URL , '' );
+ $fonts_tool_url = admin_url( 'themes.php?page=wc-fonts-options' );
+ $jetpack_modules_url = admin_url( 'admin.php?page=jetpack_modules' );
+ $jetpack_custom_css_active = method_exists( '\Jetpack_Custom_CSS', 'init' );
+
+ require_once( dirname( __DIR__ ) . '/views/page-remote-css.php' );
+}
+
+/**
+ * Get the mode for outputting the custom CSS.
+ *
+ * This just uses the same mode as Jetpack's CSS post, because it wouldn't make any sense to have them configured
+ * with opposite values.
+ *
+ * @return string
+ */
+function get_output_mode() {
+ $mode = 'add-on';
+
+ try {
+ $jetpack_css_post_id = get_jetpack_css_post_id();
+
+ if ( 'no' === get_post_meta( $jetpack_css_post_id, 'custom_css_add', true ) ) {
+ $mode = 'replace';
+ }
+ } catch( \Exception $exception ) {
+ // Just fall back to the default $mode
+ }
+
+ return $mode;
+}
+
+/**
+ * Get or set the mode for outputting the custom CSS.
+ *
+ * See get_output_mode() for notes.
+ *
+ * @param string $mode
+ */
+function set_output_mode( $mode ) {
+ $mode = 'replace' === $mode ? 'no' : 'yes';
+ $revision_id = get_jetpack_css_latest_revision_id();
+
+ update_post_meta( get_jetpack_css_post_id(), 'custom_css_add', $mode );
+
+ if ( $revision_id ) {
+ update_metadata( 'post', $revision_id, 'custom_css_add', $mode ); // update_post_meta doesn't allow modifying revisions
+ }
+}
+
+/**
+ * Get the ID of Jetpack's safecss post
+ *
+ * If it doesn't exist yet, create it, because we'll need it for the Output Mode setting.
+ *
+ * @throws \Exception if Jetpack's Custom CSS module isn't available
+ *
+ * @return int
+ */
+function get_jetpack_css_post_id() {
+ if ( ! is_callable( array( '\Jetpack_Custom_CSS', 'post_id' ) ) ) {
+ throw new \Exception(
+ __( "<code>Jetpack_Custom_CSS::post_id()</code> is not available.
+ Please make sure Jetpack's Custom CSS module has been activated.",
+ 'wordcamporg' )
+ );
+ }
+
+ $post_id = \Jetpack_Custom_CSS::post_id();
+
+ if ( ! is_int( $post_id ) || 0 === $post_id ) {
+ $post_values = array(
+ 'post_content' => '',
+ 'post_title' => 'safecss',
+ 'post_status' => 'publish',
+ 'post_type' => 'safecss',
+ 'post_content_filtered' => '',
+ );
+
+ $post_id = wp_insert_post( $post_values, true );
+
+ if ( is_wp_error( $post_id ) ) {
+ throw new \Exception( $post_id->get_error_message() );
+ }
+ }
+
+ return $post_id;
+}
+
+/**
+ * Get the ID of the current Jetpack's safecss revision post
+ *
+ * @throws \Exception if Jetpack's Custom CSS module isn't available
+ *
+ * @return int|bool
+ */
+function get_jetpack_css_latest_revision_id() {
+ if ( ! is_callable( array( '\Jetpack_Custom_CSS', 'get_current_revision' ) ) ) {
+ throw new \Exception(
+ __( "<code>Jetpack_Custom_CSS::get_current_revision()</code> is not available.
+ Please make sure Jetpack's Custom CSS module has been activated.",
+ 'wordcamporg' )
+ );
+ }
+
+ $revision = \Jetpack_Custom_CSS::get_current_revision();
+
+ return empty( $revision['ID'] ) ? false : $revision['ID'];
+}
+
+/**
+ * Process submissions of the form on the options page
+ *
+ * @throws \Exception if the user isn't authorized
+ *
+ * @return string
+ */
+function process_options_page() {
+ check_admin_referer( 'wcrcss-options-submit', 'wcrcss-options-nonce' );
+
+ if ( ! current_user_can( 'switch_themes' ) ) {
+ throw new \Exception( __( 'Access denied.', 'wordcamporg' ) );
+ }
+
+ $remote_css_url = trim( $_POST['wcrcss-remote-css-url'] );
+
+ if ( '' === $remote_css_url ) {
+ $notice = '';
+ wp_delete_post( get_safe_css_post_id() );
+ } else {
+ $notice = __( 'The remote CSS file was successfully synchronized.', 'wordcamporg' );
+ $remote_css_url = validate_remote_css_url( $remote_css_url );
+
+ synchronize_remote_css( $remote_css_url );
+ set_output_mode( $_POST['wcrcss-output-mode'] );
+ }
+
+ update_option( OPTION_REMOTE_CSS_URL, $remote_css_url );
+
+ return $notice;
+}
+
+/**
+ * Validate the remote CSS URL provided by the user
+ *
+ * @param string $remote_css_url
+ *
+ * @throws \Exception if the URL cannot be validated
+ *
+ * @return string
+ */
+function validate_remote_css_url( $remote_css_url ) {
+ // Syntactically-valid URLs only
+ $remote_css_url = filter_var( $remote_css_url, FILTER_VALIDATE_URL );
+
+ if ( false === $remote_css_url ) {
+ throw new \Exception( __( 'The URL was invalid.', 'wordcamporg' ) );
+ }
+
+ $remote_css_url = esc_url_raw( $remote_css_url, array( 'http', 'https' ) );
+ $parsed_url = parse_url( $remote_css_url );
+
+ /*
+ * Only allow whitelisted hostnames, to prevent SSRF attacks
+ *
+ * WARNING: These must be trusted in the sense that they're not malicious, but also in the sense that they
+ * have strong internal security. We can't allow sites hosted by local WordPress communities, for instance,
+ * because an attacker could gain control over their DNS zone and then change the A record to 127.0.0.1,
+ * or an IP on our internal network.
+ *
+ * Therefore, only reputable platforms like GitHub, Beanstalk, CloudForge, BitBucket, etc should be added.
+ */
+ $trusted_hostnames = apply_filters( 'wcrcss_trusted_remote_hostnames', array() );
+
+ if ( ! in_array( $parsed_url['host'], $trusted_hostnames, true ) ) {
+ throw new \Exception( sprintf(
+ __( 'Due to security constraints, only certain third-party platforms can be used,
+ and the URL you provided is not hosted by one of our currently-supported platforms.
+ To request that it be added, please <a href="%s">create a ticket</a> on Meta Trac.',
+ 'wordcamporg' ),
+ 'https://meta.trac.wordpress.org/newticket'
+ ) );
+ }
+
+ /*
+ * Vanilla CSS only
+ *
+ * We need to force the user to do their own pre-processing, because Jetpack_Custom_CSS::save() doesn't
+ * sanitize the unsafe CSS when a preprocessor is present. We'd have to add more logic to make sure it gets
+ * sanitized, which would further couple the plugin to Jetpack.
+ */
+ if ( '.css' !== substr( $parsed_url['path'], strlen( $parsed_url['path'] ) - 4, 4 ) ) {
+ throw new \Exception(
+ __( 'The URL must be a vanilla CSS file ending in <code>.css</code>.
+ If you\'d like to use SASS/LESS, please compile it into vanilla CSS on your server,
+ and then enter the URL for that file.',
+ 'wordcamporg' )
+ );
+ }
+
+ /*
+ * Note: We also want to restrict the URL to ports 80, 443, and 8080. The 'reject_unsafe_urls' in
+ * fetch_unsafe_remote_css() takes care of that for us.
+ */
+
+ return apply_filters( 'wcrcss_validate_remote_css_url', $remote_css_url );
+}
+
+/**
+ * Print CSS for the options page
+ */
+function print_css() {
+ ?>
+
+ <style type="text/css">
+ body.appearance_page_remote-css button.button-link {
+ color: #0073aa;
+ padding: 0;
+ }
+ </style>
+
+ <?php
+}
+
+/**
+ * Register contextual help tabs
+ */
+function add_contextual_help_tabs() {
+ $screen = get_current_screen();
+ $tabs = array( 'Overview', 'Basic Setup', 'Automated Synchronization', 'Tips' );
+
+ foreach ( $tabs as $tab ) {
+ $screen->add_help_tab( array(
+ 'id' => 'wcrcss-' . sanitize_title( $tab ),
+ 'title' => $tab,
+ 'callback' => __NAMESPACE__ . '\render_contextual_help_tabs',
+ ) );
+ }
+}
+
+/**
+ * Render contextual help tabs
+ *
+ * @param \WP_Screen $screen
+ * @param array $tab
+ */
+function render_contextual_help_tabs( $screen, $tab ) {
+ $view_slug = str_replace( 'wcrcss-', '', $tab['id'] );
+
+ switch ( $view_slug ) {
+ case 'overview':
+ $jetpack_editor_url = admin_url( 'themes.php?page=editcss' );
+ break;
+
+ case 'automated-synchronization':
+ $webhook_payload_url = sprintf( '%s?action=%s', admin_url( 'admin-ajax.php' ), AJAX_ACTION );
+ break;
+
+ case 'tips':
+ $fonts_tool_url = admin_url( 'themes.php?page=wc-fonts-options' );
+ $media_library_url = admin_url( 'upload.php' );
+ break;
+ }
+
+ require_once( sprintf( '%s/views/help-%s.php', dirname( __DIR__ ), $view_slug ) );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssappwebhookhandlerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/webhook-handler.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/webhook-handler.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/app/webhook-handler.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,50 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+add_action( 'wp_ajax_' . AJAX_ACTION, __NAMESPACE__ . '\webhook_handler' ); // This is useless in production, but useful for manual testing
+add_action( 'wp_ajax_nopriv_' . AJAX_ACTION, __NAMESPACE__ . '\webhook_handler' );
+add_action( SYNCHRONIZE_ACTION, __NAMESPACE__ . '\synchronize_remote_css' );
+
+/**
+ * Trigger a synchronization when a push notification is received
+ *
+ * Because the client can't modify the remote URL that's being used, it's safe to allow anonymous access to this,
+ * provided that requests are rate-limited to prevent a malicious party from making us flood remote servers, which
+ * would result in them blocking us. The worst they could do would be to force us to unnecessarily refresh the cache.
+ *
+ * Avoiding authentication makes the process simpler because there's less to do, and also more flexible, because
+ * we don't have to handle notification formats from various platforms.
+ */
+function webhook_handler() {
+ $time_since_last_sync = time() - get_option( OPTION_LAST_UPDATE, 0 );
+
+ if ( $time_since_last_sync < WEBHOOK_RATE_LIMIT ) {
+ $time_limit_remaining = WEBHOOK_RATE_LIMIT - $time_since_last_sync;
+
+ /*
+ * We only want one event scheduled to prevent abuse and unnecessary requests, but
+ * wp_schedule_single_event() does that for us if the period is under 10 minutes.
+ */
+ wp_schedule_single_event(
+ time() + $time_limit_remaining,
+ SYNCHRONIZE_ACTION,
+ array( get_option( OPTION_REMOTE_CSS_URL ) )
+ );
+
+ wp_send_json_error( sprintf(
+ __( 'The request could not be executed immediately because of the rate limit. Instead, it has been queued and will run in %d seconds.',
+ 'wordcamporg' ),
+ $time_limit_remaining
+ ) );
+ } else {
+ try {
+ do_action( SYNCHRONIZE_ACTION, get_option( OPTION_REMOTE_CSS_URL ) );
+ wp_send_json_success( __( 'The remote CSS file was successfully synchronized.', 'wordcamporg' ) );
+ } catch ( \Exception $exception ) {
+ wp_send_json_error( strip_tags( $exception->getMessage() ) ); // strip_tags() instead of wp_strip_tags() because we want to preserve the inner content
+ }
+ }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssbootstrapphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/bootstrap.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/bootstrap.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/bootstrap.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,27 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+/*
+Plugin Name: WordCamp Remote CSS
+Description: Allows organizers to develop their Custom CSS with whatever tools and environment they prefer.
+Version: 0.1
+Author: WordCamp.org
+Author URI: http://wordcamp.org
+License: GPLv2 or later
+*/
+
+require_once( __DIR__ . '/app/common.php' );
+
+if ( is_admin() ) {
+ require_once( __DIR__ . '/app/synchronize-remote-css.php' );
+ require_once( __DIR__ . '/app/user-interface.php' );
+ require_once( __DIR__ . '/app/webhook-handler.php' );
+ require_once( __DIR__ . '/platforms/github.php' );
+}
+
+if ( ! is_admin() || defined( 'DOING_AJAX' ) ) {
+ require_once( __DIR__ . '/app/output-cached-css.php' );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssplatformsgithubphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/platforms/github.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/platforms/github.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/platforms/github.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,81 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+
+defined( 'WPINC' ) or die();
+
+/*
+ * @todo -- Once another platform has been added and you can see the similarities, this should probably be
+ * refactored to extend an abstract class or implement an interface.
+ */
+
+add_filter( 'wcrcss_trusted_remote_hostnames', __NAMESPACE__ . '\whitelist_trusted_hostnames' );
+add_filter( 'wcrcss_validate_remote_css_url', __NAMESPACE__ . '\convert_to_api_urls' );
+add_filter( 'wcrcss_unsafe_remote_css', __NAMESPACE__ . '\decode_api_response', 10, 2 );
+
+/**
+ * Add GitHub's hostnames to the whitelist of trusted hostnames
+ *
+ * @param array $hostnames
+ *
+ * @return array
+ */
+function whitelist_trusted_hostnames( $hostnames ) {
+ return array_merge( $hostnames, array( 'github.com', 'raw.githubusercontent.com', GITHUB_API_HOSTNAME ) );
+}
+
+/**
+ * Convert various GitHub URLs to their API equivalents
+ *
+ * We need to use the API to request the the file contents, because github.com shows the file embedded in an HTML
+ * page, and raw.githubusercontent.come is cached and will often respond with stale content.
+ *
+ * @param string $remote_css_url
+ *
+ * @return string
+ */
+function convert_to_api_urls( $remote_css_url ) {
+ $owner = $repository = $file_path = null;
+
+ $parsed_url = parse_url( $remote_css_url );
+ $path = explode( '/', $parsed_url['path'] );
+
+ if ( 'github.com' == $parsed_url['host'] ) {
+ $owner = $path[1];
+ $repository = $path[2];
+ $file_path = implode( '/', array_slice( $path, 5 ) );
+ } elseif ( 'raw.githubusercontent.com' == $parsed_url['host'] ) {
+ $owner = $path[1];
+ $repository = $path[2];
+ $file_path = implode( '/', array_slice( $path, 4 ) );
+ }
+
+ if ( $owner && $repository && $file_path ) {
+ $remote_css_url = sprintf(
+ 'https://%s/repos/%s/%s/contents/%s',
+ GITHUB_API_HOSTNAME,
+ $owner,
+ $repository,
+ $file_path
+ );
+ }
+
+ return $remote_css_url;
+}
+
+/**
+ * Decode the file contents from GitHub's API response
+ *
+ * @param string $response_body
+ * @param string $remote_css_url
+ *
+ * @return string
+ */
+function decode_api_response( $response_body, $remote_css_url ) {
+ if ( false !== strpos( $remote_css_url, GITHUB_API_HOSTNAME ) ) {
+ $response_body = json_decode( $response_body );
+ $response_body = base64_decode( $response_body->content );
+ }
+
+ return $response_body;
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpautomatedsynchronizationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-automated-synchronization.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-automated-synchronization.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-automated-synchronization.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,59 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+defined( 'WPINC' ) or die();
+
+?>
+
+<p>
+ <?php _e( "You don't have to manually synchronize the local file every time you make a change to the remote file;
+ instead, you can setup a webhook to trigger synchronization automatically.", 'wordcamporg' ); ?>
+</p>
+
+<h2><?php _e( 'Setup', 'wordcamporg' ); ?></h2>
+
+<p>
+ <?php _e( "The details will vary depending on your server, but let's use GitHub as an example.", 'wordcamporg' ); ?>
+</p>
+
+<ol>
+ <li>
+ <?php printf(
+ __( 'Follow <a href="%s">GitHub\'s instructions for creating a webhook</a>.', 'wordcamporg' ),
+ 'https://developer.github.com/webhooks/creating/'
+ ); ?>
+ </li>
+
+ <li>
+ <?php printf(
+ __( 'For the <code>Payload URL</code>, enter <code>%s</code>.', 'wordcamporg' ),
+ esc_url( $webhook_payload_url )
+ ); ?>
+ </li>
+
+ <li><?php _e( 'For the rest of the options, you can accept the default values.', 'wordcamporg' ); ?></li>
+</ol>
+
+<p>
+ <?php _e( "If you're not using GitHub, your process will be different, but at the end of the day all you need to do
+ is setup something to open an HTTP request to the payload URL above whenever your file changes.", 'wordcamporg' ); ?>
+</p>
+
+<h2><?php _e( 'Testing & Troubleshooting', 'wordcamporg' ); ?></h2>
+
+<p>
+ <?php _e( 'To test if the synchronization is working, make a change to the file, commit it, push it to GitHub,
+ and then check the site to see if that change is active.', 'wordcamporg' ); ?>
+</p>
+
+<p>
+ <?php _e( "If your change isn't active on WordCamp.org, edit the webhook and scroll down to the <strong>Recent Deliveries</strong> section,
+ then open the latest delivery and look at the <strong>Response</strong> tab for any errors.", 'wordcamporg' ); ?>
+</p>
+
+<p>
+ <?php printf(
+ __( 'If that doesn\'t help solve the problem, you can ask for help in the <code>#meta-wordcamp</code> channel on <a href="%s">Slack</a>.', 'wordcamporg' ),
+ 'https://chat.wordpress.org'
+ ); ?>
+</p>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpbasicsetupphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-basic-setup.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-basic-setup.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-basic-setup.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,77 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+defined( 'WPINC' ) or die();
+
+?>
+
+<ol>
+ <li>
+ <p>
+ <?php _e( '<strong>Publish your CSS file</strong> to anywhere on the public Internet.', 'wordcamporg' ); ?>
+ </p>
+
+ <p>
+ <?php _e( "If you're using SASS or LESS, you'll need to compile it into vanilla CSS and publish that file.", 'wordcamporg' ); ?>
+ </p>
+ </li>
+
+ <li>
+ <p>
+ <?php _e( '<strong>Enter the URL</strong> for the CSS file into the input box below.', 'wordcamporg' ); ?>
+ </p>
+
+ <p>
+ <?php printf(
+ __( 'Due to security constraints, only certain third-party platforms can be used.
+ We currently only support GitHub, but more platforms can be added if there\'s interest from organizers.
+ To request an additional platform, please <a href="%s">create a ticket</a> on Meta Trac.', 'wordcamporg' ),
+ 'https://meta.trac.wordpress.org/newticket'
+ ); ?>
+ </p>
+
+ <p>
+ <?php _e( "If you're using GitHub, you can enter the URL in any of the following formats,
+ but we'll convert them to use the GitHub API.", 'wordcamporg' ); ?>
+ </p>
+
+ <ul>
+ <li>
+ <?php _e( 'Web-based file browser:', 'wordcamporg' ); ?>
+ <code>https://github.com/WordPressSeattle/seattle.wordcamp.org-<?php echo esc_html( date( 'Y' ) ); ?>/blob/master/style.css</code>
+ </li>
+
+ <li>
+ <?php _e( 'Raw file:', 'wordcamporg' ); ?>
+ <code>https://raw.githubusercontent.com/WordPressSeattle/seattle.wordcamp.org-<?php echo esc_html( date( 'Y' ) ); ?>/master/style.css</code>
+ </li>
+
+ <li>
+ <?php _e( 'API:', 'wordcamporg' ); ?>
+ <code>https://api.github.com/repos/WordPressSeattle/seattle.wordcamp.org-<?php echo esc_html( date( 'Y' ) ); ?>/contents/style.css</code>
+ </li>
+ </ul>
+ </li>
+
+ <li>
+ <p><?php _e( 'Click the <strong>Update</strong> button.', 'wordcamporg' ); ?></p>
+
+ <p>
+ <?php _e( "WordCamp.org will download the file, sanitize it, and store a local copy,
+ then enqueue the local copy as a stylesheet alongside your theme's default stylesheet.", 'wordcamporg' ); ?>
+ </p>
+ </li>
+
+ <li>
+ <?php _e( 'The local copy will need to be <strong>synchronized</strong> whenever you make a change to the file.
+ You can either update manually by pushing the <strong>Update</strong> button again, or update automatically by setting up a webhook.
+ For instructions on setting up a webhook, open the <strong>Automated Synchronization</strong> tab.', 'wordcamporg' ); ?>
+ </li>
+</ol>
+
+<p>
+ <?php printf(
+ __( 'If you run into any problems, you can ask for help in the <code>#meta-wordcamp</code> channel on <a href="%s">Slack</a>.', 'wordcamporg' ),
+ 'https://chat.wordpress.org'
+ ); ?>
+</p>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelpoverviewphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-overview.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-overview.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-overview.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,39 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+defined( 'WPINC' ) or die();
+
+?>
+
+<p>
+ <?php _e( 'Remote CSS gives you a lot more flexibility in how you develop your site than the Jetpack Custom CSS module.
+ For instance, you can:', 'wordcamporg' ); ?>
+</p>
+
+<ul>
+ <li><?php _e( 'Work in a local development environment, like Varying Vagrant Vagrants.', 'wordcamporg' ); ?></li>
+ <li><?php _e( 'Use your favorite IDE or text-editor, like PhpStorm or Sublime Text.', 'wordcamporg' ); ?></li>
+ <li><?php _e( 'Use SASS or LESS instead of vanilla CSS.', 'wordcamporg' ); ?></li>
+ <li><?php _e( 'Use tools like Grunt to automate your workflow.', 'wordcamporg' ); ?></li>
+ <li><?php _e( 'Manage your CSS in a source control system like Git.', 'wordcamporg' ); ?></li>
+ <li><?php _e( 'Collaborate with others on a social coding platform like GitHub.', 'wordcamporg' ); ?></li>
+</ul>
+
+<p>
+ <?php _e( "You can use all of those tools, only some of them, or completely different ones.
+ It's up to you how you choose to work.", 'wordcamporg' ); ?>
+</p>
+
+<p>
+ <?php _e( "This tool works by fetching your CSS file from a remote server (like GitHub.com), sanitizing the CSS,
+ and then storing a local copy on WordCamp.org. The local copy is then enqueued as a stylesheet, either in addition to your theme's stylesheet,
+ or as a replacement for it. The local copy of the CSS is synchronized with the remote file whenever you press the <strong>Update</strong> button,
+ and you can also setup webhook notifications for automatic synchronization when the remote file changes.", 'wordcamporg' ); ?>
+</p>
+
+<p>
+ <?php printf(
+ __( 'If you\'re looking for something simpler, <a href="%s">Jetpack\'s CSS Editor</a> is a great option.', 'wordcamporg' ),
+ esc_url( $jetpack_editor_url )
+ ); ?>
+</p>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewshelptipsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-tips.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-tips.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/help-tips.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,42 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+defined( 'WPINC' ) or die();
+
+?>
+
+<ul>
+ <li>
+ <?php printf(
+ __( 'We recommend <a href="%s">setting up a local development environment that mirrors WordCamp.org</a>.', 'wordcamporg' ),
+ 'https://make.wordpress.org/community/handbook/wordcamp-organizer-handbook/first-steps/web-presence/contributing-to-wordcamp-org/setting-up-a-local-wordcamp-org-sandbox/'
+ ); ?>
+ </li>
+
+ <li>
+ <?php _e( "Don't use post IDs as selectors, because they can change between your development environment and production.
+ Instead, use the slug; e.g. <code>body.post-slug-call-for-volunteers</code>, or <code>body.wcb_speaker-slug-sergey-biryukov</code>.
+ Just make sure that you update your CSS if you rename a post.", 'wordcamporg' ); ?>
+ </li>
+
+ <li>
+ <?php printf(
+ __( 'Use <a href="%s">the Fonts tool</a> to embed your web fonts.', 'wordcamporg' ),
+ esc_url( $fonts_tool_url )
+ ); ?>
+ </li>
+
+ <li>
+ <?php printf(
+ __( 'Upload your images to <a href="%s">the Media Library</a> rather than hosting them on 3rd party servers.
+ That way, visitors will avoid an extra DNS request,
+ and you won\'t have to worry about them going offline if there\'s a problem with the external server.', 'wordcamporg' ),
+ esc_url( $media_library_url )
+ ); ?>
+ </li>
+
+ <li>
+ <?php _e( "This tool plays nicely with Jetpack's CSS editor, and it's possible to use both.
+ If you do, the rules in the Jetpack editor will take precedence.", 'wordcamporg' ); ?>
+ </li>
+</ul>
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswordcampremotecssviewspageremotecssphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/page-remote-css.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/page-remote-css.php (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wordcamp-remote-css/views/page-remote-css.php 2015-11-24 18:57:05 UTC (rev 2124)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,101 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\RemoteCSS;
+defined( 'WPINC' ) or die();
+
+?>
+
+<div class="wrap">
+ <h1><?php _e( 'Remote CSS', 'wordcamporg' ); ?></h1>
+
+ <?php if ( ! $jetpack_custom_css_active ) : ?>
+ <div id="message" class="notice notice-error inline">
+ <?php
+ /*
+ * Jetpack_Custom_CSS is used to sanitize the unsafe CSS, and for removing the theme's stylesheet
+ * in `replace` mode. Methods from Jetpack_Custom_CSS are called throughout this plugin, so we
+ * need it to be active.
+ */
+ ?>
+
+ <p>
+ <?php printf(
+ __( 'This tool uses some functionality from Jetpack\'s Custom CSS module,
+ but it doesn\'t look like it\'s available.
+ Please <a href="%s">activate it</a>.',
+ 'wordcamporg' ),
+ esc_url( $jetpack_modules_url )
+ ); ?>
+ </p>
+ </div>
+ <?php endif; ?>
+
+ <?php
+ if ( is_callable( '\WordCamp\Jetpack_Tweaks\notify_import_rules_stripped' ) ) {
+ // This has to be called manually because process_options_page() is called after `admin_notices` fires
+ \WordCamp\Jetpack_Tweaks\notify_import_rules_stripped();
+ }
+ ?>
+
+ <?php if ( $notice ) : ?>
+ <div id="message" class="notice <?php echo esc_attr( $notice_class ); ?> is-dismissible">
+ <?php
+ /*
+ * Typically KSES is discouraged when displaying text because it's expensive, but in this case
+ * it's appropriate because the underlying layers need to pass HTML-formatted error messages, and
+ * this only only runs when the options are updated.
+ */
+ ?>
+
+ <p><?php echo wp_kses( $notice, wp_kses_allowed_html( 'data' ) ); ?></p>
+ </div>
+ <?php endif; ?>
+
+ <p>
+ <?php _e( 'This tool allows you to develop your CSS in any environment that you choose, and with the tools that you prefer,
+ rather than with Jetpack\'s CSS Editor.
+ <button type="button" id="wcrcss-open-help-tab" class="button-link">Open the Help tab</button> for detailed instructions.',
+ 'wordcamporg' ); ?>
+ </p>
+
+ <form action="" method="POST">
+ <?php wp_nonce_field( 'wcrcss-options-submit', 'wcrcss-options-nonce' ); ?>
+
+ <fieldset <?php disabled( $jetpack_custom_css_active, false ); ?>>
+ <p>
+ <label>
+ <?php _e( 'Remote CSS File:', 'wordcamporg' ); ?><br />
+ <input type="text" name="wcrcss-remote-css-url" class="large-text" value="<?php echo esc_url( $remote_css_url ); ?>" />
+ </label>
+ </p>
+
+ <div>
+ <?php _e( 'Output Mode:', 'wordcamporg' ); ?>
+
+ <ul>
+ <li>
+ <label>
+ <input type="radio" name="wcrcss-output-mode" value="add-on" <?php checked( $output_mode, 'add-on' ); ?> />
+ <?php _e( "Add-on: The theme's stylesheet will remain, and your custom CSS will be added after it.", 'wordcamporg' ); ?>
+ </label>
+ </li>
+
+ <li>
+ <label>
+ <input type="radio" name="wcrcss-output-mode" value="replace" <?php checked( $output_mode, 'replace' ); ?> />
+ <?php _e( "Replace: The theme's stylesheet will be removed, so that only your custom CSS is present.", 'wordcamporg' ); ?>
+ </label>
+ </li>
+ </ul>
+ </div>
+
+ <?php submit_button( __( 'Update', 'wordcamporg' ) ); ?>
+ </fieldset>
+ </form>
+</div>
+
+<script>
+ jQuery( '#wcrcss-open-help-tab' ).click( function() {
+ jQuery( '#contextual-help-link' ).click();
+ } );
+</script>
</ins></span></pre>
</div>
</div>
</body>
</html>