diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e9530261221d1863318d1f0574f6592b8aae4001..3a8f6cb080013e40fd61279e6be21b24b08fbb39 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,42 +24,29 @@ before_script: - composer global require automattic/vipwpcs - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs -PHPunit:PHP5.3:MySQL: - image: containers.ethitter.com:443/docker/images/php:5.3 - services: - - mysql:5.6 - script: - - phpcs -n - - phpunit - -PHPunit:PHP5.6:MySQL: - image: containers.ethitter.com:443/docker/images/php:5.6 - services: - - mysql:5.6 - script: - - phpcs -n - - phpunit - -PHPunit:PHP7.0:MySQL: +test_7.0: image: containers.ethitter.com:443/docker/images/php:7.0 services: - mysql:5.6 script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l - phpcs -n - phpunit -PHPunit:PHP7.1:MySQL: +test_7.1: image: containers.ethitter.com:443/docker/images/php:7.1 services: - mysql:5.6 script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l - phpcs -n - phpunit -PHPunit:PHP7.2:MySQL: +test_7.2: image: containers.ethitter.com:443/docker/images/php:7.2 services: - mysql:5.6 script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l - phpcs -n - phpunit diff --git a/README.md b/README.md index c19e0354724a3aa3f46a3a19d7256abb68012ef7..fa835b3b4d9705de32fcf155e1a12d81ee2ee2c8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ **Donate link:** https://ethitter.com/donate/ **Tags:** image, proxy, cdn **Requires at least:** 4.9 +**Requires PHP:** 7.0 **Tested up to:** 4.9 **Stable tag:** 0.1.0 **License:** GPLv2 or later diff --git a/camo-image-proxy.php b/camo-image-proxy.php index 1c407ca4b6eaf7dc68e0ad0c029719869946afbd..9ed3d6e82970288e689fb4688c5ea76568ee3433 100755 --- a/camo-image-proxy.php +++ b/camo-image-proxy.php @@ -13,3 +13,58 @@ */ namespace Camo_Image_Proxy; + +const PLUGIN_PATH = __DIR__; + +/** + * Trait for singletons + */ +require_once PLUGIN_PATH . '/inc/trait-singleton.php'; + +/** + * Plugin options + */ +require_once PLUGIN_PATH . '/inc/class-options.php'; + +/** + * Options page + */ +require_once PLUGIN_PATH . '/inc/class-options-page.php'; + +/** + * URL Building + */ +require_once PLUGIN_PATH . '/inc/class-url.php'; + +/** + * Rewrite WordPress-generated URLs + */ +require_once PLUGIN_PATH . '/inc/class-rewrite-urls.php'; + +/** + * Rewrite URLs in post content + */ +require_once PLUGIN_PATH . '/inc/class-rewrite-content.php'; + +/** + * Assorted functions + */ +require_once PLUGIN_PATH . '/inc/functions.php'; + +/** + * Load plugin singletons + */ +function init() { + Options::instance(); + URL::instance(); + + if ( is_admin() ) { + Options_Page::instance(); + } + + if ( URL::instance()->can_rewrite() ) { + Rewrite_URLs::instance(); + Rewrite_Content::instance(); + } +} +add_action( 'init', __NAMESPACE__ . '\init' ); diff --git a/inc/class-options-page.php b/inc/class-options-page.php new file mode 100644 index 0000000000000000000000000000000000000000..2e9540697b0a2be04cf06dbc1d68c149581a28cf --- /dev/null +++ b/inc/class-options-page.php @@ -0,0 +1,86 @@ +<?php +/** + * Plugin options page + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Class Options_Page + */ +class Options_Page { + use Singleton; + + /** + * Settings screen section + * + * @var string + */ + private $section = 'camp-image-proxy'; + + /** + * Field labels + * + * @var array + */ + private $labels = []; + + /** + * Option name + * + * @var string + */ + private $name; + + /** + * Hooks + */ + public function setup() { + $this->name = Options::instance()->name; + + $this->labels['host'] = __( 'Host', 'camo-image-proxy' ); + $this->labels['key'] = __( 'Shared Key', 'camo-image-proxy' ); + + add_action( 'admin_init', [ $this, 'action_admin_init' ] ); + } + + /** + * Add fields to Media settings page + */ + public function action_admin_init() { + register_setting( 'media', $this->name, [ Options::instance(), 'sanitize_all' ] ); + add_settings_section( $this->section, __( 'Camo Image Proxy', 'camo-image-proxy' ), '__return_false', 'media' ); + + foreach ( $this->labels as $key => $label ) { + $args = [ + 'option' => $key, + 'label' => $label, + ]; + add_settings_field( $key, $label, [ $this, 'screen' ], 'media', $this->section, $args ); + } + } + + /** + * Render options field + * + * @param array $args Field arguments. + */ + public function screen( $args ) { + $value = Options::instance()->get( $args['option'] ); + $input_type = 'host' === $args['option'] ? 'url' : 'text'; + $name = sprintf( '%1$s[%2$s]', $this->name, $args['option'] ); + $html_id = sprintf( '%1$s-%2$s', str_replace( '_', '-', $this->name ), $args['option'] ); + + ?> + <input + type="<?php echo esc_attr( $input_type ); ?>" + name="<?php echo esc_attr( $name ); ?>" + class="regular-text" + id="<?php echo esc_attr( $html_id ); ?>" + value="<?php echo esc_attr( $value ); ?>" + /> + <?php + } +} diff --git a/inc/class-options.php b/inc/class-options.php new file mode 100644 index 0000000000000000000000000000000000000000..888477643126d055e138888218cc3f497ffbf446 --- /dev/null +++ b/inc/class-options.php @@ -0,0 +1,132 @@ +<?php +/** + * Plugin options + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Class Options + */ +class Options { + use Singleton; + + /** + * Option name + * + * @var string + */ + private $name = 'camo_image_proxy_opts'; + + /** + * Allowed options + * + * @var array + */ + private $allowed_options = [ + 'host' => '', + 'key' => '', + ]; + + /** + * Access certain private properties + * + * @param string $name Property name. + * @return mixed + */ + public function __get( string $name ) { + switch ( $name ) { + case 'name': + return $this->name; + + default: + return new \WP_Error( 'invalid-property', __( 'Invalid property requested.', 'camo-image-proxy' ), $name ); + } + } + + /** + * Retrieve full plugin options + * + * @return array + */ + private function get_all() : array { + $options = get_option( $this->name, [] ); + $options = wp_parse_args( $options, $this->allowed_options ); + return $options; + } + + /** + * Get plugin option + * + * @param string $option Plugin option to retrieve. + * @return mixed + */ + public function get( string $option ) { + if ( ! array_key_exists( $option, $this->allowed_options ) ) { + return false; + } + + $options = $this->get_all(); + return $options[ $option ] ?? false; + } + + /** + * Set plugin option + * + * @param string $option Plugin option to set. + * @param mixed $value Option value. + * @return bool + */ + public function set( string $option, $value ) : bool { + $value = $this->sanitize( $option, $value ); + $options = $this->get_all(); + $options[ $option ] = $value; + + return update_option( $this->name, $options ); + } + + /** + * Sanitize option + * + * @param string $option Plugin option. + * @param mixed $value Option value to sanitize. + * @return mixed + */ + public function sanitize( string $option, $value ) { + switch ( $option ) { + case 'host': + $value = esc_url( $value ); + + if ( ! empty( $value ) ) { + $value = untrailingslashit( $value ); + } + + break; + + case 'key': + $value = sanitize_text_field( $value ); + break; + + default: + return false; + } + + return $value; + } + + /** + * Sanitize array of options + * + * @param array $options Options to sanitize. + * @return array + */ + public function sanitize_all( array $options ) : array { + foreach ( $options as $option => $value ) { + $options[ $option ] = $this->sanitize( $option, $value ); + } + + return $options; + } +} diff --git a/inc/class-rewrite-content.php b/inc/class-rewrite-content.php new file mode 100644 index 0000000000000000000000000000000000000000..ccf31a0a317a083ed6eeedb47785d6f1fd33fb78 --- /dev/null +++ b/inc/class-rewrite-content.php @@ -0,0 +1,43 @@ +<?php +/** + * Rewrite images in content + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Class Rewrite_Content + */ +class Rewrite_Content { + use Singleton; + + /** + * Filter priority + * + * @var int + */ + private $priority; + + /** + * Hooks + */ + public function setup() { + $priority = apply_filters( 'camo_image_proxy_rewrite_content_priority', PHP_INT_MAX - 1 ); + $this->priority = absint( $priority ); + + add_filter( 'the_content', [ $this, 'filter_the_content' ], $this->priority ); + } + + /** + * Rewrite image URLs in content + * + * @param string $content Post content. + * @return string + */ + public function filter_the_content( string $content ) : string { + // TODO: only deal with image srcs, use DOM Document. + return $content; + } +} diff --git a/inc/class-rewrite-urls.php b/inc/class-rewrite-urls.php new file mode 100644 index 0000000000000000000000000000000000000000..cd68b690bb0f07dc10f95e4201f315547181bfe2 --- /dev/null +++ b/inc/class-rewrite-urls.php @@ -0,0 +1,38 @@ +<?php +/** + * Force Core's image functions to use Camo + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Class Rewrite_URLs + */ +class Rewrite_URLs { + use Singleton; + + /** + * Hooks + */ + public function setup() { + add_filter( 'wp_get_attachment_image_src', [ $this, 'encode_image' ] ); + } + + /** + * Camouflage attachment URL + * + * @param array $image Image data. + * @return array + */ + public function encode_image( array $image ) : array { + $url = URL::instance()->encode( $image[0] ); + + if ( is_string( $url ) ) { + $image[0] = $url; + } + + return $image; + } +} diff --git a/inc/class-url.php b/inc/class-url.php new file mode 100644 index 0000000000000000000000000000000000000000..33b4496143841e0b94eb78ba4d2b63035651c2bf --- /dev/null +++ b/inc/class-url.php @@ -0,0 +1,93 @@ +<?php +/** + * URL Building + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Class URL + */ +class URL { + use Singleton; + + /** + * Can URLs be rewritten to use Camo? + * + * @return bool + */ + public function can_rewrite() : bool { + // Never rewrite in admin. + if ( is_admin() ) { + return false; + } + + $host = Options::instance()->get( 'host' ); + $key = Options::instance()->get( 'key' ); + + $can_rewrite = true; + + // Validate host. + if ( ! $this->is_valid_url( $host ) ) { + $can_rewrite = false; + } + + // Validate key. + // TODO: make sure it's an HMAC or something? + if ( empty( $key ) || ! is_string( $key ) ) { + $can_rewrite = false; + } + + return apply_filters( 'camo_image_proxy_can_rewrite', $can_rewrite, $host, $key ); + } + + /** + * Encode image URL + * + * @param string $url Image URL to encode. + * @return string|bool + */ + public function encode( string $url ) : string { + if ( ! $this->can_rewrite() || ! $this->is_valid_url( $url ) ) { + return false; + } + + $key = hash_hmac( 'sha1', $url, Options::instance()->get( 'key' ) ); + $url_encoded = bin2hex( $url ); + + $url_encoded = sprintf( '%1$s/%2$s/%3$s', Options::instance()->get( 'host' ), $key, $url_encoded ); + $url_encoded = set_url_scheme( $url_encoded, 'https' ); + + return $url_encoded; + } + + /** + * Decode encoded URL + * + * @param string $url Camo URL to decode. + * @return string|bool + */ + public function decode( string $url ) : string { + return false; + } + + /** + * Can we encode this URL? + * + * @param string $url URL to validate. + * @return bool + */ + private function is_valid_url( string $url ) : bool { + if ( empty( $url ) ) { + return false; + } + + if ( false === filter_var( $url, FILTER_VALIDATE_URL ) && false === filter_var( $url, FILTER_VALIDATE_IP ) ) { + return false; + } + + return true; + } +} diff --git a/inc/functions.php b/inc/functions.php new file mode 100644 index 0000000000000000000000000000000000000000..9fce3b24cf1505bbec4cf71f004f19bcb5f9fbc2 --- /dev/null +++ b/inc/functions.php @@ -0,0 +1,8 @@ +<?php +/** + * Assorted helpers + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; diff --git a/inc/trait-singleton.php b/inc/trait-singleton.php new file mode 100644 index 0000000000000000000000000000000000000000..b890d8dea40792bc78dbf38c352bc4eaf030840d --- /dev/null +++ b/inc/trait-singleton.php @@ -0,0 +1,40 @@ +<?php +/** + * Trait file for Singletons. + * + * @package Camo_Image_Proxy + */ + +namespace Camo_Image_Proxy; + +/** + * Make a class into a singleton. + */ +trait Singleton { + /** + * Existing instance. + * + * @var object + */ + protected static $instance; + + /** + * Get class instance. + * + * @return object + */ + public static function instance() { + if ( ! isset( static::$instance ) ) { + static::$instance = new static(); + static::$instance->setup(); + } + return static::$instance; + } + + /** + * Setup the singleton. + */ + public function setup() { + // Silence. + } +} diff --git a/languages/camo-image-proxy.pot b/languages/camo-image-proxy.pot index 41db33f1d396c42e2424d5947384583cff1abf12..85a017c1f7a04dbb132fd2d8fe6faadf6df288c9 100644 --- a/languages/camo-image-proxy.pot +++ b/languages/camo-image-proxy.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Camo Image Proxy 0.1.0\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/camo-image-proxy\n" -"POT-Creation-Date: 2018-02-18 19:07:54+00:00\n" +"POT-Creation-Date: 2018-02-19 01:54:23+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -24,10 +24,22 @@ msgstr "" "X-Poedit-Bookmarks: \n" "X-Textdomain-Support: yes\n" +#: inc/class-options-page.php:43 +msgid "Host" +msgstr "" + +#: inc/class-options-page.php:44 +msgid "Shared Key" +msgstr "" + #. Plugin Name of the plugin/theme msgid "Camo Image Proxy" msgstr "" +#: inc/class-options.php:45 +msgid "Invalid property requested." +msgstr "" + #. Plugin URI of the plugin/theme msgid "https://ethitter.com/plugins/" msgstr "" diff --git a/readme.txt b/readme.txt index 62370f4c382a6d0ebcb33b5ed03f44e478780698..877751af577415030358442c66a82c49dfa14ed7 100755 --- a/readme.txt +++ b/readme.txt @@ -3,6 +3,7 @@ Contributors: ethitter Donate link: https://ethitter.com/donate/ Tags: image, proxy, cdn Requires at least: 4.9 +Requires PHP: 7.0 Tested up to: 4.9 Stable tag: 0.1.0 License: GPLv2 or later