CVE-2015-10094 (PHP, published 2023-03-06T15:15Z)
Description: value: A vulnerability was found in Fastly Plugin up to 0.97 on WordPress. It has been rated as problematic. Affected by this issue is the function post of the file lib/api.php. The manipulation of the argument url leads to cross site scripting. The attack may be launched remotely. Upgrading to version 0.98 is able to address this issue. The patch is identified as d7fe42538f4d4af500e3af9678b6b06fba731656. It is recommended to upgrade the affected component. VDB-222326 is the identifier assigned to this vulnerability.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/wp-plugins/fastly
Files changed:
README.md
fastly.php
lib/JSON.php
lib/admin.php
lib/api.php
lib/purge.php
static/fastly.css
static/fastly.js
Commit message: Version 0.98 - CSRF/XSS Security Fixes

Bumps version to 0.98. Addresses potential XSS/CSRF security issues in plugin.

Changes admin.php to use the esc_attr() and esc_url() on the Fastly
option values prior to outputting them in the context of the HTML to be
rendered in the admin panel. While it appears that the values of these
options could not be set from an unprivileged context we should be
escaping them prior to rendering irregardless.

Previously the FastlyAdmin::ajaxSetPage() and FastlyAdmin::ajaxSignUp()
handlers in lib/admin.php were not enforcing the presence of a CSRF
token. This commit also modifies these handlers to use the
wp_verify_nonce() function. The static/fastly.js AJAX posts were
modified to supply a wp_create_nonce() token injected into the scripts
context by wp_localize_script(). The wp_register_script() and associated
calls were moved to a wp_enqueue_scripts event handler in order to
ensure that wp_create_nonce() is defined/loaded at the time we try to
use it.

Changes all calls to update_option() with dynamic content to use
esc_sql() on the data before passing it to update_option() to avoid
potential for SQLi.

Thanks to Zack Tollman for the original vulnerability reports.




git-svn-id: https://plugins.svn.wordpress.org/fastly/trunk@1081712 b8457f37-d9ea-0310-8a92-e5e31aec5664


Vulnerable code:
Fastly.com WordPress Plugin
Copyright (C) 2011,2012,2013 Fastly.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

<?php

/*
Plugin Name: Fastly
Plugin URI: http://fastly.com/
Description: Configuration and cache purging for the Fastly CDN.
Author: Fastly.com
Version: 0.97
Author URI: http://fastly.com/
*/

/**
 * Main plugin code.
 * @package Fastly
 * @version 0.97
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */

// Basic plugin definitions
define('FASTLY_VERSION', '0.97');
define('FASTLY_PLUGIN_URL', plugin_dir_url( __FILE__ ));

// Includes
include_once dirname( __FILE__ ) . '/lib/purge.php';
include_once dirname( __FILE__ ) . '/lib/admin.php';
include_once dirname( __FILE__ ) . '/lib/api.php';

// Check for JSON support
if (!function_exists('json_decode')) {
  require_once dirname( __FILE__ ) . '/lib/JSON.php';
  define('FASTLY_JSON', false);
}

// Plugin Options
add_option('fastly_hostname', '');
add_option('fastly_api_key', '');
add_option('fastly_service_id', '');
add_option('fastly_api_hostname', 'https://api.fastly.com');
add_option('fastly_api_port', null);
add_option('fastly_page', 'welcome');
add_option('fastly_log_purges', '0');

// Setup Purging
new FastlyPurge();

// Setup admin (if needed)
if (is_admin()) {
  new FastlyAdmin();
}

// Custom action links for the plugin.
function fastly_action_links($links, $file) {
  static $this_plugin;
  if (!$this_plugin) {
    $this_plugin = plugin_basename(__FILE__);
  }
  if ($file == $this_plugin) {
    $settings_link = '<a href="' . get_bloginfo('wpurl') . '/wp-admin/options-general.php?page=fastly-admin-panel">Settings</a>';
    array_unshift($links, $settings_link);
  }
  return $links;
}
add_filter('plugin_action_links', 'fastly_action_links', 10, 2);

// "Look out honey, cause I'm using technology..." - Iggy Pop

?>
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Converts to and from JSON format.
 *
 * JSON (JavaScript Object Notation) is a lightweight data-interchange
 * format. It is easy for humans to read and write. It is easy for machines
 * to parse and generate. It is based on a subset of the JavaScript
 * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
 * This feature can also be found in  Python. JSON is a text format that is
 * completely language independent but uses conventions that are familiar
 * to programmers of the C-family of languages, including C, C++, C#, Java,
 * JavaScript, Perl, TCL, and many others. These properties make JSON an
 * ideal data-interchange language.
 *
 * This package provides a simple encoder and decoder for JSON notation. It
 * is intended for use with client-side Javascript applications that make
 * use of HTTPRequest to perform server communication functions - data can
 * be encoded into JSON notation for use in a client-side javascript, or
 * decoded from incoming Javascript requests. JSON format is native to
 * Javascript, and can be directly eval()'ed with no further parsing
 * overhead
 *
 * All strings should be in ASCII or UTF-8 format!
 *
 * LICENSE: Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met: Redistributions of source code must retain the
 * above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * @category
 * @package     Services_JSON
 * @author      Michal Migurski <mike-json@teczno.com>
 * @author      Matt Knapp <mdknapp[at]gmail[dot]com>
 * @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
 * @copyright   2005 Michal Migurski
 * @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
 * @license     http://www.opensource.org/licenses/bsd-license.php
 * @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
 */

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_SLICE',   1);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_STR',  2);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_ARR',  3);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_OBJ',  4);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_CMT', 5);

/**
 * Behavior switch for Services_JSON::decode()
 */
define('SERVICES_JSON_LOOSE_TYPE', 16);

/**
 * Behavior switch for Services_JSON::decode()
 */
define('SERVICES_JSON_SUPPRESS_ERRORS', 32);

/**
 * Converts to and from JSON format.
 *
 * Brief example of use:
 *
 * <code>
 * // create a new instance of Services_JSON
 * $json = new Services_JSON();
 *
 * // convert a complexe value to JSON notation, and send it to the browser
 * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
 * $output = $json->encode($value);
 *
 * print($output);
 * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
 *
 * // accept incoming POST data, assumed to be in JSON notation
 * $input = file_get_contents('php://input', 1000000);
 * $value = $json->decode($input);
 * </code>
 */
class Services_JSON
{
   /**
    * constructs a new JSON instance
    *
    * @param    int     $use    object behavior flags; combine with boolean-OR
    *
    *                           possible values:
    *                           - SERVICES_JSON_LOOSE_TYPE:  loose typing.
    *                                   "{...}" syntax creates associative arrays
    *                                   instead of objects in decode().
    *                           - SERVICES_JSON_SUPPRESS_ERRORS:  error suppression.
    *                                   Values which can't be encoded (e.g. resources)
    *                                   appear as NULL instead of throwing errors.
    *                                   By default, a deeply-nested resource will
    *                                   bubble up with an error, so all return values
    *                                   from encode() should be checked with isError()
    */
    function Services_JSON($use = 0)
    {
        $this->use = $use;
    }

   /**
    * convert a string from one UTF-16 char to one UTF-8 char
    *
    * Normally should be handled by mb_convert_encoding, but
    * provides a slower PHP-only method for installations
    * that lack the multibye string extension.
    *
    * @param    string  $utf16  UTF-16 character
    * @return   string  UTF-8 character
    * @access   private
    */
    function utf162utf8($utf16)
    {
        // oh please oh please oh please oh please oh please
        if(function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
        }

        $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});

        switch(true) {
            case ((0x7F & $bytes) == $bytes):
                // this case should never be reached, because we are in ASCII range
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0x7F & $bytes);

            case (0x07FF & $bytes) == $bytes:
                // return a 2-byte UTF-8 character
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0xC0 | (($bytes >> 6) & 0x1F))
                     . chr(0x80 | ($bytes & 0x3F));

            case (0xFFFF & $bytes) == $bytes:
                // return a 3-byte UTF-8 character
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0xE0 | (($bytes >> 12) & 0x0F))
                     . chr(0x80 | (($bytes >> 6) & 0x3F))
                     . chr(0x80 | ($bytes & 0x3F));
        }

        // ignoring UTF-32 for now, sorry
        return '';
    }

   /**
    * convert a string from one UTF-8 char to one UTF-16 char
    *
    * Normally should be handled by mb_convert_encoding, but
    * provides a slower PHP-only method for installations
    * that lack the multibye string extension.
    *
    * @param    string  $utf8   UTF-8 character
    * @return   string  UTF-16 character
    * @access   private
    */
    function utf82utf16($utf8)
    {
        // oh please oh please oh please oh please oh please
        if(function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
        }

        switch(strlen($utf8)) {
            case 1:
                // this case should never be reached, because we are in ASCII range
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return $utf8;

            case 2:
                // return a UTF-16 character from a 2-byte UTF-8 char
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0x07 & (ord($utf8{0}) >> 2))
                     . chr((0xC0 & (ord($utf8{0}) << 6))
                         | (0x3F & ord($utf8{1})));

            case 3:
                // return a UTF-16 character from a 3-byte UTF-8 char
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr((0xF0 & (ord($utf8{0}) << 4))
                         | (0x0F & (ord($utf8{1}) >> 2)))
                     . chr((0xC0 & (ord($utf8{1}) << 6))
                         | (0x7F & ord($utf8{2})));
        }

        // ignoring UTF-32 for now, sorry
        return '';
    }

   /**
    * encodes an arbitrary variable into JSON format
    *
    * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
    *                           see argument 1 to Services_JSON() above for array-parsing behavior.
    *                           if var is a strng, note that encode() always expects it
    *                           to be in ASCII or UTF-8 format!
    *
    * @return   mixed   JSON string representation of input var or an error if a problem occurs
    * @access   public
    */
    function encode($var)
    {
        switch (gettype($var)) {
            case 'boolean':
                return $var ? 'true' : 'false';

            case 'NULL':
                return 'null';

            case 'integer':
                return (int) $var;

            case 'double':
            case 'float':
                return (float) $var;

            case 'string':
                // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
                $ascii = '';
                $strlen_var = strlen($var);

               /*
                * Iterate over every character in the string,
                * escaping with a slash or encoding to UTF-8 where necessary
                */
                for ($c = 0; $c < $strlen_var; ++$c) {

                    $ord_var_c = ord($var{$c});

                    switch (true) {
                        case $ord_var_c == 0x08:
                            $ascii .= '\b';
                            break;
                        case $ord_var_c == 0x09:
                            $ascii .= '\t';
                            break;
                        case $ord_var_c == 0x0A:
                            $ascii .= '\n';
                            break;
                        case $ord_var_c == 0x0C:
                            $ascii .= '\f';
                            break;
                        case $ord_var_c == 0x0D:
                            $ascii .= '\r';
                            break;

                        case $ord_var_c == 0x22:
                        case $ord_var_c == 0x2F:
                        case $ord_var_c == 0x5C:
                            // double quote, slash, slosh
                            $ascii .= '\\'.$var{$c};
                            break;

                        case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
                            // characters U-00000000 - U-0000007F (same as ASCII)
                            $ascii .= $var{$c};
                            break;

                        case (($ord_var_c & 0xE0) == 0xC0):
                            // characters U-00000080 - U-000007FF, mask 110XXXXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
                            $c += 1;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xF0) == 0xE0):
                            // characters U-00000800 - U-0000FFFF, mask 1110XXXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}));
                            $c += 2;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xF8) == 0xF0):
                            // characters U-00010000 - U-001FFFFF, mask 11110XXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}));
                            $c += 3;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xFC) == 0xF8):
                            // characters U-00200000 - U-03FFFFFF, mask 111110XX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}),
                                         ord($var{$c + 4}));
                            $c += 4;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xFE) == 0xFC):
                            // characters U-04000000 - U-7FFFFFFF, mask 1111110X
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}),
                                         ord($var{$c + 4}),
                                         ord($var{$c + 5}));
                            $c += 5;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;
                    }
                }

                return '"'.$ascii.'"';

            case 'array':
               /*
                * As per JSON spec if any array key is not an integer
                * we must treat the the whole array as an object. We
                * also try to catch a sparsely populated associative
                * array with numeric keys here because some JS engines
                * will create an array with empty indexes up to
                * max_index which can cause memory issues and because
                * the keys, which may be relevant, will be remapped
                * otherwise.
                *
                * As per the ECMA and JSON specification an object may
                * have any string as a property. Unfortunately due to
                * a hole in the ECMA specification if the key is a
                * ECMA reserved word or starts with a digit the
                * parameter is only accessible using ECMAScript's
                * bracket notation.
                */

                // treat as a JSON object
                if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
                    $properties = array_map(array($this, 'name_value'),
                                            array_keys($var),
                                            array_values($var));

                    foreach($properties as $property) {
                        if(Services_JSON::isError($property)) {
                            return $property;
                        }
                    }

                    return '{' . join(',', $properties) . '}';
                }

                // treat it like a regular array
                $elements = array_map(array($this, 'encode'), $var);

                foreach($elements as $element) {
                    if(Services_JSON::isError($element)) {
                        return $element;
                    }
                }

                return '[' . join(',', $elements) . ']';

            case 'object':
                $vars = get_object_vars($var);

                $properties = array_map(array($this, 'name_value'),
                                        array_keys($vars),
                                        array_values($vars));

                foreach($properties as $property) {
                    if(Services_JSON::isError($property)) {
                        return $property;
                    }
                }

                return '{' . join(',', $properties) . '}';

            default:
                return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
                    ? 'null'
                    : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
        }
    }

   /**
    * array-walking function for use in generating JSON-formatted name-value pairs
    *
    * @param    string  $name   name of key to use
    * @param    mixed   $value  reference to an array element to be encoded
    *
    * @return   string  JSON-formatted name-value pair, like '"name":value'
    * @access   private
    */
    function name_value($name, $value)
    {
        $encoded_value = $this->encode($value);

        if(Services_JSON::isError($encoded_value)) {
            return $encoded_value;
        }

        return $this->encode(strval($name)) . ':' . $encoded_value;
    }

   /**
    * reduce a string by removing leading and trailing comments and whitespace
    *
    * @param    $str    string      string value to strip of comments and whitespace
    *
    * @return   string  string value stripped of comments and whitespace
    * @access   private
    */
    function reduce_string($str)
    {
        $str = preg_replace(array(

                // eliminate single line comments in '// ...' form
                '#^\s*//(.+)$#m',

                // eliminate multi-line comments in '/* ... */' form, at start of string
                '#^\s*/\*(.+)\*/#Us',

                // eliminate multi-line comments in '/* ... */' form, at end of string
                '#/\*(.+)\*/\s*$#Us'

            ), '', $str);

        // eliminate extraneous space
        return trim($str);
    }

   /**
    * decodes a JSON string into appropriate variable
    *
    * @param    string  $str    JSON-formatted string
    *
    * @return   mixed   number, boolean, string, array, or object
    *                   corresponding to given JSON input string.
    *                   See argument 1 to Services_JSON() above for object-output behavior.
    *                   Note that decode() always returns strings
    *                   in ASCII or UTF-8 format!
    * @access   public
    */
    function decode($str)
    {
        $str = $this->reduce_string($str);

        switch (strtolower($str)) {
            case 'true':
                return true;

            case 'false':
                return false;

            case 'null':
                return null;

            default:
                $m = array();

                if (is_numeric($str)) {
                    // Lookie-loo, it's a number

                    // This would work on its own, but I'm trying to be
                    // good about returning integers where appropriate:
                    // return (float)$str;

                    // Return float or int, as appropriate
                    return ((float)$str == (integer)$str)
                        ? (integer)$str
                        : (float)$str;

                } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
                    // STRINGS RETURNED IN UTF-8 FORMAT
                    $delim = substr($str, 0, 1);
                    $chrs = substr($str, 1, -1);
                    $utf8 = '';
                    $strlen_chrs = strlen($chrs);

                    for ($c = 0; $c < $strlen_chrs; ++$c) {

                        $substr_chrs_c_2 = substr($chrs, $c, 2);
                        $ord_chrs_c = ord($chrs{$c});

                        switch (true) {
                            case $substr_chrs_c_2 == '\b':
                                $utf8 .= chr(0x08);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\t':
                                $utf8 .= chr(0x09);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\n':
                                $utf8 .= chr(0x0A);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\f':
                                $utf8 .= chr(0x0C);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\r':
                                $utf8 .= chr(0x0D);
                                ++$c;
                                break;

                            case $substr_chrs_c_2 == '\\"':
                            case $substr_chrs_c_2 == '\\\'':
                            case $substr_chrs_c_2 == '\\\\':
                            case $substr_chrs_c_2 == '\\/':
                                if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
                                   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
                                    $utf8 .= $chrs{++$c};
                                }
                                break;

                            case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
                                // single, escaped unicode character
                                $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
                                       . chr(hexdec(substr($chrs, ($c + 4), 2)));
                                $utf8 .= $this->utf162utf8($utf16);
                                $c += 5;
                                break;

                            case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
                                $utf8 .= $chrs{$c};
                                break;

                            case ($ord_chrs_c & 0xE0) == 0xC0:
                                // characters U-00000080 - U-000007FF, mask 110XXXXX
                                //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 2);
                                ++$c;
                                break;

                            case ($ord_chrs_c & 0xF0) == 0xE0:
                                // characters U-00000800 - U-0000FFFF, mask 1110XXXX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 3);
                                $c += 2;
                                break;

                            case ($ord_chrs_c & 0xF8) == 0xF0:
                                // characters U-00010000 - U-001FFFFF, mask 11110XXX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 4);
                                $c += 3;
                                break;

                            case ($ord_chrs_c & 0xFC) == 0xF8:
                                // characters U-00200000 - U-03FFFFFF, mask 111110XX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 5);
                                $c += 4;
                                break;

                            case ($ord_chrs_c & 0xFE) == 0xFC:
                                // characters U-04000000 - U-7FFFFFFF, mask 1111110X
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 6);
                                $c += 5;
                                break;

                        }

                    }

                    return $utf8;

                } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
                    // array, or object notation

                    if ($str{0} == '[') {
                        $stk = array(SERVICES_JSON_IN_ARR);
                        $arr = array();
                    } else {
                        if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                            $stk = array(SERVICES_JSON_IN_OBJ);
                            $obj = array();
                        } else {
                            $stk = array(SERVICES_JSON_IN_OBJ);
                            $obj = new stdClass();
                        }
                    }

                    array_push($stk, array('what'  => SERVICES_JSON_SLICE,
                                           'where' => 0,
                                           'delim' => false));

                    $chrs = substr($str, 1, -1);
                    $chrs = $this->reduce_string($chrs);

                    if ($chrs == '') {
                        if (reset($stk) == SERVICES_JSON_IN_ARR) {
                            return $arr;

                        } else {
                            return $obj;

                        }
                    }

                    //print("\nparsing {$chrs}\n");

                    $strlen_chrs = strlen($chrs);

                    for ($c = 0; $c <= $strlen_chrs; ++$c) {

                        $top = end($stk);
                        $substr_chrs_c_2 = substr($chrs, $c, 2);

                        if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
                            // found a comma that is not inside a string, array, etc.,
                            // OR we've reached the end of the character list
                            $slice = substr($chrs, $top['where'], ($c - $top['where']));
                            array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
                            //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                            if (reset($stk) == SERVICES_JSON_IN_ARR) {
                                // we are in an array, so just push an element onto the stack
                                array_push($arr, $this->decode($slice));

                            } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
                                // we are in an object, so figure
                                // out the property name and set an
                                // element in an associative array,
                                // for now
                                $parts = array();
                                
                                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
                                    // "name":value pair
                                    $key = $this->decode($parts[1]);
                                    $val = $this->decode($parts[2]);

                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                                        $obj[$key] = $val;
                                    } else {
                                        $obj->$key = $val;
                                    }
                                } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
                                    // name:value pair, where name is unquoted
                                    $key = $parts[1];
                                    $val = $this->decode($parts[2]);

                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                                        $obj[$key] = $val;
                                    } else {
                                        $obj->$key = $val;
                                    }
                                }

                            }

                        } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
                            // found a quote, and we are not inside a string
                            array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
                            //print("Found start of string at {$c}\n");

                        } elseif (($chrs{$c} == $top['delim']) &&
                                 ($top['what'] == SERVICES_JSON_IN_STR) &&
                                 ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
                            // found a quote, we're in a string, and it's not escaped
                            // we know that it's not escaped becase there is _not_ an
                            // odd number of backslashes at the end of the string so far
                            array_pop($stk);
                            //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");

                        } elseif (($chrs{$c} == '[') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a left-bracket, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
                            //print("Found start of array at {$c}\n");

                        } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
                            // found a right-bracket, and we're in an array
                            array_pop($stk);
                            //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        } elseif (($chrs{$c} == '{') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a left-brace, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
                            //print("Found start of object at {$c}\n");

                        } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
                            // found a right-brace, and we're in an object
                            array_pop($stk);
                            //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        } elseif (($substr_chrs_c_2 == '/*') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a comment start, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
                            $c++;
                            //print("Found start of comment at {$c}\n");

                        } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
                            // found a comment end, and we're in one now
                            array_pop($stk);
                            $c++;

                            for ($i = $top['where']; $i <= $c; ++$i)
                                $chrs = substr_replace($chrs, ' ', $i, 1);

                            //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        }

                    }

                    if (reset($stk) == SERVICES_JSON_IN_ARR) {
                        return $arr;

                    } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
                        return $obj;

                    }

                }
        }
    }

    /**
     * @todo Ultimately, this should just call PEAR::isError()
     */
    function isError($data, $code = null)
    {
        if (class_exists('pear')) {
            return PEAR::isError($data, $code);
        } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
                                 is_subclass_of($data, 'services_json_error'))) {
            return true;
        }

        return false;
    }
}

if (class_exists('PEAR_Error')) {

    class Services_JSON_Error extends PEAR_Error
    {
        function Services_JSON_Error($message = 'unknown error', $code = null,
                                     $mode = null, $options = null, $userinfo = null)
        {
            parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
        }
    }

} else {

    /**
     * @todo Ultimately, this class shall be descended from PEAR_Error
     */
    class Services_JSON_Error
    {
        function Services_JSON_Error($message = 'unknown error', $code = null,
                                     $mode = null, $options = null, $userinfo = null)
        {

        }
    }

}
    
?>
<?php

/**
 * Fastly Admin Panel.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyAdmin {
  /**
   * Initializes the fastly admin panel.
   */
  function FastlyAdmin() {
    // Setup admin interface
    add_action('admin_menu', array(&$this, 'adminPanel'));
    add_action('admin_init', array(&$this, 'adminInit'));
    
    // Add scripts and styles
    wp_register_style('fastly.css', $this->resource('fastly.css'));
    wp_enqueue_style('fastly.css');
    wp_register_script('fastly.js', $this->resource('fastly.js'));
    wp_enqueue_script('fastly.js');
    
    // Ajax Actions
    add_action('wp_ajax_set_page', array(&$this, 'ajaxSetPage'));
    add_action('wp_ajax_sign_up', array(&$this, 'ajaxSignUp'));
    
    #update_option('fastly_page', 'welcome');
    update_option('fastly_page', 'configure');
    
    /* Point to CI API Server
    update_option('fastly_api_hostname', '184.106.66.217');
    update_option('fastly_api_port', 5500);
    //*/
    
    /* Point to Dev API Server
    update_option('fastly_api_hostname', '10.235.5.18');
    update_option('fastly_api_port', 80);
    //*/
    
    // Grab an instance of the API adapter
    $this->api = new FastlyAPI(
      get_option('fastly_api_key'),
      get_option('fastly_api_hostname'),
      get_option('fastly_api_port')
    );
  }
  
  /**
   * @param $p Page name to test.
   * @return True if the given page is valid, false otherwise.
   */
  function validPage($p) {
    return in_array($p, array_keys($this->templates));
  }
  
  /**
   * Set the user's default page.
   */
  function ajaxSetPage() {
    if (isset($_REQUEST['page']) && $this->validPage($_REQUEST['page'])) {
      update_option('fastly_page', $_REQUEST['page']);
      die(1);
    }
    die();
  }
  
  /**
   * Make a sign up request to teh fastly API.
   */
  function ajaxSignUp() {
    unset($_REQUEST['action']);
    $_REQUEST['wizard'] = 'wordpress';
    
    $response = $this->api->post('/signup', $_REQUEST);
        
    if ($response === -1)
      wp_die('Could not connect to Fastly API.');
      
    $code = $response['code'];
    $body = $this->decode($response['body']);
    $defaultError = "An error occurred while connecting to the fastly API, please try your request again.";
    
    switch ($code) {
      case 200:
        update_option('fastly_api_key', $body['api_key']);
        update_option('fastly_service_id', $body['service_id']);
        update_option('fastly_page', 'configure');
        
        // Update internal host name
        $parts = explode('/', $_REQUEST['website_address']);
        if (count($parts) >= 3) {
          update_option('fastly_hostname', $parts[2]);
        }
        
        $response = array('status' => 'success');
        break;
      case 400:
        if ($body['class'] == "Customer")
          $msg = "A customer with that name already exists.";
        else if ($body['class'] == "User") {
          if (!empty($body['errors']['login']))
            $msg = "Invalid e-mail address.";
          else
            $msg = "A user with the given e-mail address already exists.";
        }
        else
          $msg = $defaultError;
        $response = array('status' => 'error', 'msg' => $msg);
        break;
      case 404:
        $response = array('status' => 'error', 'msg' => $defaultError);
        break;
    }
    
    die($this->encode($response));
  }
  
  
  /**
   * Called when admin is initialized by wordpress.
   */
  function adminInit() {
    // Config form group
    register_setting('fastly-group', 'fastly_hostname');
    register_setting('fastly-group', 'fastly_api_hostname');
    register_setting('fastly-group', 'fastly_api_port');
    register_setting('fastly-group', 'fastly_api_key');
    register_setting('fastly-group', 'fastly_service_id');
    register_setting('fastly-group', 'fastly_log_purges');
    
    // Page change group
    register_setting('fastly-page-group', 'fastly_page');
    
    // Generate front-end templates
    $this->templates = array(
      'welcome' => $this->welcome(),
      'configure' => $this->configure(),
    );

    // Get the current page
    $this->page = get_option('fastly_page');
    if (!$this->validPage($this->page)) {
      $this->page = 'welcome';
      update_option('fastly_page', 'welcome');
    }
  }
  
  /**
   * Adds the admin panel for the plugin.
   */
  function adminPanel() {
    add_options_page('Configure Fastly', 'Fastly', 'manage_options', 'fastly-admin-panel', array(&$this, 'render'));
  }
  
  /**
   * Fetches various static resources for the fastly plugin (js, css, images, etc.)
   * @param $name Name of the resource to fetch.
   * @return The URL to the resource in the plugin directory.
   */
  function resource($name='') {
    return FASTLY_PLUGIN_URL . 'static/' . $name;
  }
  
  /**
   * Backwards compatible JSON encoder.
   * @param $obj Object to encode.
   * @return The JSON encoding of the given object.
   */
  function encode($obj) {
    if (function_exists('json_encode')) {
      return json_encode($obj);
    }
    else {
      $json = new Services_JSON();
      return $json->encode($obj);
    }
  }

  /**
   * Backwards compatible JSON decode.
   * @param $str A json string.
   * @return The object represented by the json.
   */
  function decode($str) {
    if (function_exists('json_decode')) {
      return json_decode($str, true);
    }
    else {
      $json = new Services_JSON();
      return $json->decode($str);
    }
  }
  
  /**
   * @return Sign Up / Welcome page markup.
   */
  function welcome() {
    $customer = get_bloginfo('name');
    $address = $_SERVER['SERVER_ADDR'];
    $website_address = get_bloginfo('wpurl');
      
    return '
      <div class="signup fastly-admin-page">
        <h2>Sign Up</h2>
        <p class="error-flash"></p>
        <p>To create your free Fastly account enter your information, click the checkbox, and press the &quot;Sign Up&quot; button.</p>
        
        <fieldset>
          <p><b>Blog Name</b></p>
          <p><input class="text" id="customer" type="text" value="' . $customer . '"></p>
          <p><b>Your Name</b></p>
          <p><input class="text" id="name" type="text"></p>
          <p><b>Email Address</b></p>
          <p><input class="text" id="email" type="text"></p>
        </fieldset>
        
        <br>
        
        <fieldset>
          <p><b>Blog Address</b></p>
          <p><input class="text" id="website_address" type="text" value="' . $website_address . '"></p>
          <p><b>Server Address</b></p>
          <p><input class="text" id="address" type="text" value="' . $address . '"></p>
        </fieldset>
        
        <p><label id="agree_tos_label" for="agree_tos"><input id="agree_tos" type="checkbox"> I agree to the
          <a href="#" target="_blank">terms of service</a></label></p>
        
        <p class="button-row"><a href="#" class="button submit">Sign Up</a> <img class="loading" src="' . $this->resource('loading.gif') . '"></p>
      </div>
      
      <div class="welcome fastly-admin-page">
        <br>
        <p><a href="#" class="configure">Click here if you already have a Fastly account for your site.</a></p>
      </div>
    ';
  }
  
  /**
   * @return Configuration page markup.
   */
  function configure() {
    // TODO NEEDS TEH: $_SERVER['SERVER_ADDR']
    ob_start();    
    echo '
      <div class="configure fastly-admin-page">
        <h2>Configure</h2>
        <form method="post" action="options.php">
    ';
    
    settings_fields('fastly-group');
    $parts = parse_url( get_bloginfo('wpurl') );
    $testUrl = 'http://' . $parts['host'] . '.global.prod.fastly.net';
    if( !empty($parts['path']) ) {
      $testUrl .= $parts['path'];
    }
    
    echo '
          <fieldset>
            <p><b>Fastly API Key</b></p>
            <p><input class="text" type="text" name="fastly_api_key" value="' . get_option('fastly_api_key') . '"></p>
            <p><b>Service Id</b></p>
            <p><input class="text" type="text" name="fastly_service_id" value="' . get_option('fastly_service_id') . '"></p>
          </fieldset>
      
          <p><a href="#" class="advanced">Advanced Configuration</a></p>
      
          <fieldset class="advanced">
            <p><b>Fastly API Hostname</b></p>
            <p><input class="text" name="fastly_api_hostname" type="text" value="' . get_option('fastly_api_hostname') . '"></p>
            <p><b>Fastly API Port</b></p>
            <p><input class="text" name="fastly_api_port" type="text" value="' . get_option('fastly_api_port') . '"></p>
            <p><input class="checkbox" name="fastly_log_purges" type="checkbox" value="1" ' . ((int)get_option('fastly_log_purges')?"checked='checked'":"") . '> <b>Log purges to PHP errorlog</b></p>
            
            <!--
            <p><b></b></p>
            <p><input type="text" value=""></p>
            <p><b></b></p>
            <p><input type="text" value=""></p>
            -->
          </fieldset>
      
          <p><input type="submit" class="button" value="Save Settings"></p>
        </form>
      </div>
      <p>Test your site: <a href="' . $testUrl . '">' . $testUrl . '</a></p>
    ';
    
    $form = ob_get_contents();
    ob_end_clean();
    
    return $form;
  }
  
  /**
   * Initializes the JS for the page.
   */
  function initJS() {
    echo '<script type="text/javascript">
      Fastly.init("' . $this->page . '", ' . $this->encode($this->templates) . ');
    </script>';
  }
  
  /**
   * Renders the admin panel for the plugin.
   */
  function render() {
    if (!current_user_can('manage_options'))  {
      wp_die( __('You do not have sufficient permissions to access this page.') );
    }
    
    echo '<div id="fastly-admin" class="wrap">';
      echo '<h1><img alt="fastly" src="' . $this->resource('logo_white.gif') . '"><br><span style="font-size: x-small;">version: '. FASTLY_VERSION .'</span></h1>';
      echo '<div class="content">' . $this->templates[$this->page] . '</div>';
    echo '</div>';
    
    $this->initJS();
  }
}

// "Some people call me the space cowboy, yeah | Some call me the gangster of love | Some people call me Maurice" -- Steve Miller

?>
<?php

/**
 * Fastly API for PHP.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyAPI {  
  /**
   * Default constructor.
   * @param $api_key Fastly API key.
   * @param $host Hostname of the API server.
   * @param $port Port for the API server.
   */
  function FastlyAPI($api_key='', $host='https://app.fastly.com', $port=null) {
    $this->api_key   = $api_key;
    $this->host      = $host;
    $this->port      = $port;
    $this->host_name = preg_replace('/^(ssl|https?):\/\//', '', $host);
  }
  
  /**
   * Sends a purge request to the Fastly API.
   * @param $uri URI to purge.
   */
  function purge($uris) {
    // TODO How can we handle this more elegantly?
    if (!$this->api_key)
      return;
    
    if (!is_array($uris)) {
      $uris = array($uris);
    }
    // TODO - change this to a curl_multi_exec at some point
	$logPurges = (bool)get_option('fastly_log_purges');
    foreach ($uris as $uri) {
      #$uri = preg_replace("/^https?:\/\//", '', $uri); 
	  if( $logPurges ) {
        error_log("Purging " . $uri);
      }
      $this->post($uri);
    }
  }

  /**
   * Sends a purge all request to the Fastly API.
   */
  function purgeAll($service_id) {
    $url = $this->host;
    if (!is_null($this->port) && is_numeric($this->port)) {
      $url .= ":" . $this->port; 
    } 
    $url .= '/service/' . $service_id . '/purge_all';
      
    return $this->post($url, true);
  }

  /** 
   * Sends a post request to the Fastly API.
   * @param $path Path to call on the remote host.
   * @param $data Data for the body for the post request.
   * @return The response from the server or -1 if an error occurred.
   */
  function post($url, $do_post = true) {

    $headers = array();
    if ($this->api_key) {
      $headers[] = "Fastly-Key: " . $this->api_key;
    }

    $ch  = curl_init();
    # Temporary workaround to fix purging. Use POST instead of PURGE method.
    # Strip off protocol
    $url = get_option('fastly_api_hostname') . "/purge/" . preg_replace("/^http(s?):\/\//",'', $url);

	if( (bool)get_option('fastly_log_purges') ) {
      error_log("Purging using POST for " . $url);
    }

    curl_setopt($ch, CURLOPT_URL, $url );
    if ($do_post) {
      curl_setopt($ch, CURLOPT_POST, 1);
    } else {
      curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PURGE");      
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);
    return !!$response;
    
  }
} 

// "WHITE LIGHT, doo-doo doo-doo doo, WHITE LIGHT" -- Gorillaz
?>
<?php

/**
 * Fastly Purger - Detects changes in content and automatically sends purge requests.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyPurge {
  /** 
   * Default constructor - configures purge listeners.
   */
  function FastlyPurge() {
    // Posts and Pages
    add_action('edit_post', array(&$this, 'purgePost'), 99);
    add_action('edit_post', array(&$this, 'purgePostDependencies'), 99);
    add_action('transition_post_status', array(&$this,'purgePostStatus'),99, 3);
    add_action('deleted_post', array(&$this, 'purgePost'), 99);
    add_action('deleted_post', array(&$this, 'purgeCommon'), 99);
    
    // Comments
    add_action('comment_post', array(&$this, 'purgeComments'),99);
    add_action('edit_comment', array(&$this, 'purgeComments'),99);
    add_action('trashed_comment', array(&$this, 'purgeComments'),99);
    add_action('untrashed_comment', array(&$this, 'purgeComments'),99);
    add_action('deleted_comment', array(&$this, 'purgeComments'),99);
    
    // Full purges (theme changes, etc.)
    add_action('switch_theme', array(&$this, 'purgeAll'), 99);
    add_action('update_option_sidebars_widgets', array(&$this, 'purgeAll'), 99);
    add_action('widgets.php', array(&$this, 'purgeAll'), 99);
    add_action("update_option_theme_mods_".get_option('stylesheet'), array(&$this, 'purgeAll'), 99);
    
    // Links
    add_action("deleted_link",array(&$this, 'purgeLinks'), 99);
    add_action("edit_link",array(&$this, 'purgeLinks'), 99);
    add_action("add_link",array(&$this, 'purgeLinks'), 99);
    
    // Categories
    add_action("edit_category",array(&$this, 'purgeCategory'), 99);       
    add_action("edit_link_category",array(&$this, 'purgeLinkCategory'), 99);
    add_action("edit_post_tag",array(&$this, 'purgeTagCategory'), 99);
    
    // Setup API
    $this->api = new FastlyAPI(
      get_option('fastly_api_key'), 
      get_option('fastly_api_hostname'),
      get_option('fastly_api_port')
    );
  }
  
  /**
   * Sends a purge request for the given url.
   * @param $url URL to purge from the cache server.
   */
  function purge($urls) {
    $this->api->purge($urls);   
  }
  
  /**
   * Purges all pages on the site.
   */
  function purgeAll() {
    $this->api->purgeAll( get_option('fastly_service_id') );
  }
  
  /**
   * Purges common pages.
   */
  function purgeCommon($call=true) {
    $urls = array(get_bloginfo('wpurl')."/", get_bloginfo('wpurl')."/feed", get_bloginfo('wpurl')."/feed/atom");

    // TODO Need Regex Support
    /*
      if (get_site_option($this->wpv_update_pagenavi_optname) == 1) {
         $this->WPVarnishPurgerPurgeObject("/page/(.*)");
      }
    */
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges posts and pages on update.
   */
  function purgePost($postId, $call=true) {
    $urls = array( get_permalink($postId) );
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Purges objects that depend on the post.
   */
  function purgePostDependencies($postId, $call=true) { 
    $urls = array();
    $urls = array_merge($urls, $this->purgeCommon(false));
    $urls = array_merge($urls, $this->purgeCategories($postId, false));
    $urls = array_merge($urls, $this->purgeArchives($postId, false));
    $urls = array_merge($urls, $this->purgeTags($postId, false));
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges categories associated with a post.
   * @param $postId Id of the post.
   */
  function purgeCategories($postId, $call=true) {
    $urls       = array();
    $categories = get_the_category($postId);
    foreach ($categories as $cat) {
      $urls = array_merge($urls, $this->purgeCategory($cat->cat_ID, false));
    }
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges post comments.
   */
  function purgeComments($commentId, $call=true) {
    $comment  = get_comment($commentId);
    $approved = $comment->comment_approved;
    $urls     = array();

    if ($approved == 1 || $approved == 'trash') {
      $postId = $comment->comment_post_ID;
      #$this->purge('/\\\?comments_popup=' . $postId);
      $urls[] = get_bloginfo('wpurl') . '/?comments_popup=' . $postId;
      
      // TODO Need Regex Support
      /*
      if (get_site_option($this->wpv_update_commentnavi_optname) == 1) {
        $this->purge('/\\\?comments_popup=' . $postId . '&(.*)');
      }
      */
    }
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges links.
   */
  function purgeLinks() {
    if (is_active_widget(false, false, 'links')) {
        $this->purgeAll();
    }
  }
  
  /**
   * Purges post categories.
   * @param $categoryId Id of the category to purge.
   */
  function purgeCategory($categoryId, $call=true) {
    $urls = array();
    if (is_active_widget(false, false, 'categories')) {
      $this->purgeAll();
    }
    else {
      $urls[] = get_category_link($categoryId);
    }
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Purges link categories.
   * @param $categoryId Id of the category to purge.
   */
  function purgeLinkCategory($categoryId) {
    if (is_active_widget(false,false,'links')){
      $this->purgeAll();
    }
  }

  /**
   * Purges a tag category.
   * @param $categoryId Id of the category to purge.
   */
  function purgeTagCategory($categoryId, $call=true) {
    $urls = array( get_tag_link($categoryId) );
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges archives pages.
   * @param $postId Id of the post that triggered the purge.
   */
  function purgeArchives($postId, $call=true) {
    $urls = array(
      get_day_link(get_post_time('Y',false,$postId), get_post_time('m',true,$postId),get_post_time('d',true,$postId)),
      get_month_link(get_post_time('Y',false,$postId), get_post_time('m',true,$postId)),
      get_year_link(get_post_time('Y',false,$postId)),
    );

    return $call ? $this->purge($urls) : $urls;
  }  

  /**
   * Purges tags associated with a post.
   * @param $postId Id of the post being purged.
   */
  function purgeTags($postId, $call=true) {
    $urls = array();
    $tags = wp_get_post_tags($postId);
    foreach ($tags as $tag) {
      $urls = array_merge($urls, $this->purgeTagCategory($tag->term_id, false));
    }
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Handles post status purges.
   */
  function purgePostStatus($new_status, $old_status, $post, $call=true) {
    $urls = array();
    $urls = array_merge($urls, $this->purgePost($post->ID, false));
    $urls = array_merge($urls, $this->purgePostDependencies($post->ID, false));
    return $call ? $this->purge($urls) : $urls;
  }
}

// "While mona lisas and mad hatters, sons of bankers, sons of lawyers turn around and say good morning to the night..." -- Elton John

/*
 * Fastly Wordpress Plugin
 * Admin Interface Styles
 * @author Ryan Sandor Richards
 */
#fastly-admin h1 {
  float: left;
}

#fastly-admin .content {
  padding: 10px 0 0 20px;
  margin: 10px 0 0 200px;
}

#fastly-admin .content .button-row {
  margin: 20px 0;
  line-height: 20px;
}

#fastly-admin .error {
  background-color: #faa;
  padding: 4px;
}

#fastly-admin .error-flash {
  color: red;
  font-weight: bold;
}

#fastly-admin a.disabled {
  cursor: default !important;
}

#fastly-admin .loading {
  display: none;
  margin-bottom: -3px;
  margin-left: 4px;
}

#fastly-admin a.advanced {
  font-size: 9px;
}

#fastly-admin fieldset {
  -moz-border-radius: 6px;
  border-radius: 6px;
  background: #efefef;
  padding: 6px;
  width: 500px;
}

#fastly-admin fieldset.advanced {
  display: none;
}

#fastly-admin input.text {
  font-size: 18px;
  padding: 4px;
  width: 300px;
}

#fastly-admin fieldset input.text {
  width: 99%;
}

/*
 * Fastly Wordpress Plugin
 * Admin Interface Scripting
 * @author Ryan Sandor Richards
 */
window.Fastly = (function($) {
  var content, page, templates;

  /**
   * Validation information (emancipation proclaimation)
   */
  var validation = {
    '#email': {
      regex: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
      message: 'Please provide a valid email address.'
    },
    '#name': {
      message: 'Please enter your name.'
    },
    '#agree_tos': {
      checkbox: 'checked',
      message: 'Please agree to the Fastly terms of service.'
    },
    '#customer': {
      message: 'Please enter a customer (blog) name.'
    },
    '#website_address': {
      regex: /^http:\/\/.+$/,
      message: 'Please enter a valid website address'
    },
    '#address': {
      regex: /(\d+\.)+/,
      message: 'Please provide a valid IP address.'
    }
  }

  /**
   * UI Event Handlers
   */
  var events = {
    /**
     * Welcome page "existing account" button.
     */
    'click .welcome .configure': function(e) {
      loadPage('configure');
      $.ajax({
        url: ajaxurl, 
        data: {action: 'set_page', page: 'configure'}
      });
    },
    
    /**
     * Signup Form Submit.
     */
    'click .signup .submit': function(e) {
      var email = $('#email'),
        agree_tos = $('#agree_tos'),
        name = $('#name'),
        customer = $('#customer'),
        address = $('#address'),
        website_address = $('#website_address'),
        flash = $('#fastly-admin .error-flash'),
        messages = [];
      
      flash.html('');
      $('#email, #agree_tos_label').removeClass('error');

      // Perform validation checks
      for (var sel in validation) {
        var element = $(sel),
          value = $.trim( element.val() ),
          rules = validation[sel];
        
        if (typeof rules.regex != "undefined") {
          if (!value.match(rules.regex)) {
            messages.push(rules.message);
          }
        }
        else if (typeof rules.checkbox != "undefined") {
          if (typeof element.attr('checked') == "undefined" || element.attr('checked') == null) {
            messages.push(rules.message);
          }
        }
        else if (!value) {
          messages.push(rules.message);
        }
      }

      if (messages.length > 0) {
        flash.html( messages.join('<br>') );
        return;
      }
      
      // Send the sign up request
      function disable() {
        $(e.target).addClass('disabled');
        $('fieldset input').attr('disabled', true);
        $('.loading').show();
      }
      
      function enable() {
        $(e.target).removeClass('disabled');
        $('fieldset input').attr('disabled', false);
        $('.loading').hide();
      }
      
      disable();
      
      $.ajax({
        url: ajaxurl,
        data: {
          action: 'sign_up',
          customer: customer.val(),
          name: name.val(),
          email: email.val(),
          address: address.val(),
          website_address: website_address.val()
        },
        dataType: 'json',
        success: function(response) {
          if (response.status == "success") {
            window.location.reload();
          }
          else {
            flash.html(response.msg);
            enable();
          }
        },
        error: function() {
          flash.html("An error occurred while connecting to the fastly API, please try your request again.");
          enable();
        }
      });
    },
    
    /**
     * Toggle advanced settings.
     */
    'click .configure a.advanced': function(e) {
      $('fieldset.advanced').toggle(250);
    }
  };
  
  /**
   * Initalizes events for each of the pages.
   */
  function initEvents() {
    for (var k in events) {
      var parts = k.match(/([^\s]+)\s+(.*)/),
        name = parts[1],
        selector = parts[2];
        
      // Trixy hobittses...
      $(selector).live(name, (function(handler) {
        return function(e) {
          if ($(e.target).hasClass('disabled') && $(e.target).hasClass('button')) {
            e.stopPropagation();
            e.preventDefault();
            return;
          }
          return handler(e);
        };
      })(events[k]));
    }
  }
  
  /**
   * Loads the appropriate page given a name.
   */
  function loadPage(name) {
    if (typeof templates[name] == "undefined" || !templates[name]) {
      throw "Fastly.loadPage(): Undefined template '" + name + "'";
    }
    content.html( templates[name] );
  }
  
  
  // Set content element
  $(function() { content = $('#fastly-admin .content'); });
  
  /**
   * Public interface.
   */
  return {
    /**
     * Initializes the fastly pluging configuration page.
     * @param p Current page the user is on.
     * @param t A string containing the JSON representation of the page templates object.
     */
    init: function(p, t) {
      page = p;
      templates = t;
      initEvents();
    },
    
    loadPage: loadPage
  };
})(jQuery)

// We're the last living souls...

Fixed code:
# The Wordpress Plugin

Integrates Fastly with Wordpress' publishing tools.

## Installation

You can either install from source (you're looking at it), or from the Wordpress [plugin directory](http://wordpress.org/plugins/fastly/).

0. If you don't already have it send us a support request asking to have the Wordpress feature turned on for your account.
1. Add a new WordPress config to a Service and set up the path to the Wordpress install. Examples:
  - If your blog is at `http://blog.example.com/`, your path is `/`
  - If your blog is at `http://example.com/blog/`, your path is `/blog/`
2. Deploy the new Version of the Service.
3. With your API key and the Service id in hand, install the plugin under Wordpress.
4. Set up the Fastly plugin inside your Wordpress config panel - you should just have to input the API key and the Service id that you noted in the last step.
5. That's it! Everything should just work. :metal: If you have any problems, email us.

_Note: you may have to disable other caching plugins like W3TotalCache to avoid getting odd cache behaviour._

## What's going on?

Take a look at the inline comments in the [code](https://github.com/fastly/WordPress-Plugin/tree/master/lib) for an in depth description. But, the plugin:

- Pulls in the [Fastly API](http://docs.fastly.com/api)
- Wires Instant Purging into the publishing process, keeping content up to date
- Includes an admin panel in `wp-admin`

## License

Fastly.com WordPress Plugin
Copyright (C) 2011,2012,2013 Fastly.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

<?php

/*
Plugin Name: Fastly
Plugin URI: http://fastly.com/
Description: Configuration and cache purging for the Fastly CDN.
Author: Fastly.com
Version: 0.98
Author URI: http://fastly.com/
*/

/**
 * Main plugin code.
 * @package Fastly
 * @version 0.98
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */

// Basic plugin definitions
define('FASTLY_VERSION', '0.98');
define('FASTLY_PLUGIN_URL', plugin_dir_url( __FILE__ ));

// Includes
include_once dirname( __FILE__ ) . '/lib/purge.php';
include_once dirname( __FILE__ ) . '/lib/admin.php';
include_once dirname( __FILE__ ) . '/lib/api.php';

// Check for JSON support
if (!function_exists('json_decode')) {
  require_once dirname( __FILE__ ) . '/lib/JSON.php';
  define('FASTLY_JSON', false);
}

// Plugin Options
add_option('fastly_hostname', '');
add_option('fastly_api_key', '');
add_option('fastly_service_id', '');
add_option('fastly_api_hostname', 'https://api.fastly.com');
add_option('fastly_api_port', null);
add_option('fastly_page', 'welcome');
add_option('fastly_log_purges', '0');

// Setup Purging
new FastlyPurge();

// Setup admin (if needed)
if (is_admin()) {
  new FastlyAdmin();
}

// Custom action links for the plugin.
function fastly_action_links($links, $file) {
  static $this_plugin;
  if (!$this_plugin) {
    $this_plugin = plugin_basename(__FILE__);
  }
  if ($file == $this_plugin) {
    $settings_link = '<a href="' . get_bloginfo('wpurl') . '/wp-admin/options-general.php?page=fastly-admin-panel">Settings</a>';
    array_unshift($links, $settings_link);
  }
  return $links;
}
add_filter('plugin_action_links', 'fastly_action_links', 10, 2);

// "Look out honey, cause I'm using technology..." - Iggy Pop

?>
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Converts to and from JSON format.
 *
 * JSON (JavaScript Object Notation) is a lightweight data-interchange
 * format. It is easy for humans to read and write. It is easy for machines
 * to parse and generate. It is based on a subset of the JavaScript
 * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
 * This feature can also be found in  Python. JSON is a text format that is
 * completely language independent but uses conventions that are familiar
 * to programmers of the C-family of languages, including C, C++, C#, Java,
 * JavaScript, Perl, TCL, and many others. These properties make JSON an
 * ideal data-interchange language.
 *
 * This package provides a simple encoder and decoder for JSON notation. It
 * is intended for use with client-side Javascript applications that make
 * use of HTTPRequest to perform server communication functions - data can
 * be encoded into JSON notation for use in a client-side javascript, or
 * decoded from incoming Javascript requests. JSON format is native to
 * Javascript, and can be directly eval()'ed with no further parsing
 * overhead
 *
 * All strings should be in ASCII or UTF-8 format!
 *
 * LICENSE: Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met: Redistributions of source code must retain the
 * above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * @category
 * @package     Services_JSON
 * @author      Michal Migurski <mike-json@teczno.com>
 * @author      Matt Knapp <mdknapp[at]gmail[dot]com>
 * @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
 * @copyright   2005 Michal Migurski
 * @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
 * @license     http://www.opensource.org/licenses/bsd-license.php
 * @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
 */

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_SLICE',   1);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_STR',  2);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_ARR',  3);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_OBJ',  4);

/**
 * Marker constant for Services_JSON::decode(), used to flag stack state
 */
define('SERVICES_JSON_IN_CMT', 5);

/**
 * Behavior switch for Services_JSON::decode()
 */
define('SERVICES_JSON_LOOSE_TYPE', 16);

/**
 * Behavior switch for Services_JSON::decode()
 */
define('SERVICES_JSON_SUPPRESS_ERRORS', 32);

/**
 * Converts to and from JSON format.
 *
 * Brief example of use:
 *
 * <code>
 * // create a new instance of Services_JSON
 * $json = new Services_JSON();
 *
 * // convert a complexe value to JSON notation, and send it to the browser
 * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
 * $output = $json->encode($value);
 *
 * print($output);
 * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
 *
 * // accept incoming POST data, assumed to be in JSON notation
 * $input = file_get_contents('php://input', 1000000);
 * $value = $json->decode($input);
 * </code>
 */
class Services_JSON
{
   /**
    * constructs a new JSON instance
    *
    * @param    int     $use    object behavior flags; combine with boolean-OR
    *
    *                           possible values:
    *                           - SERVICES_JSON_LOOSE_TYPE:  loose typing.
    *                                   "{...}" syntax creates associative arrays
    *                                   instead of objects in decode().
    *                           - SERVICES_JSON_SUPPRESS_ERRORS:  error suppression.
    *                                   Values which can't be encoded (e.g. resources)
    *                                   appear as NULL instead of throwing errors.
    *                                   By default, a deeply-nested resource will
    *                                   bubble up with an error, so all return values
    *                                   from encode() should be checked with isError()
    */
    function Services_JSON($use = 0)
    {
        $this->use = $use;
    }

   /**
    * convert a string from one UTF-16 char to one UTF-8 char
    *
    * Normally should be handled by mb_convert_encoding, but
    * provides a slower PHP-only method for installations
    * that lack the multibye string extension.
    *
    * @param    string  $utf16  UTF-16 character
    * @return   string  UTF-8 character
    * @access   private
    */
    function utf162utf8($utf16)
    {
        // oh please oh please oh please oh please oh please
        if(function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
        }

        $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});

        switch(true) {
            case ((0x7F & $bytes) == $bytes):
                // this case should never be reached, because we are in ASCII range
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0x7F & $bytes);

            case (0x07FF & $bytes) == $bytes:
                // return a 2-byte UTF-8 character
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0xC0 | (($bytes >> 6) & 0x1F))
                     . chr(0x80 | ($bytes & 0x3F));

            case (0xFFFF & $bytes) == $bytes:
                // return a 3-byte UTF-8 character
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0xE0 | (($bytes >> 12) & 0x0F))
                     . chr(0x80 | (($bytes >> 6) & 0x3F))
                     . chr(0x80 | ($bytes & 0x3F));
        }

        // ignoring UTF-32 for now, sorry
        return '';
    }

   /**
    * convert a string from one UTF-8 char to one UTF-16 char
    *
    * Normally should be handled by mb_convert_encoding, but
    * provides a slower PHP-only method for installations
    * that lack the multibye string extension.
    *
    * @param    string  $utf8   UTF-8 character
    * @return   string  UTF-16 character
    * @access   private
    */
    function utf82utf16($utf8)
    {
        // oh please oh please oh please oh please oh please
        if(function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
        }

        switch(strlen($utf8)) {
            case 1:
                // this case should never be reached, because we are in ASCII range
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return $utf8;

            case 2:
                // return a UTF-16 character from a 2-byte UTF-8 char
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr(0x07 & (ord($utf8{0}) >> 2))
                     . chr((0xC0 & (ord($utf8{0}) << 6))
                         | (0x3F & ord($utf8{1})));

            case 3:
                // return a UTF-16 character from a 3-byte UTF-8 char
                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                return chr((0xF0 & (ord($utf8{0}) << 4))
                         | (0x0F & (ord($utf8{1}) >> 2)))
                     . chr((0xC0 & (ord($utf8{1}) << 6))
                         | (0x7F & ord($utf8{2})));
        }

        // ignoring UTF-32 for now, sorry
        return '';
    }

   /**
    * encodes an arbitrary variable into JSON format
    *
    * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
    *                           see argument 1 to Services_JSON() above for array-parsing behavior.
    *                           if var is a strng, note that encode() always expects it
    *                           to be in ASCII or UTF-8 format!
    *
    * @return   mixed   JSON string representation of input var or an error if a problem occurs
    * @access   public
    */
    function encode($var)
    {
        switch (gettype($var)) {
            case 'boolean':
                return $var ? 'true' : 'false';

            case 'NULL':
                return 'null';

            case 'integer':
                return (int) $var;

            case 'double':
            case 'float':
                return (float) $var;

            case 'string':
                // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
                $ascii = '';
                $strlen_var = strlen($var);

               /*
                * Iterate over every character in the string,
                * escaping with a slash or encoding to UTF-8 where necessary
                */
                for ($c = 0; $c < $strlen_var; ++$c) {

                    $ord_var_c = ord($var{$c});

                    switch (true) {
                        case $ord_var_c == 0x08:
                            $ascii .= '\b';
                            break;
                        case $ord_var_c == 0x09:
                            $ascii .= '\t';
                            break;
                        case $ord_var_c == 0x0A:
                            $ascii .= '\n';
                            break;
                        case $ord_var_c == 0x0C:
                            $ascii .= '\f';
                            break;
                        case $ord_var_c == 0x0D:
                            $ascii .= '\r';
                            break;

                        case $ord_var_c == 0x22:
                        case $ord_var_c == 0x2F:
                        case $ord_var_c == 0x5C:
                            // double quote, slash, slosh
                            $ascii .= '\\'.$var{$c};
                            break;

                        case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
                            // characters U-00000000 - U-0000007F (same as ASCII)
                            $ascii .= $var{$c};
                            break;

                        case (($ord_var_c & 0xE0) == 0xC0):
                            // characters U-00000080 - U-000007FF, mask 110XXXXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
                            $c += 1;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xF0) == 0xE0):
                            // characters U-00000800 - U-0000FFFF, mask 1110XXXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}));
                            $c += 2;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xF8) == 0xF0):
                            // characters U-00010000 - U-001FFFFF, mask 11110XXX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}));
                            $c += 3;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xFC) == 0xF8):
                            // characters U-00200000 - U-03FFFFFF, mask 111110XX
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}),
                                         ord($var{$c + 4}));
                            $c += 4;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;

                        case (($ord_var_c & 0xFE) == 0xFC):
                            // characters U-04000000 - U-7FFFFFFF, mask 1111110X
                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                            $char = pack('C*', $ord_var_c,
                                         ord($var{$c + 1}),
                                         ord($var{$c + 2}),
                                         ord($var{$c + 3}),
                                         ord($var{$c + 4}),
                                         ord($var{$c + 5}));
                            $c += 5;
                            $utf16 = $this->utf82utf16($char);
                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
                            break;
                    }
                }

                return '"'.$ascii.'"';

            case 'array':
               /*
                * As per JSON spec if any array key is not an integer
                * we must treat the the whole array as an object. We
                * also try to catch a sparsely populated associative
                * array with numeric keys here because some JS engines
                * will create an array with empty indexes up to
                * max_index which can cause memory issues and because
                * the keys, which may be relevant, will be remapped
                * otherwise.
                *
                * As per the ECMA and JSON specification an object may
                * have any string as a property. Unfortunately due to
                * a hole in the ECMA specification if the key is a
                * ECMA reserved word or starts with a digit the
                * parameter is only accessible using ECMAScript's
                * bracket notation.
                */

                // treat as a JSON object
                if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
                    $properties = array_map(array($this, 'name_value'),
                                            array_keys($var),
                                            array_values($var));

                    foreach($properties as $property) {
                        if(Services_JSON::isError($property)) {
                            return $property;
                        }
                    }

                    return '{' . join(',', $properties) . '}';
                }

                // treat it like a regular array
                $elements = array_map(array($this, 'encode'), $var);

                foreach($elements as $element) {
                    if(Services_JSON::isError($element)) {
                        return $element;
                    }
                }

                return '[' . join(',', $elements) . ']';

            case 'object':
                $vars = get_object_vars($var);

                $properties = array_map(array($this, 'name_value'),
                                        array_keys($vars),
                                        array_values($vars));

                foreach($properties as $property) {
                    if(Services_JSON::isError($property)) {
                        return $property;
                    }
                }

                return '{' . join(',', $properties) . '}';

            default:
                return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
                    ? 'null'
                    : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
        }
    }

   /**
    * array-walking function for use in generating JSON-formatted name-value pairs
    *
    * @param    string  $name   name of key to use
    * @param    mixed   $value  reference to an array element to be encoded
    *
    * @return   string  JSON-formatted name-value pair, like '"name":value'
    * @access   private
    */
    function name_value($name, $value)
    {
        $encoded_value = $this->encode($value);

        if(Services_JSON::isError($encoded_value)) {
            return $encoded_value;
        }

        return $this->encode(strval($name)) . ':' . $encoded_value;
    }

   /**
    * reduce a string by removing leading and trailing comments and whitespace
    *
    * @param    $str    string      string value to strip of comments and whitespace
    *
    * @return   string  string value stripped of comments and whitespace
    * @access   private
    */
    function reduce_string($str)
    {
        $str = preg_replace(array(

                // eliminate single line comments in '// ...' form
                '#^\s*//(.+)$#m',

                // eliminate multi-line comments in '/* ... */' form, at start of string
                '#^\s*/\*(.+)\*/#Us',

                // eliminate multi-line comments in '/* ... */' form, at end of string
                '#/\*(.+)\*/\s*$#Us'

            ), '', $str);

        // eliminate extraneous space
        return trim($str);
    }

   /**
    * decodes a JSON string into appropriate variable
    *
    * @param    string  $str    JSON-formatted string
    *
    * @return   mixed   number, boolean, string, array, or object
    *                   corresponding to given JSON input string.
    *                   See argument 1 to Services_JSON() above for object-output behavior.
    *                   Note that decode() always returns strings
    *                   in ASCII or UTF-8 format!
    * @access   public
    */
    function decode($str)
    {
        $str = $this->reduce_string($str);

        switch (strtolower($str)) {
            case 'true':
                return true;

            case 'false':
                return false;

            case 'null':
                return null;

            default:
                $m = array();

                if (is_numeric($str)) {
                    // Lookie-loo, it's a number

                    // This would work on its own, but I'm trying to be
                    // good about returning integers where appropriate:
                    // return (float)$str;

                    // Return float or int, as appropriate
                    return ((float)$str == (integer)$str)
                        ? (integer)$str
                        : (float)$str;

                } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
                    // STRINGS RETURNED IN UTF-8 FORMAT
                    $delim = substr($str, 0, 1);
                    $chrs = substr($str, 1, -1);
                    $utf8 = '';
                    $strlen_chrs = strlen($chrs);

                    for ($c = 0; $c < $strlen_chrs; ++$c) {

                        $substr_chrs_c_2 = substr($chrs, $c, 2);
                        $ord_chrs_c = ord($chrs{$c});

                        switch (true) {
                            case $substr_chrs_c_2 == '\b':
                                $utf8 .= chr(0x08);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\t':
                                $utf8 .= chr(0x09);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\n':
                                $utf8 .= chr(0x0A);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\f':
                                $utf8 .= chr(0x0C);
                                ++$c;
                                break;
                            case $substr_chrs_c_2 == '\r':
                                $utf8 .= chr(0x0D);
                                ++$c;
                                break;

                            case $substr_chrs_c_2 == '\\"':
                            case $substr_chrs_c_2 == '\\\'':
                            case $substr_chrs_c_2 == '\\\\':
                            case $substr_chrs_c_2 == '\\/':
                                if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
                                   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
                                    $utf8 .= $chrs{++$c};
                                }
                                break;

                            case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
                                // single, escaped unicode character
                                $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
                                       . chr(hexdec(substr($chrs, ($c + 4), 2)));
                                $utf8 .= $this->utf162utf8($utf16);
                                $c += 5;
                                break;

                            case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
                                $utf8 .= $chrs{$c};
                                break;

                            case ($ord_chrs_c & 0xE0) == 0xC0:
                                // characters U-00000080 - U-000007FF, mask 110XXXXX
                                //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 2);
                                ++$c;
                                break;

                            case ($ord_chrs_c & 0xF0) == 0xE0:
                                // characters U-00000800 - U-0000FFFF, mask 1110XXXX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 3);
                                $c += 2;
                                break;

                            case ($ord_chrs_c & 0xF8) == 0xF0:
                                // characters U-00010000 - U-001FFFFF, mask 11110XXX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 4);
                                $c += 3;
                                break;

                            case ($ord_chrs_c & 0xFC) == 0xF8:
                                // characters U-00200000 - U-03FFFFFF, mask 111110XX
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 5);
                                $c += 4;
                                break;

                            case ($ord_chrs_c & 0xFE) == 0xFC:
                                // characters U-04000000 - U-7FFFFFFF, mask 1111110X
                                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                                $utf8 .= substr($chrs, $c, 6);
                                $c += 5;
                                break;

                        }

                    }

                    return $utf8;

                } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
                    // array, or object notation

                    if ($str{0} == '[') {
                        $stk = array(SERVICES_JSON_IN_ARR);
                        $arr = array();
                    } else {
                        if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                            $stk = array(SERVICES_JSON_IN_OBJ);
                            $obj = array();
                        } else {
                            $stk = array(SERVICES_JSON_IN_OBJ);
                            $obj = new stdClass();
                        }
                    }

                    array_push($stk, array('what'  => SERVICES_JSON_SLICE,
                                           'where' => 0,
                                           'delim' => false));

                    $chrs = substr($str, 1, -1);
                    $chrs = $this->reduce_string($chrs);

                    if ($chrs == '') {
                        if (reset($stk) == SERVICES_JSON_IN_ARR) {
                            return $arr;

                        } else {
                            return $obj;

                        }
                    }

                    //print("\nparsing {$chrs}\n");

                    $strlen_chrs = strlen($chrs);

                    for ($c = 0; $c <= $strlen_chrs; ++$c) {

                        $top = end($stk);
                        $substr_chrs_c_2 = substr($chrs, $c, 2);

                        if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
                            // found a comma that is not inside a string, array, etc.,
                            // OR we've reached the end of the character list
                            $slice = substr($chrs, $top['where'], ($c - $top['where']));
                            array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
                            //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                            if (reset($stk) == SERVICES_JSON_IN_ARR) {
                                // we are in an array, so just push an element onto the stack
                                array_push($arr, $this->decode($slice));

                            } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
                                // we are in an object, so figure
                                // out the property name and set an
                                // element in an associative array,
                                // for now
                                $parts = array();
                                
                                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
                                    // "name":value pair
                                    $key = $this->decode($parts[1]);
                                    $val = $this->decode($parts[2]);

                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                                        $obj[$key] = $val;
                                    } else {
                                        $obj->$key = $val;
                                    }
                                } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
                                    // name:value pair, where name is unquoted
                                    $key = $parts[1];
                                    $val = $this->decode($parts[2]);

                                    if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                                        $obj[$key] = $val;
                                    } else {
                                        $obj->$key = $val;
                                    }
                                }

                            }

                        } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
                            // found a quote, and we are not inside a string
                            array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
                            //print("Found start of string at {$c}\n");

                        } elseif (($chrs{$c} == $top['delim']) &&
                                 ($top['what'] == SERVICES_JSON_IN_STR) &&
                                 ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
                            // found a quote, we're in a string, and it's not escaped
                            // we know that it's not escaped becase there is _not_ an
                            // odd number of backslashes at the end of the string so far
                            array_pop($stk);
                            //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");

                        } elseif (($chrs{$c} == '[') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a left-bracket, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
                            //print("Found start of array at {$c}\n");

                        } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
                            // found a right-bracket, and we're in an array
                            array_pop($stk);
                            //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        } elseif (($chrs{$c} == '{') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a left-brace, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
                            //print("Found start of object at {$c}\n");

                        } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
                            // found a right-brace, and we're in an object
                            array_pop($stk);
                            //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        } elseif (($substr_chrs_c_2 == '/*') &&
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
                            // found a comment start, and we are in an array, object, or slice
                            array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
                            $c++;
                            //print("Found start of comment at {$c}\n");

                        } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
                            // found a comment end, and we're in one now
                            array_pop($stk);
                            $c++;

                            for ($i = $top['where']; $i <= $c; ++$i)
                                $chrs = substr_replace($chrs, ' ', $i, 1);

                            //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");

                        }

                    }

                    if (reset($stk) == SERVICES_JSON_IN_ARR) {
                        return $arr;

                    } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
                        return $obj;

                    }

                }
        }
    }

    /**
     * @todo Ultimately, this should just call PEAR::isError()
     */
    function isError($data, $code = null)
    {
        if (class_exists('pear')) {
            return PEAR::isError($data, $code);
        } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
                                 is_subclass_of($data, 'services_json_error'))) {
            return true;
        }

        return false;
    }
}

if (class_exists('PEAR_Error')) {

    class Services_JSON_Error extends PEAR_Error
    {
        function Services_JSON_Error($message = 'unknown error', $code = null,
                                     $mode = null, $options = null, $userinfo = null)
        {
            parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
        }
    }

} else {

    /**
     * @todo Ultimately, this class shall be descended from PEAR_Error
     */
    class Services_JSON_Error
    {
        function Services_JSON_Error($message = 'unknown error', $code = null,
                                     $mode = null, $options = null, $userinfo = null)
        {

        }
    }

}
    
?>
<?php

/**
 * Fastly Admin Panel.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyAdmin {
  /**
   * Initializes the fastly admin panel.
   */
  function FastlyAdmin() {
    // Setup admin interface
    add_action('admin_menu', array(&$this, 'adminPanel'));
    add_action('admin_init', array(&$this, 'adminInit'));
    
    // Register scripts and styles
    add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
    
    // Ajax Actions
    add_action('wp_ajax_set_page', array(&$this, 'ajaxSetPage'));
    add_action('wp_ajax_sign_up', array(&$this, 'ajaxSignUp'));
    
    #update_option('fastly_page', 'welcome');
    update_option('fastly_page', 'configure');
    
    /* Point to CI API Server
    update_option('fastly_api_hostname', '184.106.66.217');
    update_option('fastly_api_port', 5500);
    //*/
    
    /* Point to Dev API Server
    update_option('fastly_api_hostname', '10.235.5.18');
    update_option('fastly_api_port', 80);
    //*/
    
    // Grab an instance of the API adapter
    $this->api = new FastlyAPI(
      get_option('fastly_api_key'),
      get_option('fastly_api_hostname'),
      get_option('fastly_api_port')
    );
  }

  /**
   * Register scripts and styles needed for the admin option page.
   *
   * @return void
   */
  function admin_enqueue_scripts( $hook_suffix ) {
    if ('settings_page_fastly-admin-panel' !== $hook_suffix) {
      return;
    }

    // Add scripts and styles
    wp_register_style('fastly.css', $this->resource('fastly.css'));
    wp_enqueue_style('fastly.css');

    wp_register_script('fastly.js', $this->resource('fastly.js'));
    // Expose a WP CSRF nonce to the fastly.js script
    $nonce = wp_create_nonce('fastly-admin');
    wp_localize_script('fastly.js', 'fastlyNonce', $nonce);
    wp_enqueue_script('fastly.js');
  }
  
  /**
   * @param $p Page name to test.
   * @return True if the given page is valid, false otherwise.
   */
  function validPage($p) {
    return in_array($p, array_keys($this->templates));
  }
  
  /**
   * Set the user's default page.
   */
  function ajaxSetPage() {
    if (!isset($_REQUEST['nonce']) || !wp_verify_nonce($_REQUEST['nonce'], 'fastly-admin')) {
      wp_die('Bad CSRF Nonce.');
    }

    if (isset($_REQUEST['page']) && $this->validPage($_REQUEST['page'])) {
      update_option('fastly_page', esc_sql($_REQUEST['page']));
      die(1);
    }
    die();
  }
  
  /**
   * Make a sign up request to teh fastly API.
   */
  function ajaxSignUp() {
    if (!isset($_REQUEST['nonce']) || !wp_verify_nonce($_REQUEST['nonce'], 'fastly-admin')) {
      wp_die('Bad CSRF Nonce.');
    }

    unset($_REQUEST['action']);
    $_REQUEST['wizard'] = 'wordpress';
    
    $response = $this->api->post('/signup', $_REQUEST);
        
    if ($response === -1)
      wp_die('Could not connect to Fastly API.');
      
    $code = $response['code'];
    $body = $this->decode($response['body']);
    $defaultError = "An error occurred while connecting to the fastly API, please try your request again.";
    
    switch ($code) {
      case 200:
        update_option('fastly_api_key', esc_sql($body['api_key']));
        update_option('fastly_service_id', esc_sql($body['service_id']));
        update_option('fastly_page', 'configure');
        
        // Update internal host name
        $parts = explode('/', $_REQUEST['website_address']);
        if (count($parts) >= 3) {
          update_option('fastly_hostname', esc_sql($parts[2]));
        }
        
        $response = array('status' => 'success');
        break;
      case 400:
        if ($body['class'] == "Customer")
          $msg = "A customer with that name already exists.";
        else if ($body['class'] == "User") {
          if (!empty($body['errors']['login']))
            $msg = "Invalid e-mail address.";
          else
            $msg = "A user with the given e-mail address already exists.";
        }
        else
          $msg = $defaultError;
        $response = array('status' => 'error', 'msg' => $msg);
        break;
      case 404:
        $response = array('status' => 'error', 'msg' => $defaultError);
        break;
    }
    
    die($this->encode($response));
  }
  
  
  /**
   * Called when admin is initialized by wordpress.
   */
  function adminInit() {
    // Config form group
    register_setting('fastly-group', 'fastly_hostname');
    register_setting('fastly-group', 'fastly_api_hostname');
    register_setting('fastly-group', 'fastly_api_port');
    register_setting('fastly-group', 'fastly_api_key');
    register_setting('fastly-group', 'fastly_service_id');
    register_setting('fastly-group', 'fastly_log_purges');
    
    // Page change group
    register_setting('fastly-page-group', 'fastly_page');
    
    // Generate front-end templates
    $this->templates = array(
      'welcome' => $this->welcome(),
      'configure' => $this->configure(),
    );

    // Get the current page
    $this->page = get_option('fastly_page');
    if (!$this->validPage($this->page)) {
      $this->page = 'welcome';
      update_option('fastly_page', 'welcome');
    }
  }
  
  /**
   * Adds the admin panel for the plugin.
   */
  function adminPanel() {
    add_options_page('Configure Fastly', 'Fastly', 'manage_options', 'fastly-admin-panel', array(&$this, 'render'));
  }
  
  /**
   * Fetches various static resources for the fastly plugin (js, css, images, etc.)
   * @param $name Name of the resource to fetch.
   * @return The URL to the resource in the plugin directory.
   */
  function resource($name='') {
    return FASTLY_PLUGIN_URL . 'static/' . $name;
  }
  
  /**
   * Backwards compatible JSON encoder.
   * @param $obj Object to encode.
   * @return The JSON encoding of the given object.
   */
  function encode($obj) {
    if (function_exists('json_encode')) {
      return json_encode($obj);
    }
    else {
      $json = new Services_JSON();
      return $json->encode($obj);
    }
  }

  /**
   * Backwards compatible JSON decode.
   * @param $str A json string.
   * @return The object represented by the json.
   */
  function decode($str) {
    if (function_exists('json_decode')) {
      return json_decode($str, true);
    }
    else {
      $json = new Services_JSON();
      return $json->decode($str);
    }
  }
  
  /**
   * @return Sign Up / Welcome page markup.
   */
  function welcome() {
    $customer = get_bloginfo('name');
    $address = $_SERVER['SERVER_ADDR'];
    $website_address = get_bloginfo('wpurl');
      
    return '
      <div class="signup fastly-admin-page">
        <h2>Sign Up</h2>
        <p class="error-flash"></p>
        <p>To create your free Fastly account enter your information, click the checkbox, and press the &quot;Sign Up&quot; button.</p>
        
        <fieldset>
          <p><b>Blog Name</b></p>
          <p><input class="text" id="customer" type="text" value="' . esc_attr($customer) . '"></p>
          <p><b>Your Name</b></p>
          <p><input class="text" id="name" type="text"></p>
          <p><b>Email Address</b></p>
          <p><input class="text" id="email" type="text"></p>
        </fieldset>
        
        <br>
        
        <fieldset>
          <p><b>Blog Address</b></p>
          <p><input class="text" id="website_address" type="text" value="' . esc_url($website_address) . '"></p>
          <p><b>Server Address</b></p>
          <p><input class="text" id="address" type="text" value="' . esc_attr($address) . '"></p>
        </fieldset>
        
        <p><label id="agree_tos_label" for="agree_tos"><input id="agree_tos" type="checkbox"> I agree to the
          <a href="#" target="_blank">terms of service</a></label></p>
        
        <p class="button-row"><a href="#" class="button submit">Sign Up</a> <img class="loading" src="' . $this->resource('loading.gif') . '"></p>
      </div>
      
      <div class="welcome fastly-admin-page">
        <br>
        <p><a href="#" class="configure">Click here if you already have a Fastly account for your site.</a></p>
      </div>
    ';
  }
  
  /**
   * @return Configuration page markup.
   */
  function configure() {
    // TODO NEEDS TEH: $_SERVER['SERVER_ADDR']
    ob_start();    
    echo '
      <div class="configure fastly-admin-page">
        <h2>Configure</h2>
        <form method="post" action="options.php">
    ';
    
    settings_fields('fastly-group');
    $parts = parse_url( get_bloginfo('wpurl') );
    $testUrl = 'http://' . $parts['host'] . '.global.prod.fastly.net';
    if( !empty($parts['path']) ) {
      $testUrl .= $parts['path'];
    }
    
    echo '
          <fieldset>
            <p><b>Fastly API Key</b></p>
            <p><input class="text" type="text" name="fastly_api_key" value="' . esc_attr(get_option('fastly_api_key')) . '"></p>
            <p><b>Service Id</b></p>
            <p><input class="text" type="text" name="fastly_service_id" value="' . esc_attr(get_option('fastly_service_id')) . '"></p>
          </fieldset>
      
          <p><a href="#" class="advanced">Advanced Configuration</a></p>
      
          <fieldset class="advanced">
            <p><b>Fastly API Hostname</b></p>
            <p><input class="text" name="fastly_api_hostname" type="text" value="' . esc_url(get_option('fastly_api_hostname')) . '"></p>
            <p><b>Fastly API Port</b></p>
            <p><input class="text" name="fastly_api_port" type="text" value="' . esc_attr(get_option('fastly_api_port')) . '"></p>
            <p><input class="checkbox" name="fastly_log_purges" type="checkbox" value="1" ' . ((int)get_option('fastly_log_purges')?"checked='checked'":"") . '> <b>Log purges to PHP errorlog</b></p>
            
            <!--
            <p><b></b></p>
            <p><input type="text" value=""></p>
            <p><b></b></p>
            <p><input type="text" value=""></p>
            -->
          </fieldset>
      
          <p><input type="submit" class="button" value="Save Settings"></p>
        </form>
      </div>
      <p>Test your site: <a href="' . esc_url($testUrl) . '">' . esc_url($testUrl) . '</a></p>
    ';
    
    $form = ob_get_contents();
    ob_end_clean();
    
    return $form;
  }
  
  /**
   * Initializes the JS for the page.
   */
  function initJS() {
    echo '<script type="text/javascript">
      Fastly.init("' . $this->page . '", ' . $this->encode($this->templates) . ');
    </script>';
  }
  
  /**
   * Renders the admin panel for the plugin.
   */
  function render() {
    if (!current_user_can('manage_options'))  {
      wp_die( __('You do not have sufficient permissions to access this page.') );
    }
    
    echo '<div id="fastly-admin" class="wrap">';
      echo '<h1><img alt="fastly" src="' . $this->resource('logo_white.gif') . '"><br><span style="font-size: x-small;">version: '. FASTLY_VERSION .'</span></h1>';
      echo '<div class="content">' . $this->templates[$this->page] . '</div>';
    echo '</div>';
    
    $this->initJS();
  }
}

// "Some people call me the space cowboy, yeah | Some call me the gangster of love | Some people call me Maurice" -- Steve Miller

?>
<?php

/**
 * Fastly API for PHP.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyAPI {  
  /**
   * Default constructor.
   * @param $api_key Fastly API key.
   * @param $host Hostname of the API server.
   * @param $port Port for the API server.
   */
  function FastlyAPI($api_key='', $host='https://app.fastly.com', $port=null) {
    $this->api_key   = $api_key;
    $this->host      = $host;
    $this->port      = $port;
    $this->host_name = preg_replace('/^(ssl|https?):\/\//', '', $host);
  }
  
  /**
   * Sends a purge request to the Fastly API.
   * @param $uri URI to purge.
   */
  function purge($uris) {
    // TODO How can we handle this more elegantly?
    if (!$this->api_key)
      return;
    
    if (!is_array($uris)) {
      $uris = array($uris);
    }
    // TODO - change this to a curl_multi_exec at some point
	$logPurges = (bool)get_option('fastly_log_purges');
    foreach ($uris as $uri) {
      #$uri = preg_replace("/^https?:\/\//", '', $uri); 
	  if( $logPurges ) {
        error_log("Purging " . $uri);
      }
      $this->post($uri);
    }
  }

  /**
   * Sends a purge all request to the Fastly API.
   */
  function purgeAll($service_id) {
    $url = $this->host;
    if (!is_null($this->port) && is_numeric($this->port)) {
      $url .= ":" . $this->port; 
    } 
    $url .= '/service/' . $service_id . '/purge_all';
      
    return $this->post($url, true);
  }

  /** 
   * Sends a post request to the Fastly API.
   * @param $path Path to call on the remote host.
   * @param $data Data for the body for the post request.
   * @return The response from the server or -1 if an error occurred.
   */
  function post($url, $do_post = true) {

    $headers = array();
    if ($this->api_key) {
      $headers[] = "Fastly-Key: " . $this->api_key;
    }

    $ch  = curl_init();
    # Temporary workaround to fix purging. Use POST instead of PURGE method.
    # Strip off protocol
    $url = get_option('fastly_api_hostname') . "/purge/" . preg_replace("/^http(s?):\/\//",'', $url);

	if( (bool)get_option('fastly_log_purges') ) {
      error_log("Purging using POST for " . esc_url($url));
    }

    $args = array(
        'headers' => $headers,
        'method'  => (true === $do_post) ? 'POST' : 'PURGE',
    );

    $response = wp_remote_request($url, $args);

    return ( is_wp_error( $response ) ) ? -1 : $response;
  }
} 

// "WHITE LIGHT, doo-doo doo-doo doo, WHITE LIGHT" -- Gorillaz
?>
<?php

/**
 * Fastly Purger - Detects changes in content and automatically sends purge requests.
 * @package Fastly
 * @author Ryan Sandor Richards
 * @copyright 2011 Fastly.com, All Rights Reserved
 */
class FastlyPurge {
  /** 
   * Default constructor - configures purge listeners.
   */
  function FastlyPurge() {
    // Posts and Pages
    add_action('edit_post', array(&$this, 'purgePost'), 99);
    add_action('edit_post', array(&$this, 'purgePostDependencies'), 99);
    add_action('transition_post_status', array(&$this,'purgePostStatus'),99, 3);
    add_action('deleted_post', array(&$this, 'purgePost'), 99);
    add_action('deleted_post', array(&$this, 'purgeCommon'), 99);
    
    // Comments
    add_action('comment_post', array(&$this, 'purgeComments'),99);
    add_action('edit_comment', array(&$this, 'purgeComments'),99);
    add_action('trashed_comment', array(&$this, 'purgeComments'),99);
    add_action('untrashed_comment', array(&$this, 'purgeComments'),99);
    add_action('deleted_comment', array(&$this, 'purgeComments'),99);
    
    // Full purges (theme changes, etc.)
    add_action('switch_theme', array(&$this, 'purgeAll'), 99);
    add_action('update_option_sidebars_widgets', array(&$this, 'purgeAll'), 99);
    add_action('widgets.php', array(&$this, 'purgeAll'), 99);
    add_action("update_option_theme_mods_".get_option('stylesheet'), array(&$this, 'purgeAll'), 99);
    
    // Links
    add_action("deleted_link",array(&$this, 'purgeLinks'), 99);
    add_action("edit_link",array(&$this, 'purgeLinks'), 99);
    add_action("add_link",array(&$this, 'purgeLinks'), 99);
    
    // Categories
    add_action("edit_category",array(&$this, 'purgeCategory'), 99);       
    add_action("edit_link_category",array(&$this, 'purgeLinkCategory'), 99);
    add_action("edit_post_tag",array(&$this, 'purgeTagCategory'), 99);
    
    // Setup API
    $this->api = new FastlyAPI(
      get_option('fastly_api_key'), 
      get_option('fastly_api_hostname'),
      get_option('fastly_api_port')
    );
  }
  
  /**
   * Sends a purge request for the given url.
   * @param $url URL to purge from the cache server.
   */
  function purge($urls) {
    $this->api->purge($urls);   
  }
  
  /**
   * Purges all pages on the site.
   */
  function purgeAll() {
    $this->api->purgeAll( get_option('fastly_service_id') );
  }
  
  /**
   * Purges common pages.
   */
  function purgeCommon($call=true) {
    $urls = array(get_bloginfo('wpurl')."/", get_bloginfo('wpurl')."/feed", get_bloginfo('wpurl')."/feed/atom");

    // TODO Need Regex Support
    /*
      if (get_site_option($this->wpv_update_pagenavi_optname) == 1) {
         $this->WPVarnishPurgerPurgeObject("/page/(.*)");
      }
    */
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges posts and pages on update.
   */
  function purgePost($postId, $call=true) {
    $urls = array( get_permalink($postId) );
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Purges objects that depend on the post.
   */
  function purgePostDependencies($postId, $call=true) { 
    $urls = array();
    $urls = array_merge($urls, $this->purgeCommon(false));
    $urls = array_merge($urls, $this->purgeCategories($postId, false));
    $urls = array_merge($urls, $this->purgeArchives($postId, false));
    $urls = array_merge($urls, $this->purgeTags($postId, false));
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges categories associated with a post.
   * @param $postId Id of the post.
   */
  function purgeCategories($postId, $call=true) {
    $urls       = array();
    $categories = get_the_category($postId);
    foreach ($categories as $cat) {
      $urls = array_merge($urls, $this->purgeCategory($cat->cat_ID, false));
    }
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges post comments.
   */
  function purgeComments($commentId, $call=true) {
    $comment  = get_comment($commentId);
    $approved = $comment->comment_approved;
    $urls     = array();

    if ($approved == 1 || $approved == 'trash') {
      $postId = $comment->comment_post_ID;
      #$this->purge('/\\\?comments_popup=' . $postId);
      $urls[] = get_bloginfo('wpurl') . '/?comments_popup=' . $postId;
      
      // TODO Need Regex Support
      /*
      if (get_site_option($this->wpv_update_commentnavi_optname) == 1) {
        $this->purge('/\\\?comments_popup=' . $postId . '&(.*)');
      }
      */
    }
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges links.
   */
  function purgeLinks() {
    if (is_active_widget(false, false, 'links')) {
        $this->purgeAll();
    }
  }
  
  /**
   * Purges post categories.
   * @param $categoryId Id of the category to purge.
   */
  function purgeCategory($categoryId, $call=true) {
    $urls = array();
    if (is_active_widget(false, false, 'categories')) {
      $this->purgeAll();
    }
    else {
      $urls[] = get_category_link($categoryId);
    }
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Purges link categories.
   * @param $categoryId Id of the category to purge.
   */
  function purgeLinkCategory($categoryId) {
    if (is_active_widget(false,false,'links')){
      $this->purgeAll();
    }
  }

  /**
   * Purges a tag category.
   * @param $categoryId Id of the category to purge.
   */
  function purgeTagCategory($categoryId, $call=true) {
    $urls = array( get_tag_link($categoryId) );
    return $call ? $this->purge($urls) : $urls;
  }
  
  /**
   * Purges archives pages.
   * @param $postId Id of the post that triggered the purge.
   */
  function purgeArchives($postId, $call=true) {
    $urls = array(
      get_day_link(get_post_time('Y',false,$postId), get_post_time('m',true,$postId),get_post_time('d',true,$postId)),
      get_month_link(get_post_time('Y',false,$postId), get_post_time('m',true,$postId)),
      get_year_link(get_post_time('Y',false,$postId)),
    );

    return $call ? $this->purge($urls) : $urls;
  }  

  /**
   * Purges tags associated with a post.
   * @param $postId Id of the post being purged.
   */
  function purgeTags($postId, $call=true) {
    $urls = array();
    $tags = wp_get_post_tags($postId);
    foreach ($tags as $tag) {
      $urls = array_merge($urls, $this->purgeTagCategory($tag->term_id, false));
    }
    return $call ? $this->purge($urls) : $urls;
  }

  /**
   * Handles post status purges.
   */
  function purgePostStatus($new_status, $old_status, $post, $call=true) {
    $urls = array();
    $urls = array_merge($urls, $this->purgePost($post->ID, false));
    $urls = array_merge($urls, $this->purgePostDependencies($post->ID, false));
    return $call ? $this->purge($urls) : $urls;
  }
}

// "While mona lisas and mad hatters, sons of bankers, sons of lawyers turn around and say good morning to the night..." -- Elton John

/*
 * Fastly Wordpress Plugin
 * Admin Interface Styles
 * @author Ryan Sandor Richards
 */
#fastly-admin h1 {
  float: left;
}

#fastly-admin .content {
  padding: 10px 0 0 20px;
  margin: 10px 0 0 200px;
}

#fastly-admin .content .button-row {
  margin: 20px 0;
  line-height: 20px;
}

#fastly-admin .error {
  background-color: #faa;
  padding: 4px;
}

#fastly-admin .error-flash {
  color: red;
  font-weight: bold;
}

#fastly-admin a.disabled {
  cursor: default !important;
}

#fastly-admin .loading {
  display: none;
  margin-bottom: -3px;
  margin-left: 4px;
}

#fastly-admin a.advanced {
  font-size: 9px;
}

#fastly-admin fieldset {
  -moz-border-radius: 6px;
  border-radius: 6px;
  background: #efefef;
  padding: 6px;
  width: 500px;
}

#fastly-admin fieldset.advanced {
  display: none;
}

#fastly-admin input.text {
  font-size: 18px;
  padding: 4px;
  width: 300px;
}

#fastly-admin fieldset input.text {
  width: 99%;
}

/*
 * Fastly Wordpress Plugin
 * Admin Interface Scripting
 * @author Ryan Sandor Richards
 */
window.Fastly = (function($) {
  var content, page, templates;

  /**
   * Validation information (emancipation proclaimation)
   */
  var validation = {
    '#email': {
      regex: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
      message: 'Please provide a valid email address.'
    },
    '#name': {
      message: 'Please enter your name.'
    },
    '#agree_tos': {
      checkbox: 'checked',
      message: 'Please agree to the Fastly terms of service.'
    },
    '#customer': {
      message: 'Please enter a customer (blog) name.'
    },
    '#website_address': {
      regex: /^http:\/\/.+$/,
      message: 'Please enter a valid website address'
    },
    '#address': {
      regex: /(\d+\.)+/,
      message: 'Please provide a valid IP address.'
    }
  }

  /**
   * UI Event Handlers
   */
  var events = {
    /**
     * Welcome page "existing account" button.
     */
    'click .welcome .configure': function(e) {
      loadPage('configure');
      $.ajax({
        url: ajaxurl, 
        data: {
          action: 'set_page',
          page: 'configure',
          //fastlyNonce provided by wp_localize_script() in admin.php
          nonce: fastlyNonce
        }
      });
    },
    
    /**
     * Signup Form Submit.
     */
    'click .signup .submit': function(e) {
      var email = $('#email'),
        agree_tos = $('#agree_tos'),
        name = $('#name'),
        customer = $('#customer'),
        address = $('#address'),
        website_address = $('#website_address'),
        flash = $('#fastly-admin .error-flash'),
        messages = [];
      
      flash.html('');
      $('#email, #agree_tos_label').removeClass('error');

      // Perform validation checks
      for (var sel in validation) {
        var element = $(sel),
          value = $.trim( element.val() ),
          rules = validation[sel];
        
        if (typeof rules.regex != "undefined") {
          if (!value.match(rules.regex)) {
            messages.push(rules.message);
          }
        }
        else if (typeof rules.checkbox != "undefined") {
          if (typeof element.attr('checked') == "undefined" || element.attr('checked') == null) {
            messages.push(rules.message);
          }
        }
        else if (!value) {
          messages.push(rules.message);
        }
      }

      if (messages.length > 0) {
        flash.html( messages.join('<br>') );
        return;
      }
      
      // Send the sign up request
      function disable() {
        $(e.target).addClass('disabled');
        $('fieldset input').attr('disabled', true);
        $('.loading').show();
      }
      
      function enable() {
        $(e.target).removeClass('disabled');
        $('fieldset input').attr('disabled', false);
        $('.loading').hide();
      }
      
      disable();
      
      $.ajax({
        url: ajaxurl,
        data: {
          action: 'sign_up',
          customer: customer.val(),
          name: name.val(),
          email: email.val(),
          address: address.val(),
          website_address: website_address.val(),
          //fastlyNonce provided by wp_localize_script() in admin.php
          nonce: fastlyNonce
        },
        dataType: 'json',
        success: function(response) {
          if (response.status == "success") {
            window.location.reload();
          }
          else {
            flash.html(response.msg);
            enable();
          }
        },
        error: function() {
          flash.html("An error occurred while connecting to the fastly API, please try your request again.");
          enable();
        }
      });
    },
    
    /**
     * Toggle advanced settings.
     */
    'click .configure a.advanced': function(e) {
      $('fieldset.advanced').toggle(250);
    }
  };
  
  /**
   * Initalizes events for each of the pages.
   */
  function initEvents() {
    for (var k in events) {
      var parts = k.match(/([^\s]+)\s+(.*)/),
        name = parts[1],
        selector = parts[2];
        
      // Trixy hobittses...
      $(selector).live(name, (function(handler) {
        return function(e) {
          if ($(e.target).hasClass('disabled') && $(e.target).hasClass('button')) {
            e.stopPropagation();
            e.preventDefault();
            return;
          }
          return handler(e);
        };
      })(events[k]));
    }
  }
  
  /**
   * Loads the appropriate page given a name.
   */
  function loadPage(name) {
    if (typeof templates[name] == "undefined" || !templates[name]) {
      throw "Fastly.loadPage(): Undefined template '" + name + "'";
    }
    content.html( templates[name] );
  }
  
  
  // Set content element
  $(function() { content = $('#fastly-admin .content'); });
  
  /**
   * Public interface.
   */
  return {
    /**
     * Initializes the fastly pluging configuration page.
     * @param p Current page the user is on.
     * @param t A string containing the JSON representation of the page templates object.
     */
    init: function(p, t) {
      page = p;
      templates = t;
      initEvents();
    },
    
    loadPage: loadPage
  };
})(jQuery)

// We're the last living souls...