commit f9f65dc84df2e0a9e3bfe3e53595e16119db6e06 Author: Alex Rennie-Lis Date: Mon Nov 2 11:04:39 2026 +0000 Initial commit diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..11e08fc --- /dev/null +++ b/css/admin.css @@ -0,0 +1,64 @@ +#sistContainer { + margin-right: 310px; +} + +#sistContent { + width: 100%; + float: left; + min-width: 600px; +} + +#sistSidebar { + float: right; + margin-right: -310px; + width: 290px; +} + +/* Generate page */ + +.export-log { + height: 200px; + overflow: auto; + border: 1px solid #DFDFDF; + color: #000; + background: #fff; + width: 100%; +} + +.export-log pre { + margin: 0; + padding: 3px 4px; +} + +/* Options page */ + +.tab-pane, tr.delivery-method { + display: none; +} + +.tab-pane.active, tr.delivery-method.active { + display: table-row; +} + +select:disabled, select.disabled { + border-color: rgba(222,222,222,.75); + background: rgba(255,255,255,.5); + color: rgba(51,51,51,.5); +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +/* Mailchimp Form CSS */ + +/* Mailchimp original CSS at: + http://cdn-images.mailchimp.com/embedcode/slim-081711.css +*/ +#mc_embed_signup input {border:1px solid #999; -webkit-appearance:none;} +#mc_embed_signup input:focus {border-color:#333;} +#mc_embed_signup .clear {clear:none; display:inline;} +#mc_embed_signup input.email {display:block; padding:8px 0; margin:0 4% 10px 0; text-indent:5px; width:58%; min-width:130px; width:100%;} diff --git a/includes/class-simply-static-archive-creator.php b/includes/class-simply-static-archive-creator.php new file mode 100644 index 0000000..96105a3 --- /dev/null +++ b/includes/class-simply-static-archive-creator.php @@ -0,0 +1,355 @@ +slug = $slug; + $this->destination_scheme = $destination_scheme; + $this->destination_host = $destination_host; + $this->temp_files_dir = $temp_files_dir; + $this->additional_urls = $additional_urls; + } + + /** + * Get the list of URLs in the archive + * + * @param array $export_log URLs that were successfully added to the archive + */ + public function get_export_log() { + return $this->export_log; + } + + /** + * Create a static version of the site + * + * @return void + */ + public function create_archive() { + global $blog_id; + // TODO: Do ajax calls instead of just running forever and ever + set_time_limit(0); + + // Create archive directory + $current_user = wp_get_current_user(); + $archive_name = join( '-', array( $this->slug, $blog_id, time(), $current_user->user_login ) ); + $this->archive_dir = trailingslashit( $this->temp_files_dir . $archive_name ); + + if ( ! file_exists( $this->archive_dir ) ) { + wp_mkdir_p( $this->archive_dir ); + } + + // Add URLs to queue + $origin_url = home_url(); + $destination_url = $this->destination_scheme . '://' . $this->destination_host; + $urls_queue = array_unique( array_merge( + array( trailingslashit( $origin_url ) ), + // using preg_split to intelligently break at newlines + // see: http://stackoverflow.com/questions/1483497/how-to-put-string-in-array-split-by-new-line + preg_split( "/\r\n|\n|\r/", $this->additional_urls ) + ) ); + + while ( count( $urls_queue ) ) { + $current_url = array_shift( $urls_queue ); + + $response = Simply_Static_Url_Fetcher::fetch( $current_url ); + + // If we get a WP_Error then somehow our request failed (e.g. space in URL) + // TODO: Keep a queue of failed urls too + if ( is_wp_error( $response ) ) { + continue; + } + + $url_parts = parse_url( $response->url ); + $path = $url_parts['path']; + $is_html = $response->is_html(); + + // If we get a 30x redirect... + if ( in_array( $response->code, array( 301, 302, 303, 307 ) ) ) { + + $redirect_url = $response->get_redirect_url(); + + // WP likes to 301 redirect `/path` to `/path/` -- we want to + // check for this and just add the trailing slashed version + if ( $redirect_url === trailingslashit( $current_url ) ) { + + $urls_queue = $this->add_url_to_queue( $urls_queue, $redirect_url ); + + } else { + + /// convert our potentially relative URL to an absolute URL + $redirect_url = sist_relative_to_absolute_url( $redirect_url, $current_url ); + + if ( $redirect_url ) { + + // check if this is a local URL + if ( sist_is_local_url( $redirect_url ) ) { + + // add the redirected page to the queue + $urls_queue = $this->add_url_to_queue( $urls_queue, $redirect_url ); + // and update the URL + $redirect_url = str_replace( $origin_url, $destination_url, $redirect_url ); + + } + + $view = new Simply_Static_View(); + + $content = $view->set_template( 'redirect' ) + ->assign( 'redirect_url', $redirect_url ) + ->render_to_string(); + + $this->save_url_to_file( $path, $content, $is_html ); + + $this->export_log[] = $current_url; + + } + } + + continue; + } + + // Not a 200 for the response code? Move on. + // TODO: Keep a queue of failed urls too + if ( $response->code != 200 ) { + continue; + } + + $this->export_log[] = $current_url; + + // Fetch all URLs from the page and add them to the queue... + $urls = $response->extract_urls(); + foreach ( $urls as $url ) { + $urls_queue = $this->add_url_to_queue( $urls_queue, $url ); + } + + // Replace the origin URL with the destination URL + $response->replace_url( $origin_url, $destination_url ); + + // Save the page to our archive + $content = $response->body; + $this->save_url_to_file( $path, $content, $is_html ); + } + } + + /** + * Add a URL to the processing queue if we haven't already processed it and + * if it's not in the queue to be processed + * + * @param array $urls_queue Queue of URLs to be processed + * @param string $url URL to add to the processing queue + * @return array Queue of URLs to be processed + */ + private function add_url_to_queue( $urls_queue, $url ) { + if ( ! in_array( $url, $this->export_log ) && ! in_array( $url, $urls_queue ) ) { + $urls_queue[] = $url; + } + + return $urls_queue; + } + + /** + * Create a ZIP file using the archive directory + * + * @return string|WP_Error $temporary_zip The path to the archive zip file + */ + public function create_zip() { + $temporary_zip = untrailingslashit( $this->archive_dir ) . '.tmp'; + $zip_archive = new ZipArchive(); + + if ( $zip_archive->open( $temporary_zip, ZIPARCHIVE::CREATE ) !== true ) { + return new WP_Error( 'create_zip_failed', __( 'Unable to create ZIP archive', $this->slug ) ); + } + + $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $this->archive_dir ) ); + foreach ( $iterator as $file_name => $file_object ) { + $base_name = basename( $file_name ); + + if ( $base_name != '.' && $base_name != '..' ) { + if ( ! $zip_archive->addFile( realpath( $file_name ), str_replace( $this->archive_dir, '', $file_name ) ) ) { + return new WP_Error( 'cannot_add_file_to_zip', sprintf( __( "Could not add file: %s", $this->slug ), $file_name ) ); + } + } + } + + $zip_archive->close(); + $zip_file = untrailingslashit( $this->archive_dir ) . '.zip'; + rename( $temporary_zip, $zip_file ); + + $archive_url = str_replace( plugin_dir_path( dirname( __FILE__ ) ), plugin_dir_url( dirname( __FILE__ ) ), $zip_file ); + + return $archive_url; + } + + /** + * Publish to AWS S3 bucket + * + * @return boolean|WP_Error + */ + public function publish_to_s3( $bucket, $aws_access_key_id, $aws_secret_access_key, $aws_s3_region = 'us-east-1', $aws_s3_endpoint = 's3.amazonaws.com' ) { + $directory_iterator = new RecursiveDirectoryIterator( $this->archive_dir, RecursiveDirectoryIterator::SKIP_DOTS ); + $recursive_iterator = new RecursiveIteratorIterator( $directory_iterator, RecursiveIteratorIterator::SELF_FIRST ); + + S3::$useExceptions = true; + $useSSL = true; + $endpoint = $aws_s3_endpoint; + if ( preg_match( '#^https://#i', $endpoint ) ) { + $useSSL = true; + $endpoint = preg_replace( '#^https://#i', '', $endpoint ); + } elseif ( preg_match( '#^http://#i', $endpoint ) ) { + $useSSL = false; + $endpoint = preg_replace( '#^http://#i', '', $endpoint ); + } + $endpoint = untrailingslashit( $endpoint ); + if ( empty( $endpoint ) ) { + $endpoint = 's3.amazonaws.com'; + } + + $s3 = new S3( $aws_access_key_id, $aws_secret_access_key, $useSSL, $endpoint, $aws_s3_region ); + S3::setRegion( $aws_s3_region ); + + foreach ( $recursive_iterator as $item ) { + if (!$item->isDir()) { + $path = $recursive_iterator->getSubPathName(); + try { + $s3->putObject( S3::inputFile( $item->getRealPath() ), $bucket, $path, S3::ACL_PUBLIC_READ ); + } catch (any $err) { + return new WP_Error( 'cannot_publish_to_s3', sprintf( __( "Could not publish file to S3: %s: %s", $this->slug, $err ), $path ) ); + } + } + } + return true; + } + + /** + * Copy static files to a local directory + * + * @return boolean|WP_Error + */ + public function copy_static_files( $local_dir ) { + $directory_iterator = new RecursiveDirectoryIterator( $this->archive_dir, RecursiveDirectoryIterator::SKIP_DOTS ); + $recursive_iterator = new RecursiveIteratorIterator( $directory_iterator, RecursiveIteratorIterator::SELF_FIRST ); + + foreach ( $recursive_iterator as $item ) { + $path = $local_dir . $recursive_iterator->getSubPathName(); + $success = $item->isDir() ? wp_mkdir_p( $path ) : copy( $item, $path ); + if ( ! $success ) { + return new WP_Error( 'cannot_create_file_or_dir', sprintf( __( "Could not create file or directory: %s", $this->slug ), $path ) ); + } + } + + return true; + } + + /** + * Delete generated static files + * + * @param $archive_dir The archive directory path + * @return boolean|WP_Error + */ + public function delete_static_files() { + $directory_iterator = new RecursiveDirectoryIterator( $this->archive_dir, FilesystemIterator::SKIP_DOTS ); + $recursive_iterator = new RecursiveIteratorIterator( $directory_iterator, RecursiveIteratorIterator::CHILD_FIRST ); + + // recurse through the entire directory and delete all files / subdirectories + foreach ( $recursive_iterator as $item ) { + $success = $item->isDir() ? rmdir( $item ) : unlink( $item ); + if ( ! $success ) { + return new WP_Error( 'cannot_delete_file_or_dir', sprintf( __( "Could not delete temporary file or directory: %s", $this->slug ), $item ) ); + } + } + + // must make sure to delete the original directory at the end + $success = rmdir( $this->archive_dir ); + if ( ! $success ) { + return new WP_Error( 'cannot_delete_file_or_dir', sprintf( __( "Could not delete temporary file or directory: %s", $this->slug ), $item ) ); + } + + return true; + } + + /** + * Save the contents of a page to a file in our archive directory + * + * @param string $path The relative path for the URL to save + * @param string $content The contents of the page we want to save + * @param boolean $is_html Is this an html page? + * @return boolean $success Did we successfully save the file? + */ + protected function save_url_to_file( $path, $content, $is_html ) { + $path_info = pathinfo( $path && $path != '/' ? $path : 'index.html' ); + + // Create file directory if it doesn't exist + $file_dir = $this->archive_dir . ( $path_info['dirname'] ? $path_info['dirname'] : '' ); + if ( empty( $path_info['extension'] ) && $path_info['basename'] == $path_info['filename'] ) { + $file_dir .= '/' . $path_info['basename']; + $path_info['filename'] = 'index'; + } + if ( ! file_exists( $file_dir ) ) { + wp_mkdir_p( $file_dir ); + } + + // Save file contents + $file_extension = ( $is_html || ! isset( $path_info['extension'] ) ) ? 'html' : $path_info['extension']; + $file_name = $file_dir . '/' . $path_info['filename'] . '.' . $file_extension; + $success = file_put_contents( $file_name, $content ); + return $success; + } +} diff --git a/includes/class-simply-static-options.php b/includes/class-simply-static-options.php new file mode 100644 index 0000000..342f96f --- /dev/null +++ b/includes/class-simply-static-options.php @@ -0,0 +1,69 @@ +options = $options; + $this->option_key = $option_key; + } + + /** + * Updates the option identified by $name with the value provided in $value + * + * @param string $name The option name + * @param mixed $value The option value + * @return Simply_Static_Options + */ + public function set( $name, $value ) { + $this->options[$name] = $value; + return $this; + } + + /** + * Returns a value of the option identified by $name + * + * @param string $name The option name + * @return mixed|null + */ + public function get( $name ) { + return array_key_exists( $name, $this->options ) ? $this->options[$name] : null; + } + + /** + * Saves the internal options data to the wp_options table using the stored $option_key value as the key + * + * @return boolean + */ + public function save() { + return update_option( $this->option_key, $this->options ); + } +} diff --git a/includes/class-simply-static-url-extractor.php b/includes/class-simply-static-url-extractor.php new file mode 100644 index 0000000..ea0d6a9 --- /dev/null +++ b/includes/class-simply-static-url-extractor.php @@ -0,0 +1,211 @@ + array( 'href', 'urn' ), + 'base' => array( 'href' ), + 'form' => array( 'action', 'data' ), + 'img' => array( 'src', 'usemap', 'longdesc', 'dynsrc', 'lowsrc' ), + 'link' => array( 'href' ), + + 'applet' => array( 'code', 'codebase', 'archive', 'object' ), + 'area' => array( 'href' ), + 'body' => array( 'background', 'credits', 'instructions', 'logo' ), + 'input' => array( 'src', 'usemap', 'dynsrc', 'lowsrc', 'action', 'formaction' ), + + 'blockquote' => array( 'cite' ), + 'del' => array( 'cite' ), + 'frame' => array( 'longdesc', 'src' ), + 'head' => array( 'profile' ), + 'iframe' => array( 'longdesc', 'src' ), + 'ins' => array( 'cite' ), + 'object' => array( 'archive', 'classid', 'codebase', 'data', 'usemap' ), + 'q' => array( 'cite' ), + 'script' => array( 'src' ), + + 'audio' => array( 'src' ), + 'command' => array( 'icon' ), + 'embed' => array( 'src', 'code', 'pluginspage' ), + 'event-source' => array( 'src' ), + 'html' => array( 'manifest', 'background', 'xmlns' ), + 'source' => array( 'src' ), + 'video' => array( 'src', 'poster' ), + + 'bgsound' => array( 'src' ), + 'div' => array( 'href', 'src' ), + 'ilayer' => array( 'src' ), + 'table' => array( 'background' ), + 'td' => array( 'background' ), + 'th' => array( 'background' ), + 'layer' => array( 'src' ), + 'xml' => array( 'src' ), + + 'button' => array( 'action', 'formaction' ), + 'datalist' => array( 'data' ), + 'select' => array( 'data' ), + + 'access' => array( 'path' ), + 'card' => array( 'onenterforward', 'onenterbackward', 'ontimer' ), + 'go' => array( 'href' ), + 'option' => array( 'onpick' ), + 'template' => array( 'onenterforward', 'onenterbackward', 'ontimer' ), + 'wml' => array( 'xmlns' ) + ); + + // /** @const */ + // protected static $match_metas = array( + // 'content-base', + // 'content-location', + // 'referer', + // 'location', + // 'refresh', + // ); + + /** + * The URL request response + * + * @var Simply_Static_Url_Response + */ + protected $response; + + /** + * The url of the site + * + * @var array + */ + protected $extracted_urls = array(); + + /** + * Constructor + * + * @param string $page_contents The contents of the html/css/etc. page/file + * @param string $url The url of the site + */ + public function __construct( $response ) { + $this->response = $response; + } + + /** + * Extracts URLs from the response + * + * Returns a list of unique URLs from the body of the response. It only + * extracts URLs from the same domain, either absolute urls or relative urls + * that are then converted to absolute urls. + * + * Note that no validation is performed on whether the URLs would actually + * return a 200/OK response. + * + * @return array $urls + */ + public function extract_urls() { + + if ( $this->response->is_html() ) { + $this->extract_urls_from_html(); + } + + if ( $this->response->is_css() ) { + $this->extract_urls_from_css( $this->response->body ); + } + + return array_unique( $this->extracted_urls ); + } + + /** + * Loops through all elements in the DOM to pull out URLs + * + * @return void + */ + private function extract_urls_from_html() { + + $doc = new DOMDocument(); + libxml_use_internal_errors( true ); + $doc->loadHTML( $this->response->body ); + libxml_use_internal_errors( false ); + // get all elements on the page + $elements = $doc->getElementsByTagName( '*' ); + foreach ( $elements as $element ) { + $tag_name = $element->tagName; + + if( $tag_name === 'style' ) { + $this->extract_urls_from_css( $element->nodeValue ); + } else { + $style_attr_value = $element->getAttribute( 'style' ); + if ( $style_attr_value !== '' ) { + $this->extract_urls_from_css( $style_attr_value ); + } + + if ( array_key_exists( $tag_name, self::$match_elements ) ) { + $match_attributes = self::$match_elements[ $tag_name ]; + foreach ( $match_attributes as $attribute_name ) { + $extracted_url = $element->getAttribute( $attribute_name ); + $this->add_to_extracted_urls( $extracted_url ); + } + } + } + } + } + + /** + * Use regex to extract URLs on CSS pages + * + * URLs in CSS follow three basic patterns: + * - @import "common.css" screen, projection; + * - @import url("fineprint.css") print; + * - background-image: url(image.png); + * + * URLs are either contained within url(), part of an @import statement, + * or both. + * + * @return void + */ + private function extract_urls_from_css( $text ) { + + $patterns = array( "/url\(\s*[\"']?([^)\"']+)/", // url() + "/@import\s+[\"']([^\"']+)/" ); // @import w/o url() + + foreach ( $patterns as $pattern ) { + if ( preg_match_all( $pattern, $text, $matches, PREG_PATTERN_ORDER ) === false ) { + return; + } + + foreach ( $matches[1] as $match ) { + if ( !empty($match) ) { + $this->add_to_extracted_urls( $match ); + } + } + } + + } + + /** + * Add an extracted URL (relative or absolute) to the extracted URLs array + * + * Absolute URLs are only added if the scheme/host matches the site it was + * extracted from. Relative URLs are converted to absolute URLs before being + * added to the array. + * + * @return void + */ + private function add_to_extracted_urls( $extracted_url ) { + $absolute_url = sist_relative_to_absolute_url( $extracted_url, $this->response->url ); + + if ( $absolute_url && sist_is_local_url( $absolute_url ) ) { + $this->extracted_urls[] = sist_remove_params_and_fragment( $absolute_url ); + } + } +} diff --git a/includes/class-simply-static-url-fetcher.php b/includes/class-simply-static-url-fetcher.php new file mode 100644 index 0000000..f3f1fef --- /dev/null +++ b/includes/class-simply-static-url-fetcher.php @@ -0,0 +1,41 @@ + self::TIMEOUT, + 'sslverify' => false, // not verifying SSL because all calls are local + 'redirection' => 0 // disable redirection + ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } else { + return new Simply_Static_Url_Response( $url, $response ); + } + } + +} diff --git a/includes/class-simply-static-url-response.php b/includes/class-simply-static-url-response.php new file mode 100644 index 0000000..67bbee5 --- /dev/null +++ b/includes/class-simply-static-url-response.php @@ -0,0 +1,132 @@ +url = $url; + $this->headers = $response['headers']; + $this->body = $response['body']; + $this->code = $response['response']['code']; + $this->message = $response['response']['message']; + } + + /** + * Returns the content type + * + * @return string + */ + public function get_content_type() { + return isset( $this->headers['content-type'] ) ? $this->headers['content-type'] : null; + } + + /** + * Checks if content type is html + * + * @return bool + */ + public function is_html() { + return stripos( $this->get_content_type(), 'html' ) !== false; + } + + /** + * Checks if content type is html + * + * @return bool + */ + public function is_css() { + return stripos( $this->get_content_type(), 'css' ) !== false; + } + + /** + * Checks for a location header (redirection URL). Returns an empty string if + * it doesn't find one. + * + * @return string + */ + public function get_redirect_url() { + return isset( $this->headers['location'] ) ? $this->headers['location'] : ''; + } + + // /** + // * Removes WordPress-specific meta tags + // * + // * @return void + // */ + // protected function cleanup() { + // if ( $this->is_html() ) { + // $response_body = preg_replace( '//si', '', $this->get_response_body() ); + // $response_body = preg_replace( '//si', '', $response_body ); + // $this->set_response_body( $response_body ); + // } + // } + + /** + * Extracts the list of unique URLs + * + * @return array + */ + public function extract_urls() { + $extractor = new Simply_Static_Url_Extractor( $this ); + return $extractor->extract_urls(); + } + + /** + * Replaces base URL + * + * @param string $origin_url + * @param string $destination_url + * @return void + */ + public function replace_url( $origin_url, $destination_url ) { + if ( $this->is_html() ) { + $response_body = str_replace( $origin_url, $destination_url, $this->body ); + $this->body = $response_body; + } + } +} diff --git a/includes/class-simply-static-view.php b/includes/class-simply-static-view.php new file mode 100644 index 0000000..ff145b2 --- /dev/null +++ b/includes/class-simply-static-view.php @@ -0,0 +1,216 @@ +path = implode( '/', $path_array ); + } + + /** + * Sets a layout that will be used later in render() method. + * + * @param string $template The template filename, without extension + * @return Simply_Static_View + */ + public function set_layout( $layout ) { + $this->layout = trailingslashit( $this->path ) . 'layouts/' . $layout . self::EXTENSION; + + return $this; + } + + /** + * Sets a template that will be used later in render() method. + * + * @param string $template The template filename, without extension + * @return Simply_Static_View + */ + public function set_template( $template ) { + $this->template = trailingslashit( $this->path ) . $template . self::EXTENSION; + + return $this; + } + + /** + * Returns a value of the option identified by $name + * + * @param string $name The option name + * @return mixed|null + */ + public function __get( $name ) { + $value = array_key_exists( $name, $this->variables ) ? $this->variables[$name] : null; + return $value; + } + + /** + * Updates the view variable identified by $name with the value provided in $value + * + * @param string $name The variable name + * @param mixed $value The variable value + * @return Simply_Static_View + */ + public function __set( $name, $value ) { + $this->variables[$name] = $value; + return $this; + } + + /** + * Updates the view variable identified by $name with the value provided in $value + * + * @param string $name The variable name + * @param mixed $value The variable value + * @return Simply_Static_View + */ + public function assign( $name, $value ) { + return $this->__set( $name, $value ); + } + + /** + * Add a flash message to be displayed at the top of the page + * + * @param string $type The type of message to be displayed + * @param string $message The message to be displayed + * @return void + */ + public function add_flash( $type, $message ) { + array_push( $this->flashes, array( 'type' => $type, 'message' => $message ) ); + } + + /** + * Returns the layout (if available) or template. Checks to make sure that + * the file exists and is readable. + * + * @return string|WP_Error + */ + private function get_renderable_file() { + + // must include a template + if ( ! is_readable( $this->template ) ) { + return new WP_Error( 'invalid_template', sprintf( __( "Can't find view template: %s", $this->slug ), $this->template ) ); + } + + // layouts are optional. if no layout provided, use the template by itself. + if ( $this->layout ) { + if ( ! is_readable( $this->layout ) ) { + return new WP_Error( 'invalid_layout', sprintf( __( "Can't find view layout: %s", $this->slug ), $this->layout ) ); + } else { + // the layout should include the template + return $this->layout; + } + } else { + return $this->template; + } + + } + + /** + * Check Simply_Static's system requirements and add errors as necessary. + * + * @return void + */ + private function check_for_errors() { + + $errors = Simply_Static::instance()->check_system_requirements(); + + foreach ( $errors as $field ) { + foreach ( $field as $error ) { + $this->add_flash( 'error', $error ); + } + } + + } + + /** + * Renders the view script. + * + * @return Simply_Static_View|WP_Error + */ + public function render() { + + $this->check_for_errors(); + + $file = $this->get_renderable_file(); + + if ( is_wp_error( $file ) ) { + return $file; + } else { + include $file; + return $this; + } + + } + + /** + * Returns the view as a string. + * + * @return string|WP_Error + */ + public function render_to_string() { + + $file = $this->get_renderable_file(); + + if ( is_wp_error( $file ) ) { + return $file; + } else { + ob_start(); + include $file; + return ob_get_clean(); + } + + } +} diff --git a/includes/class-simply-static.php b/includes/class-simply-static.php new file mode 100755 index 0000000..92a04df --- /dev/null +++ b/includes/class-simply-static.php @@ -0,0 +1,420 @@ +includes(); + self::$instance->options = new Simply_Static_Options( self::SLUG ); + self::$instance->view = new Simply_Static_View(); + + // Load the text domain for i18n + add_action( 'plugins_loaded', array( self::$instance, 'load_textdomain' ) ); + // Enqueue admin styles + add_action( 'admin_enqueue_scripts', array( self::$instance, 'enqueue_admin_styles' ) ); + // Enqueue admin scripts + add_action( 'admin_enqueue_scripts', array( self::$instance, 'enqueue_admin_scripts' ) ); + // Add the options page and menu item. + add_action( 'admin_menu', array( self::$instance, 'add_plugin_admin_menu' ), 2 ); + } + + return self::$instance; + } + + /** + * Initialize singleton instance + * + * @param string $bootstrap_file + * @return Simply_Static + */ + public static function init( $bootstrap_file ) + { + $instance = self::instance(); + + // Activation + register_activation_hook( $bootstrap_file, array( $instance, 'activate' ) ); + + return $instance; + } + + /** + * Performs activation + * + * @return void + */ + public function activate() + { + // Not installed? + if ( null === $this->options->get( 'version' ) ) { + $this->options + ->set( 'version', self::VERSION ) + ->set( 'destination_scheme', sist_get_origin_scheme() ) + ->set( 'destination_host', sist_get_origin_host() ) + ->set( 'temp_files_dir', trailingslashit( plugin_dir_path( dirname( __FILE__ ) ) . 'static-files' ) ) + ->set( 'additional_urls', '' ) + ->set( 'delivery_method', 's3' ) + ->set( 'local_dir', '' ) + ->set( 'delete_temp_files', '1' ) + ->set( 'aws_s3_bucket', '' ) + ->set( 'aws_access_key_id', '' ) + ->set( 'aws_secret_access_key', '' ) + ->set( 'aws_s3_region', 'us-east-1' ) + ->set( 'aws_s3_endpoint', 's3.amazonaws.com' ) + ->save(); + } + } + + /** + * Include required files + * + * @return void + */ + private function includes() { + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/libraries/phpuri.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/libraries/S3.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-options.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-view.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-url-extractor.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-url-fetcher.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-url-response.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-simply-static-archive-creator.php'; + require plugin_dir_path( dirname( __FILE__ ) ) . 'includes/misc-functions.php'; + } + + /** + * Enqueue admin-specific style sheets for this plugin's admin pages only + * + * @return null Return early if no settings page is registered. + */ + public function enqueue_admin_styles() { + // Plugin admin CSS. Tack on plugin version. + wp_enqueue_style( self::SLUG . '-admin-styles', plugin_dir_url( dirname( __FILE__ ) ) . 'css/admin.css', array(), self::VERSION ); + } + + /** + * Enqueue admin-specific javascript files for this plugin's admin pages only + * + * @return null Return early if no settings page is registered. + */ + public function enqueue_admin_scripts() { + // Plugin admin CSS. Tack on plugin version. + wp_enqueue_script( self::SLUG . '-admin-styles', plugin_dir_url( dirname( __FILE__ ) ) . 'js/admin.js', array(), self::VERSION ); + } + + /** + * Register the administration menu for this plugin into the WordPress Dashboard menu. + * + * @return void + */ + public function add_plugin_admin_menu() { + // Base 64 encoded SVG image. + $icon_svg = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgaWQ9InN2ZzM0MzQiICAgdmVyc2lvbj0iMS4xIiAgIGlua3NjYXBlOnZlcnNpb249IjAuOTEgcjEzNzI1IiAgIHdpZHRoPSIxODAiICAgaGVpZ2h0PSIzMDAiICAgdmlld0JveD0iMCAwIDE4MCAzMDAiICAgc29kaXBvZGk6ZG9jbmFtZT0iYm9sdC12ZWN0b3ItZ3JheS5zdmciPiAgPG1ldGFkYXRhICAgICBpZD0ibWV0YWRhdGEzNDQwIj4gICAgPHJkZjpSREY+ICAgICAgPGNjOldvcmsgICAgICAgICByZGY6YWJvdXQ9IiI+ICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4gICAgICAgIDxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPiAgICAgICAgPGRjOnRpdGxlIC8+ICAgICAgPC9jYzpXb3JrPiAgICA8L3JkZjpSREY+ICA8L21ldGFkYXRhPiAgPGRlZnMgICAgIGlkPSJkZWZzMzQzOCIgLz4gIDxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMSIgICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiICAgICBncmlkdG9sZXJhbmNlPSIxMCIgICAgIGd1aWRldG9sZXJhbmNlPSIxMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTUzNiIgICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjgwMSIgICAgIGlkPSJuYW1lZHZpZXczNDM2IiAgICAgc2hvd2dyaWQ9ImZhbHNlIiAgICAgZml0LW1hcmdpbi10b3A9IjAiICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIiAgICAgZml0LW1hcmdpbi1ib3R0b209IjAiICAgICBpbmtzY2FwZTp6b29tPSIyLjE0MjM3MjkiICAgICBpbmtzY2FwZTpjeD0iOC44Njg2Njc5IiAgICAgaW5rc2NhcGU6Y3k9IjE0Ny41MDAwMSIgICAgIGlua3NjYXBlOndpbmRvdy14PSItOCIgICAgIGlua3NjYXBlOndpbmRvdy15PSItOCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmczNDM0IiAvPiAgPHBhdGggICAgIHN0eWxlPSJmaWxsOiM5Y2ExYTY7ZmlsbC1vcGFjaXR5OjEiICAgICBkPSJNIDM5LjksMjMzLjUgODQuNDMzMzMzLDE2MS4xMzMzMyAzOS45LDE2MS4xMzMzMyAxNDAuMSw2Ni41IGwgLTQ0LjUzMzMzMyw3Mi4zNjY2NyA0NC41MzMzMzMsMCB6IiAgICAgaWQ9InBhdGgzNDQ2IiAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjYyIgLz48L3N2Zz4='; + + // Add main menu item + add_menu_page( + __( 'Simply Static S3 Settings', self::SLUG ), + __( 'Simply Static S3', self::SLUG ), + 'manage_options', + self::SLUG, + array( self::$instance, 'display_generate_page' ), + $icon_svg + ); + + add_submenu_page( + self::SLUG, + __( 'Generate Static Site', self::SLUG ), + __( 'Generate', self::SLUG ), + 'manage_options', + self::SLUG, + array( self::$instance, 'display_generate_page' ) + ); + + add_submenu_page( + self::SLUG, + __( 'Simply Static S3 Settings', self::SLUG ), + __( 'Settings', self::SLUG ), + 'manage_options', + self::SLUG . '-options', + array( self::$instance, 'display_options_page' ) + ); + } + + /** + * Render the page for generating a static site. + * + * @return void + */ + public function display_generate_page() { + if ( $this->check_system_requirements() ) { + $this->view->assign( 'system_requirements_check_failed', true ); + } + + if ( isset( $_POST['generate'] ) ) { + $archive_creator = new Simply_Static_Archive_Creator( + self::SLUG, + $this->options->get( 'destination_scheme' ), + $this->options->get( 'destination_host' ), + $this->options->get( 'temp_files_dir' ), + $this->options->get( 'additional_urls' ) + ); + $archive_creator->create_archive(); + + // TODO: archive_url could be a WP_Error + if ( $this->options->get( 'delivery_method' ) == 'zip' ) { + + $archive_url = $archive_creator->create_zip(); + if ( is_wp_error( $archive_url ) ) { + $error = $archive_url->get_error_message(); + $this->view->add_flash( 'error', $error ); + } else { + $message = __( 'ZIP archive created: ', self::SLUG ); + $message .= ' ' . __( 'Click here to download', self::SLUG ) . ''; + $this->view->add_flash( 'updated', $message ); + } + + } elseif ( $this->options->get( 'delivery_method' ) == 'local' ) { + + $local_dir = $this->options->get( 'local_dir' ); + $result = $archive_creator->copy_static_files( $local_dir ); + + if ( is_wp_error( $result ) ) { + $error = $result->get_error_message(); + $this->view->add_flash( 'error', $error ); + } else { + $message = __( 'Static files copied to: ' . $local_dir, self::SLUG ); + $this->view->add_flash( 'updated', $message ); + } + + } elseif ( $this->options->get( 'delivery_method' ) == 's3' ) { + $bucket = $this->options->get( 'aws_s3_bucket' ); + $result = $archive_creator->publish_to_s3( + $bucket, + $this->options->get( 'aws_access_key_id' ), + $this->options->get( 'aws_secret_access_key' ), + $this->options->get( 'aws_s3_region' ), + $this->options->get( 'aws_s3_endpoint' ) + ); + if ( is_wp_error( $result ) ) { + $error = $result->get_error_message(); + $this->view->add_flash( 'error', $error ); + } else { + $message = __( 'Site published to S3 bucket: ' . $bucket, self::SLUG ); + $this->view->add_flash( 'updated', $message ); + } + } + + if ( $this->options->get( 'delete_temp_files' ) == '1' ) { + $deleted_successfully = $archive_creator->delete_static_files(); + } + + $export_log = $archive_creator->get_export_log(); + $this->view->assign( 'export_log', $export_log ); + } + + $this->view + ->set_layout( 'admin' ) + ->set_template( 'generate' ) + ->render(); + } + + /** + * Render the options page. + * + * @return void + */ + public function display_options_page() { + if ( isset( $_POST['save'] ) ) { + $this->save_options(); + $message = __( 'Settings saved.', self::SLUG ); + $this->view->add_flash( 'updated', $message ); + } + + $this->view + ->set_layout( 'admin' ) + ->set_template( 'options' ) + ->assign( 'slug', self::SLUG ) + ->assign( 'origin_scheme', sist_get_origin_scheme() ) + ->assign( 'origin_host', sist_get_origin_host() ) + ->assign( 'destination_scheme', $this->options->get( 'destination_scheme' ) ) + ->assign( 'destination_host', $this->options->get( 'destination_host' ) ) + ->assign( 'temp_files_dir', $this->options->get( 'temp_files_dir' ) ) + ->assign( 'additional_urls', $this->options->get( 'additional_urls' ) ) + ->assign( 'delivery_method', $this->options->get( 'delivery_method' ) ) + ->assign( 'local_dir', $this->options->get( 'local_dir' ) ) + ->assign( 'delete_temp_files', $this->options->get( 'delete_temp_files' ) ) + ->assign( 'aws_s3_bucket', $this->options->get( 'aws_s3_bucket' ) ) + ->assign( 'aws_access_key_id', $this->options->get( 'aws_access_key_id' ) ) + ->assign( 'aws_secret_access_key', $this->options->get( 'aws_secret_access_key' ) ) + ->assign( 'aws_s3_region', $this->options->get( 'aws_s3_region' ) ) + ->assign( 'aws_s3_endpoint', $this->options->get( 'aws_s3_endpoint' ) ) + ->render(); + } + + /** + * Save the options from the options page. + * + * @return void + */ + public function save_options() { + $this->options + ->set( 'destination_scheme', filter_input( INPUT_POST, 'destination_scheme' ) ) + ->set( 'destination_host', untrailingslashit( filter_input( INPUT_POST, 'destination_host', FILTER_SANITIZE_URL ) ) ) + ->set( 'temp_files_dir', sist_trailingslashit_unless_blank( filter_input( INPUT_POST, 'temp_files_dir' ) ) ) + ->set( 'additional_urls', filter_input( INPUT_POST, 'additional_urls' ) ) + ->set( 'delivery_method', filter_input( INPUT_POST, 'delivery_method' ) ) + ->set( 'local_dir', sist_trailingslashit_unless_blank( filter_input( INPUT_POST, 'local_dir' ) ) ) + ->set( 'delete_temp_files', filter_input( INPUT_POST, 'delete_temp_files' ) ) + ->set( 'aws_s3_bucket', filter_input( INPUT_POST, 'aws_s3_bucket' ) ) + ->set( 'aws_access_key_id', filter_input( INPUT_POST, 'aws_access_key_id' ) ) + ->set( 'aws_secret_access_key', filter_input( INPUT_POST, 'aws_secret_access_key' ) ) + ->set( 'aws_s3_region', filter_input( INPUT_POST, 'aws_s3_region' ) ) + ->set( 'aws_s3_endpoint', filter_input( INPUT_POST, 'aws_s3_endpoint' ) ) + ->save(); + } + + /** + * Loads the plugin language files + * + * @return void + */ + public function load_textdomain() { + load_plugin_textdomain( + self::SLUG, + false, + dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/' + ); + } + + /** + * Loads the plugin language files + * + * @return array $errors associative array containing errored field names and an array of error messages + */ + public function check_system_requirements() { + $errors = array(); + + $destination_host = $this->options->get( 'destination_host' ); + if ( strlen( $destination_host ) === 0 ) { + $errors['destination_host'][] = __( 'Destination URL cannot be blank', self::SLUG ); + } + + $temp_files_dir = $this->options->get( 'temp_files_dir' ); + if ( strlen( $temp_files_dir ) === 0 ) { + $errors['temp_files_dir'][] = __( 'Temporary Files Directory cannot be blank', self::SLUG ); + } else { + if ( file_exists( $temp_files_dir ) ) { + if ( ! is_writeable( $temp_files_dir ) ) { + $errors['delivery_method'][] = sprintf( __( 'Temporary Files Directory is not writeable: %s', self::SLUG ), $temp_files_dir ); + } + } else { + $errors['delivery_method'][] = sprintf( __( 'Temporary Files Directory does not exist: %s', self::SLUG ), $temp_files_dir ); + } + } + + + if ( strlen( get_option( 'permalink_structure' ) ) === 0 ) { + $errors['permalink_structure'][] = sprintf( __( "Your site does not have a permalink structure set. You can select one on the Permalink Settings page.", self::SLUG ), admin_url( '/options-permalink.php' ) ); + } + + if ( $this->options->get( 'delivery_method' ) == 's3' ) { + if ( strlen( $this->options->get( 'aws_s3_bucket' ) ) == 0 ) { + $errors['delivery_method'][] = __( "You haven't configured an Amazon AWS bucket for S3 publishing.", self::SLUG ); + } + if ( strlen( $this->options->get( 'aws_access_key_id' ) ) == 0 || strlen( $this->options->get( 'aws_secret_access_key' ) ) == 0 ) { + $errors['delivery_method'][] = __( "You haven't configured Amazon AWS credentials for S3 publishing.", self::SLUG ); + } + } + + if ( $this->options->get( 'delivery_method' ) == 'zip' ) { + if ( ! extension_loaded('zip') ) { + $errors['delivery_method'][] = __( "Your server does not have the PHP zip extension enabled. Please visit the PHP zip extension page for more information on how to enable it.", self::SLUG ); + } + } + + if ( $this->options->get( 'delivery_method' ) == 'local' ) { + $local_dir = $this->options->get( 'local_dir' ); + + if ( strlen( $local_dir ) === 0 ) { + $errors['delivery_method'][] = __( 'Local Directory cannot be blank', self::SLUG ); + } else { + if ( file_exists( $local_dir ) ) { + if ( ! is_writeable( $local_dir ) ) { + $errors['delivery_method'][] = sprintf( __( 'Local Directory is not writeable: %s', self::SLUG ), $local_dir ); + } + } else { + $errors['delivery_method'][] = sprintf( __( 'Local Directory does not exist: %s', self::SLUG ), $local_dir ); + } + } + } + + return $errors; + } +} diff --git a/includes/index.php b/includes/index.php new file mode 100755 index 0000000..e71af0e --- /dev/null +++ b/includes/index.php @@ -0,0 +1 @@ + $host, 'type' => $type, 'user' => $user, 'pass' => $pass); + } + + + /** + * Set the error mode to exceptions + * + * @param boolean $enabled Enable exceptions + * @return void + */ + public static function setExceptions($enabled = true) + { + self::$useExceptions = $enabled; + } + + + /** + * Set AWS time correction offset (use carefully) + * + * This can be used when an inaccurate system time is generating + * invalid request signatures. It should only be used as a last + * resort when the system time cannot be changed. + * + * @param string $offset Time offset (set to zero to use AWS server time) + * @return void + */ + public static function setTimeCorrectionOffset($offset = 0) + { + if ($offset == 0) + { + $rest = new S3Request('HEAD'); + $rest = $rest->getResponse(); + $awstime = $rest->headers['date']; + $systime = time(); + $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime); + } + self::$__timeOffset = $offset; + } + + + /** + * Set signing key + * + * @param string $keyPairId AWS Key Pair ID + * @param string $signingKey Private Key + * @param boolean $isFile Load private key from file, set to false to load string + * @return boolean + */ + public static function setSigningKey($keyPairId, $signingKey, $isFile = true) + { + self::$__signingKeyPairId = $keyPairId; + if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? + file_get_contents($signingKey) : $signingKey)) !== false) return true; + self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); + return false; + } + + + /** + * Free signing key from memory, MUST be called if you are using setSigningKey() + * + * @return void + */ + public static function freeSigningKey() + { + if (self::$__signingKeyResource !== false) + openssl_free_key(self::$__signingKeyResource); + } + + + /** + * Internal error handler + * + * @internal Internal error handler + * @param string $message Error message + * @param string $file Filename + * @param integer $line Line number + * @param integer $code Error code + * @return void + */ + private static function __triggerError($message, $file, $line, $code = 0) + { + if (self::$useExceptions) + throw new S3Exception($message, $file, $line, $code); + else + trigger_error($message, E_USER_WARNING); + } + + + /** + * Get a list of buckets + * + * @param boolean $detailed Returns detailed bucket list when true + * @return array | false + */ + public static function listBuckets($detailed = false) + { + $rest = new S3Request('GET', '', '', self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], + $rest->error['message']), __FILE__, __LINE__); + return false; + } + $results = array(); + if (!isset($rest->body->Buckets)) return $results; + + if ($detailed) + { + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $results['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + $results['buckets'] = array(); + foreach ($rest->body->Buckets->Bucket as $b) + $results['buckets'][] = array( + 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) + ); + } else + foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; + + return $results; + } + + + /** + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @param string $delimiter Delimiter + * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + if ($maxKeys == 0) $maxKeys = null; + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); + if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter); + $response = $rest->getResponse(); + if ($response->error === false && $response->code !== 200) + $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); + if ($response->error !== false) + { + self::__triggerError(sprintf("S3::getBucket(): [%s] %s", + $response->error['code'], $response->error['message']), __FILE__, __LINE__); + return false; + } + + $results = array(); + + $nextMarker = null; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) + { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->IsTruncated) && + (string)$response->body->IsTruncated == 'false') return $results; + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + // Loop through truncated results if maxKeys isn't specified + if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') + do + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + $rest->setParameter('marker', $nextMarker); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + + if (($response = $rest->getResponse()) == false || $response->code !== 200) break; + + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) + { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + + return $results; + } + + + /** + * Put a bucket + * + * @param string $bucket Bucket name + * @param constant $acl ACL flag + * @param string $location Set as "EU" to create buckets hosted in Europe + * @return boolean + */ + public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) + { + if ($location === false) + $location = self::$region; + if ($location === 'us-east-1' || empty($location)) + $location = false; + + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest->setAmzHeader('x-amz-acl', $acl); + + if ($location !== false) + { + $dom = new DOMDocument; + $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); + $locationConstraint = $dom->createElement('LocationConstraint', $location); + $createBucketConfiguration->appendChild($locationConstraint); + $dom->appendChild($createBucketConfiguration); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + } + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Delete an empty bucket + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function deleteBucket($bucket) + { + $rest = new S3Request('DELETE', $bucket, '', self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ + public static function inputFile($file, $md5sum = true) + { + if (!file_exists($file) || !is_file($file) || !is_readable($file)) + { + self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); + return false; + } + clearstatcache(false, $file); + return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? + (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); + } + + + /** + * Create input array info for putObject() with a resource + * + * @param string $resource Input resource to read from + * @param integer $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ + public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') + { + if (!is_resource($resource) || (int)$bufferSize < 0) + { + self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); + return false; + } + + // Try to figure out the bytesize + if ($bufferSize === false) + { + if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) + { + self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__); + return false; + } + fseek($resource, 0); + } + + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); + $input['fp'] =& $resource; + return $input; + } + + + /** + * Put an object + * + * @param mixed $input Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param array $requestHeaders Array of request headers or content type as a string + * @param constant $storageClass Storage class constant + * @param constant $serverSideEncryption Server-side encryption + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) + { + if ($input === false) return false; + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + + if (!is_array($input)) $input = array( + 'data' => $input, 'size' => strlen($input), + 'md5sum' => base64_encode(md5($input, true)) + ); + + // Data + if (isset($input['fp'])) + $rest->fp =& $input['fp']; + elseif (isset($input['file'])) + $rest->fp = @fopen($input['file'], 'rb'); + elseif (isset($input['data'])) + $rest->data = $input['data']; + + // Content-Length (required) + if (isset($input['size']) && $input['size'] >= 0) + $rest->size = $input['size']; + else { + if (isset($input['file'])) { + clearstatcache(false, $input['file']); + $rest->size = filesize($input['file']); + } + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); + } + + // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) + if (is_array($requestHeaders)) + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); + elseif (is_string($requestHeaders)) // Support for legacy contentType parameter + $input['type'] = $requestHeaders; + + // Content-Type + if (!isset($input['type'])) + { + if (isset($requestHeaders['Content-Type'])) + $input['type'] =& $requestHeaders['Content-Type']; + elseif (isset($input['file'])) + $input['type'] = self::__getMIMEType($input['file']); + else + $input['type'] = 'application/octet-stream'; + } + + if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class + $rest->setAmzHeader('x-amz-storage-class', $storageClass); + + if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption + $rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption); + + // We need to post with Content-Length and Content-Type, MD5 is optional + if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) + { + $rest->setHeader('Content-Type', $input['type']); + if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + + if ($acl !== false) $rest->setAmzHeader('x-amz-acl', $acl); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->getResponse(); + } else + $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) + { + self::__triggerError(sprintf("S3::putObject(): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Put an object from a file (legacy function) + * + * @param string $file Input file path + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) + { + return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') + { + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Get an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param mixed $saveTo Filename or resource to write to + * @return mixed + */ + public static function getObject($bucket, $uri, $saveTo = false) + { + $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + if ($saveTo !== false) + { + if (is_resource($saveTo)) + $rest->fp =& $saveTo; + else + if (($rest->fp = @fopen($saveTo, 'wb')) !== false) + $rest->file = realpath($saveTo); + else + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + } + if ($rest->response->error === false) $rest->getResponse(); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) + { + self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + return false; + } + return $rest->response; + } + + + /** + * Get object information + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @return mixed | false + */ + public static function getObjectInfo($bucket, $uri, $returnInfo = true) + { + $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + } + + + /** + * Copy an object + * + * @param string $srcBucket Source bucket name + * @param string $srcUri Source object URI + * @param string $bucket Destination bucket name + * @param string $uri Destination object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Optional array of x-amz-meta-* headers + * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) + * @param constant $storageClass Storage class constant + * @return mixed | false + */ + public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) + { + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest->setHeader('Content-Length', 0); + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class + $rest->setAmzHeader('x-amz-storage-class', $storageClass); + if ($acl !== false) $rest->setAmzHeader('x-amz-acl', $acl); + $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); + if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) + $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); + + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return isset($rest->body->LastModified, $rest->body->ETag) ? array( + 'time' => strtotime((string)$rest->body->LastModified), + 'hash' => substr((string)$rest->body->ETag, 1, -1) + ) : false; + } + + + /** + * Set up a bucket redirection + * + * @param string $bucket Bucket name + * @param string $location Target host name + * @return boolean + */ + public static function setBucketRedirect($bucket = NULL, $location = NULL) + { + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + + if( empty($bucket) || empty($location) ) { + self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); + return false; + } + + $dom = new DOMDocument; + $websiteConfiguration = $dom->createElement('WebsiteConfiguration'); + $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo'); + $hostName = $dom->createElement('HostName', $location); + $redirectAllRequestsTo->appendChild($hostName); + $websiteConfiguration->appendChild($redirectAllRequestsTo); + $dom->appendChild($websiteConfiguration); + $rest->setParameter('website', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Set logging for a bucket + * + * @param string $bucket Bucket name + * @param string $targetBucket Target bucket (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @return boolean + */ + public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) + { + // The S3 log delivery group has to be added to the target bucket's ACP + if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) + { + // Only add permissions to the target bucket when they do not exist + $aclWriteSet = false; + $aclReadSet = false; + foreach ($acp['acl'] as $acl) + if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') + { + if ($acl['permission'] == 'WRITE') $aclWriteSet = true; + elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; + } + if (!$aclWriteSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' + ); + if (!$aclReadSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' + ); + if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); + } + + $dom = new DOMDocument; + $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); + $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + if ($targetBucket !== null) + { + if ($targetPrefix == null) $targetPrefix = $bucket . '-'; + $loggingEnabled = $dom->createElement('LoggingEnabled'); + $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); + $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); + // TODO: Add TargetGrants? + $bucketLoggingStatus->appendChild($loggingEnabled); + } + $dom->appendChild($bucketLoggingStatus); + + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest->setParameter('logging', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string $bucket Bucket name + * @return array | false + */ + public static function getBucketLogging($bucket) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest->setParameter('logging', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + if (!isset($rest->body->LoggingEnabled)) return false; // No logging + return array( + 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, + 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, + ); + } + + + /** + * Disable bucket logging + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function disableBucketLogging($bucket) + { + return self::setBucketLogging($bucket, null); + } + + + /** + * Get a bucket's location + * + * @param string $bucket Bucket name + * @return string | false + */ + public static function getBucketLocation($bucket) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest->setParameter('location', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; + } + + + /** + * Set object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) + { + $dom = new DOMDocument; + $dom->formatOutput = true; + $accessControlPolicy = $dom->createElement('AccessControlPolicy'); + $accessControlList = $dom->createElement('AccessControlList'); + + // It seems the owner has to be passed along too + $owner = $dom->createElement('Owner'); + $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); + $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); + $accessControlPolicy->appendChild($owner); + + foreach ($acp['acl'] as $g) + { + $grant = $dom->createElement('Grant'); + $grantee = $dom->createElement('Grantee'); + $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if (isset($g['id'])) + { // CanonicalUser (DisplayName is omitted) + $grantee->setAttribute('xsi:type', 'CanonicalUser'); + $grantee->appendChild($dom->createElement('ID', $g['id'])); + } + elseif (isset($g['email'])) + { // AmazonCustomerByEmail + $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); + $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); + } + elseif ($g['type'] == 'Group') + { // Group + $grantee->setAttribute('xsi:type', 'Group'); + $grantee->appendChild($dom->createElement('URI', $g['uri'])); + } + $grant->appendChild($grantee); + $grant->appendChild($dom->createElement('Permission', $g['permission'])); + $accessControlList->appendChild($grant); + } + + $accessControlPolicy->appendChild($accessControlList); + $dom->appendChild($accessControlPolicy); + + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest->setParameter('acl', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '') + { + $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + $rest->setParameter('acl', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + + $acp = array(); + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $acp['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + + if (isset($rest->body->AccessControlList)) + { + $acp['acl'] = array(); + foreach ($rest->body->AccessControlList->Grant as $grant) + { + foreach ($grant->Grantee as $grantee) + { + if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser + $acp['acl'][] = array( + 'type' => 'CanonicalUser', + 'id' => (string)$grantee->ID, + 'name' => (string)$grantee->DisplayName, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail + $acp['acl'][] = array( + 'type' => 'AmazonCustomerByEmail', + 'email' => (string)$grantee->EmailAddress, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->URI)) // Group + $acp['acl'][] = array( + 'type' => 'Group', + 'uri' => (string)$grantee->URI, + 'permission' => (string)$grant->Permission + ); + else continue; + } + } + } + return $acp; + } + + + /** + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return boolean + */ + public static function deleteObject($bucket, $uri) + { + $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteObject(): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get a query string authenticated URL + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param integer $lifetime Lifetime in seconds + * @param boolean $hostBucket Use the bucket name as the hostname + * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @return string + */ + public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) + { + $expires = self::__getTime() + $lifetime; + $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); + return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', + // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, + $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, + urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); + } + + + /** + * Get a CloudFront signed policy URL + * + * @param array $policy Policy + * @return string + */ + public static function getSignedPolicyURL($policy) + { + $data = json_encode($policy); + $signature = ''; + if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false; + + $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data)); + $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature)); + + $url = $policy['Statement'][0]['Resource'] . '?'; + foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v) + $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&'; + return substr($url, 0, -1); + } + + + /** + * Get a CloudFront canned policy URL + * + * @param string $url URL to sign + * @param integer $lifetime URL lifetime + * @return string + */ + public static function getSignedCannedURL($url, $lifetime) + { + return self::getSignedPolicyURL(array( + 'Statement' => array( + array('Resource' => $url, 'Condition' => array( + 'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime) + )) + ) + )); + } + + + /** + * Get upload POST parameters for form uploads + * + * @param string $bucket Bucket name + * @param string $uriPrefix Object URI prefix + * @param constant $acl ACL constant + * @param integer $lifetime Lifetime in seconds + * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) + * @param string $successRedirect Redirect URL or 200 / 201 status code + * @param array $amzHeaders Array of x-amz-meta-* headers + * @param array $headers Array of request headers or content type as a string + * @param boolean $flashVars Includes additional "Filename" variable posted by Flash + * @return object + */ + public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, + $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) + { + // Create policy object + $policy = new stdClass; + $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime)); + $policy->conditions = array(); + $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); + $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); + + $obj = new stdClass; // 200 for non-redirect uploads + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $obj->success_action_status = (string)$successRedirect; + else // URL + $obj->success_action_redirect = $successRedirect; + array_push($policy->conditions, $obj); + + if ($acl !== self::ACL_PUBLIC_READ) + array_push($policy->conditions, array('eq', '$acl', $acl)); + + array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); + if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); + foreach (array_keys($headers) as $headerKey) + array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); + foreach ($amzHeaders as $headerKey => $headerVal) + { + $obj = new stdClass; + $obj->{$headerKey} = (string)$headerVal; + array_push($policy->conditions, $obj); + } + array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); + $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); + + // Create parameters + $params = new stdClass; + $params->AWSAccessKeyId = self::$__accessKey; + $params->key = $uriPrefix.'${filename}'; + $params->acl = $acl; + $params->policy = $policy; unset($policy); + $params->signature = self::__getHash($params->policy); + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $params->success_action_status = (string)$successRedirect; + else + $params->success_action_redirect = $successRedirect; + foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + return $params; + } + + + /** + * Create a CloudFront distribution + * + * @param string $bucket Bucket name + * @param boolean $enabled Enabled (true/false) + * @param array $cnames Array containing CNAME aliases + * @param string $comment Use the bucket name as the hostname + * @param string $defaultRootObject Default root object + * @param string $originAccessIdentity Origin access identity + * @param array $trustedSigners Array of trusted signers + * @return array | false + */ + public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML( + $bucket.'.s3.amazonaws.com', + $enabled, + (string)$comment, + (string)microtime(true), + $cnames, + $defaultRootObject, + $originAccessIdentity, + $trustedSigners + ); + + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) + return self::__parseCloudFrontDistributionConfig($rest->body); + return false; + } + + + /** + * Get CloudFront distribution info + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array | false + */ + public static function getDistribution($distributionId) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::getDistribution($distributionId): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement) + { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + $dist['id'] = $distributionId; + return $dist; + } + return false; + } + + + /** + * Update a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return array | false + */ + public static function updateDistribution($dist) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML( + $dist['origin'], + $dist['enabled'], + $dist['comment'], + $dist['callerReference'], + $dist['cnames'], + $dist['defaultRootObject'], + $dist['originAccessIdentity'], + $dist['trustedSigners'] + ); + + $rest->size = strlen($rest->data); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } else { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Delete a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return boolean + */ + public static function deleteDistribution($dist) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get a list of CloudFront distributions + * + * @return array + */ + public static function listDistributions() + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) + { + $list = array(); + if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) + { + //$info['marker'] = (string)$rest->body->Marker; + //$info['maxItems'] = (int)$rest->body->MaxItems; + //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; + } + foreach ($rest->body->DistributionSummary as $summary) + $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); + + return $list; + } + return array(); + } + + /** + * List CloudFront Origin Access Identities + * + * @return array + */ + public static function listOriginAccessIdentities() + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + $useSSL = self::$useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + + if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) + { + $identities = array(); + foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity) + if (isset($identity->S3CanonicalUserId)) + $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId); + return $identities; + } + return false; + } + + + /** + * Invalidate objects in a CloudFront distribution + * + * Thanks to Martin Lindkvist for S3::invalidateDistribution() + * + * @param string $distributionId Distribution ID from listDistributions() + * @param array $paths Array of object paths to invalidate + * @return boolean + */ + public static function invalidateDistribution($distributionId, $paths) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); + $rest->size = strlen($rest->data); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a InvalidationBatch DOMDocument + * + * @internal Used to create XML in invalidateDistribution() + * @param array $paths Paths to objects to invalidateDistribution + * @param int $callerReference + * @return string + */ + private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') + { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $invalidationBatch = $dom->createElement('InvalidationBatch'); + foreach ($paths as $path) + $invalidationBatch->appendChild($dom->createElement('Path', $path)); + + $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference)); + $dom->appendChild($invalidationBatch); + return $dom->saveXML(); + } + + + /** + * List your invalidation batches for invalidateDistribution() in a CloudFront distribution + * + * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html + * returned array looks like this: + * Array + * ( + * [I31TWB0CN9V6XD] => InProgress + * [IT3TFE31M0IHZ] => Completed + * [I12HK7MPO1UQDA] => Completed + * [I1IA7R6JKTC3L2] => Completed + * ) + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array + */ + public static function getDistributionInvalidationList($distributionId) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) + { + $list = array(); + foreach ($rest->body->InvalidationSummary as $summary) + $list[(string)$summary->Id] = (string)$summary->Status; + + return $list; + } + return array(); + } + + + /** + * Get a DistributionConfig DOMDocument + * + * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html + * + * @internal Used to create XML in createDistribution() and updateDistribution() + * @param string $bucket S3 Origin bucket + * @param boolean $enabled Enabled (true/false) + * @param string $comment Comment to append + * @param string $callerReference Caller reference + * @param array $cnames Array of CNAME aliases + * @param string $defaultRootObject Default root object + * @param string $originAccessIdentity Origin access identity + * @param array $trustedSigners Array of trusted signers + * @return string + */ + private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) + { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $distributionConfig = $dom->createElement('DistributionConfig'); + $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/'); + + $origin = $dom->createElement('S3Origin'); + $origin->appendChild($dom->createElement('DNSName', $bucket)); + if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity)); + $distributionConfig->appendChild($origin); + + if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject)); + + $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); + foreach ($cnames as $cname) + $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); + if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); + $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); + + $trusted = $dom->createElement('TrustedSigners'); + foreach ($trustedSigners as $id => $type) + $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); + $distributionConfig->appendChild($trusted); + + $dom->appendChild($distributionConfig); + //var_dump($dom->saveXML()); + return $dom->saveXML(); + } + + + /** + * Parse a CloudFront distribution config + * + * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html + * + * @internal Used to parse the CloudFront DistributionConfig node to an array + * @param object &$node DOMNode + * @return array + */ + private static function __parseCloudFrontDistributionConfig(&$node) + { + if (isset($node->DistributionConfig)) + return self::__parseCloudFrontDistributionConfig($node->DistributionConfig); + + $dist = array(); + if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) + { + $dist['id'] = (string)$node->Id; + $dist['status'] = (string)$node->Status; + $dist['time'] = strtotime((string)$node->LastModifiedTime); + $dist['domain'] = (string)$node->DomainName; + } + + if (isset($node->CallerReference)) + $dist['callerReference'] = (string)$node->CallerReference; + + if (isset($node->Enabled)) + $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; + + if (isset($node->S3Origin)) + { + if (isset($node->S3Origin->DNSName)) + $dist['origin'] = (string)$node->S3Origin->DNSName; + + $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ? + (string)$node->S3Origin->OriginAccessIdentity : null; + } + + $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null; + + $dist['cnames'] = array(); + if (isset($node->CNAME)) + foreach ($node->CNAME as $cname) + $dist['cnames'][(string)$cname] = (string)$cname; + + $dist['trustedSigners'] = array(); + if (isset($node->TrustedSigners)) + foreach ($node->TrustedSigners as $signer) + { + if (isset($signer->Self)) + $dist['trustedSigners'][''] = 'Self'; + elseif (isset($signer->KeyPairId)) + $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId'; + elseif (isset($signer->AwsAccountNumber)) + $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber'; + } + + $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null; + return $dist; + } + + + /** + * Grab CloudFront response + * + * @internal Used to parse the CloudFront S3Request::getResponse() output + * @param object &$rest S3Request instance + * @return object + */ + private static function __getCloudFrontResponse(&$rest) + { + $rest->getResponse(); + if ($rest->response->error === false && isset($rest->response->body) && + is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); + // Grab CloudFront errors + if (isset($rest->response->body->Error, $rest->response->body->Error->Code, + $rest->response->body->Error->Message)) + { + $rest->response->error = array( + 'code' => (string)$rest->response->body->Error->Code, + 'message' => (string)$rest->response->body->Error->Message + ); + unset($rest->response->body); + } + } + return $rest->response; + } + + + /** + * Get MIME type for file + * + * To override the putObject() Content-Type, add it to $requestHeaders + * + * To use fileinfo, ensure the MAGIC environment variable is set + * + * @internal Used to get mime types + * @param string &$file File path + * @return string + */ + private static function __getMIMEType(&$file) + { + static $exts = array( + 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', + 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', + 'zip' => 'application/zip', 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', + 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', + 'css' => 'text/css', 'js' => 'text/javascript', + 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' + ); + + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (isset($exts[$ext])) return $exts[$ext]; + + // Use fileinfo if available + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) + { + if (($type = finfo_file($finfo, $file)) !== false) + { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + if ($type !== false && strlen($type) > 0) return $type; + } + + return 'application/octet-stream'; + } + + + /** + * Get the current time + * + * @internal Used to apply offsets to sytem time + * @return integer + */ + public static function __getTime() + { + return time() + self::$__timeOffset; + } + + + /** + * Generate the auth string: "AWS AccessKey:Signature" + * + * @internal Used by S3Request::getResponse() + * @param string $string String to sign + * @return string + */ + public static function __getSignature($string) + { + return 'AWS '.self::$__accessKey.':'.self::__getHash($string); + } + + + /** + * Creates a HMAC-SHA1 hash + * + * This uses the hash extension if loaded + * + * @internal Used by __getSignature() + * @param string $string String to sign + * @return string + */ + private static function __getHash($string) + { + return base64_encode(extension_loaded('hash') ? + hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( + (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ + (str_repeat(chr(0x36), 64))) . $string))))); + } + +} + +/** + * S3 Request class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ +final class S3Request +{ + /** + * AWS URI + * + * @var string + * @access pricate + */ + private $endpoint; + + /** + * Verb + * + * @var string + * @access private + */ + private $verb; + + /** + * S3 bucket name + * + * @var string + * @access private + */ + private $bucket; + + /** + * Object URI + * + * @var string + * @access private + */ + private $uri; + + /** + * Final object URI + * + * @var string + * @access private + */ + private $resource = ''; + + /** + * Additional request parameters + * + * @var array + * @access private + */ + private $parameters = array(); + + /** + * Amazon specific request headers + * + * @var array + * @access private + */ + private $amzHeaders = array(); + + /** + * HTTP request headers + * + * @var array + * @access private + */ + private $headers = array( + 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' + ); + + /** + * Use HTTP PUT? + * + * @var bool + * @access public + */ + public $fp = false; + + /** + * PUT file size + * + * @var int + * @access public + */ + public $size = 0; + + /** + * PUT post fields + * + * @var array + * @access public + */ + public $data = false; + + /** + * S3 request respone + * + * @var object + * @access public + */ + public $response; + + + /** + * Constructor + * + * @param string $verb Verb + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param string $endpoint AWS endpoint URI + * @return mixed + */ + function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') + { + + $this->endpoint = $endpoint; + $this->verb = $verb; + $this->bucket = $bucket; + $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; + + //if ($this->bucket !== '') + // $this->resource = '/'.$this->bucket.$this->uri; + //else + // $this->resource = $this->uri; + + if ($this->bucket !== '') + { + if ($this->__dnsBucketName($this->bucket)) + { + $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; + $this->resource = '/'.$this->bucket.$this->uri; + } + else + { + $this->headers['Host'] = $this->endpoint; + $this->uri = $this->uri; + if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; + $this->bucket = ''; + $this->resource = $this->uri; + } + } + else + { + $this->headers['Host'] = $this->endpoint; + $this->resource = $this->uri; + } + + + $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); + $this->response = new STDClass; + $this->response->error = false; + $this->response->body = null; + $this->response->headers = array(); + } + + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setParameter($key, $value) + { + $this->parameters[$key] = $value; + } + + + /** + * Set request header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setHeader($key, $value) + { + $this->headers[$key] = $value; + } + + + /** + * Set x-amz-meta-* header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setAmzHeader($key, $value) + { + $this->amzHeaders[$key] = $value; + } + + + /** + * Get the S3 response + * + * @return object | false + */ + public function getResponse() + { + $query = ''; + $canonicalUri = $this->uri; + + if (sizeof($this->parameters) > 0) + { + $query = substr($this->uri, -1) !== '?' ? '?' : '&'; + foreach ($this->parameters as $var => $value) + if ($value == null || $value == '') $query .= $var.'&'; + else $query .= $var.'='.rawurlencode($value).'&'; + $query = substr($query, 0, -1); + $this->uri .= $query; + + if (array_key_exists('acl', $this->parameters) || + array_key_exists('location', $this->parameters) || + array_key_exists('torrent', $this->parameters) || + array_key_exists('website', $this->parameters) || + array_key_exists('logging', $this->parameters)) + $this->resource .= $query; + } + $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); + + if (S3::$useSSL) + { + // Set protocol version + curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); + + // SSL Validation can now be optional for those with broken OpenSSL installations + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); + + if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); + if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert); + if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + if (S3::$proxy != null && isset(S3::$proxy['host'])) + { + curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); + curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); + if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null) + curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass'])); + } + + // Headers + $headers = array(); + + if (S3::hasAuth()) { + if ($this->headers['Host'] == 'cloudfront.amazonaws.com') { + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); + } else { + $amzDate = gmdate('Ymd\THis\Z'); + $dateOnly = substr($amzDate, 0, 8); + $region = S3::$region; + $service = 's3'; + + $payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; + if ($this->fp !== false) { + $payloadHash = 'UNSIGNED-PAYLOAD'; + } elseif ($this->data !== false) { + $payloadHash = hash('sha256', $this->data); + } + + $this->amzHeaders['x-amz-date'] = $amzDate; + $this->amzHeaders['x-amz-content-sha256'] = $payloadHash; + + $allHeaders = array(); + foreach ($this->headers as $header => $value) { + if (strlen($value) > 0) $allHeaders[strtolower($header)] = trim($value); + } + foreach ($this->amzHeaders as $header => $value) { + if (strlen($value) > 0) $allHeaders[strtolower($header)] = trim($value); + } + + unset($allHeaders['date']); + + ksort($allHeaders); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($allHeaders as $header => $value) { + $canonicalHeaders .= $header . ':' . $value . "\n"; + $signedHeaders .= $header . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + + $canonicalQuery = ''; + if (sizeof($this->parameters) > 0) { + $qParams = array(); + foreach ($this->parameters as $var => $value) { + $qParams[rawurlencode($var)] = ($value === null || $value === '') ? '' : rawurlencode($value); + } + ksort($qParams); + foreach ($qParams as $k => $v) { + $canonicalQuery .= $k . '=' . $v . '&'; + } + $canonicalQuery = substr($canonicalQuery, 0, -1); + } + + $canonicalRequest = $this->verb . "\n" . + $canonicalUri . "\n" . + $canonicalQuery . "\n" . + $canonicalHeaders . "\n" . + $signedHeaders . "\n" . + $payloadHash; + + $scope = $dateOnly . '/' . $region . '/' . $service . '/aws4_request'; + + $stringToSign = "AWS4-HMAC-SHA256\n" . + $amzDate . "\n" . + $scope . "\n" . + hash('sha256', $canonicalRequest); + + $kSecret = 'AWS4' . S3::getSecretKey(); + $kDate = hash_hmac('sha256', $dateOnly, $kSecret, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); + + $signature = hash_hmac('sha256', $stringToSign, $kSigning); + + $authorization = "AWS4-HMAC-SHA256 Credential=" . S3::getAccessKey() . "/" . $scope . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + foreach ($allHeaders as $header => $value) { + $headers[] = $header . ': ' . $value; + } + $headers[] = 'Authorization: ' . $authorization; + } + } else { + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + } + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + // Request types + switch ($this->verb) + { + case 'GET': break; + case 'PUT': case 'POST': // POST only used for CloudFront + if ($this->fp !== false) + { + curl_setopt($curl, CURLOPT_PUT, true); + curl_setopt($curl, CURLOPT_INFILE, $this->fp); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); + } + elseif ($this->data !== false) + { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); + } + else + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + break; + case 'HEAD': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($curl, CURLOPT_NOBODY, true); + break; + case 'DELETE': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + + // Execute, grab errors + if (curl_exec($curl)) + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + else + $this->response->error = array( + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->headers['type']) && + $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) + { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab S3 errors + if (!in_array($this->response->code, array(200, 204, 206)) && + isset($this->response->body->Code, $this->response->body->Message)) + { + $this->response->error = array( + 'code' => (string)$this->response->body->Code, + 'message' => (string)$this->response->body->Message + ); + if (isset($this->response->body->Resource)) + $this->response->error['resource'] = (string)$this->response->body->Resource; + unset($this->response->body); + } + } + + // Clean up file resources + if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); + + return $this->response; + } + + /** + * Sort compare for meta headers + * + * @internal Used to sort x-amz meta headers + * @param string $a String A + * @param string $b String B + * @return integer + */ + private function __sortMetaHeadersCmp($a, $b) + { + $lenA = strpos($a, ':'); + $lenB = strpos($b, ':'); + $minLen = min($lenA, $lenB); + $ncmp = strncmp($a, $b, $minLen); + if ($lenA == $lenB) return $ncmp; + if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; + return $ncmp; + } + + /** + * CURL write callback + * + * @param resource $curl CURL resource + * @param string $data Data + * @return integer + */ + private function __responseWriteCallback($curl, $data) + { + if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) + return fwrite($this->fp, $data); + else + $this->response->body .= $data; + return strlen($data); + } + + + /** + * Check DNS conformity + * + * @param string $bucket Bucket name + * @return boolean + */ + private function __dnsBucketName($bucket) + { + if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; + if (S3::$useSSL && strstr($bucket, '.') !== false) return false; + if (strstr($bucket, '-.') !== false) return false; + if (strstr($bucket, '..') !== false) return false; + if (!preg_match("/^[0-9a-z]/", $bucket)) return false; + if (!preg_match("/[0-9a-z]$/", $bucket)) return false; + return true; + } + + + /** + * CURL header callback + * + * @param resource $curl CURL resource + * @param string $data Data + * @return integer + */ + private function __responseHeaderCallback($curl, $data) + { + if (($strlen = strlen($data)) <= 2) return $strlen; + if (substr($data, 0, 4) == 'HTTP') + $this->response->code = (int)substr($data, 9, 3); + else + { + $data = trim($data); + if (strpos($data, ': ') === false) return $strlen; + list($header, $value) = explode(': ', $data, 2); + if ($header == 'Last-Modified') + $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Date') + $this->response->headers['date'] = strtotime($value); + elseif ($header == 'Content-Length') + $this->response->headers['size'] = (int)$value; + elseif ($header == 'Content-Type') + $this->response->headers['type'] = $value; + elseif ($header == 'ETag') + $this->response->headers['hash'] = $value[0] == '"' ? substr($value, 1, -1) : $value; + elseif (preg_match('/^x-amz-meta-.*$/', $header)) + $this->response->headers[$header] = $value; + } + return $strlen; + } + +} + +/** + * S3 exception class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ + +class S3Exception extends Exception { + /** + * Class constructor + * + * @param string $message Exception message + * @param string $file File in which exception was created + * @param string $line Line number on which exception was created + * @param int $code Exception code + */ + function __construct($message, $file, $line, $code = 0) + { + parent::__construct($message, $code); + $this->file = $file; + $this->line = $line; + } +} diff --git a/includes/libraries/phpuri.php b/includes/libraries/phpuri.php new file mode 100644 index 0000000..c97b591 --- /dev/null +++ b/includes/libraries/phpuri.php @@ -0,0 +1,209 @@ + + * echo phpUri::parse('https://www.google.com/')->join('foo'); + * //==> https://www.google.com/foo + * + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @author P Guardiario + * @version 1.0 + */ + + /** + * phpUri + */ + class phpUri + { + + /** + * http(s):// + * @var string + */ + public $scheme; + + /** + * www.example.com + * @var string + */ + public $authority; + + /** + * /search + * @var string + */ + public $path; + + /** + * ?q=foo + * @var string + */ + public $query; + + /** + * #bar + * @var string + */ + public $fragment; + + private function __construct( $string ) + { + preg_match_all( '/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/', $string, $m ); + $this->scheme = $m[ 2 ][ 0 ]; + $this->authority = $m[ 4 ][ 0 ]; + + /** + * CHANGE: + * @author Dominik Habichtsberg + * @since 24 Mai 2015 10:02 Uhr + * + * Former code: $this->path = ( empty( $m[ 5 ][ 0 ] ) ) ? '/' : $m[ 5 ][ 0 ]; + * No tests failed, when the path is empty. + * With the former code, the relative urls //g and #s failed + */ + $this->path = $m[ 5 ][ 0 ]; + $this->query = $m[ 7 ][ 0 ]; + $this->fragment = $m[ 9 ][ 0 ]; + } + + private function to_str() + { + $ret = ''; + if ( !empty( $this->scheme ) ) + { + $ret .= "{$this->scheme}:"; + } + + if ( !empty( $this->authority ) ) + { + $ret .= "//{$this->authority}"; + } + + $ret .= $this->normalize_path( $this->path ); + + if ( !empty( $this->query ) ) + { + $ret .= "?{$this->query}"; + } + + if ( !empty( $this->fragment ) ) + { + $ret .= "#{$this->fragment}"; + } + + return $ret; + } + + private function normalize_path( $path ) + { + if ( empty( $path ) ) + { + return ''; + } + + $normalized_path = $path; + $normalized_path = preg_replace( '`//+`', '/', $normalized_path, -1, $c0 ); + $normalized_path = preg_replace( '`^/\\.\\.?/`', '/', $normalized_path, -1, $c1 ); + $normalized_path = preg_replace( '`/\\.(/|$)`', '/', $normalized_path, -1, $c2 ); + + /** + * CHANGE: + * @author Dominik Habichtsberg + * @since 24 Mai 2015 10:05 Uhr + * changed limit form -1 to 1, because climbing up the directory-tree failed + */ + $normalized_path = preg_replace( '`/[^/]*?/\\.\\.(/|$)`', '/', $normalized_path, 1, $c3 ); + $num_matches = $c0 + $c1 + $c2 + $c3; + + return ( $num_matches > 0 ) ? $this->normalize_path( $normalized_path ) : $normalized_path; + } + + /** + * Parse an url string + * + * @param string $url the url to parse + * + * @return phpUri + */ + public static function parse( $url ) + { + $uri = new phpUri( $url ); + + /** + * CHANGE: + * @author Dominik Habichtsberg + * @since 24 Mai 2015 10:25 Uhr + * The base-url should always have a path + */ + if ( empty( $uri->path ) ) + { + $uri->path = '/'; + } + + return $uri; + } + + /** + * Join with a relative url + * + * @param string $relative the relative url to join + * + * @return string + */ + public function join( $relative ) + { + $uri = new phpUri( $relative ); + switch ( TRUE ) + { + case !empty( $uri->scheme ): + break; + + case !empty( $uri->authority ): + break; + + case empty( $uri->path ): + $uri->path = $this->path; + if ( empty( $uri->query ) ) + { + $uri->query = $this->query; + } + break; + + case strpos( $uri->path, '/' ) === 0: + break; + + default: + $base_path = $this->path; + if ( strpos( $base_path, '/' ) === FALSE ) + { + $base_path = ''; + } + else + { + $base_path = preg_replace( '/\/[^\/]+$/', '/', $base_path ); + } + if ( empty( $base_path ) && empty( $this->authority ) ) + { + $base_path = '/'; + } + $uri->path = $base_path . $uri->path; + } + + if ( empty( $uri->scheme ) ) + { + $uri->scheme = $this->scheme; + if ( empty( $uri->authority ) ) + { + $uri->authority = $this->authority; + } + } + + return $uri->to_str(); + } + } \ No newline at end of file diff --git a/includes/misc-functions.php b/includes/misc-functions.php new file mode 100644 index 0000000..4f93995 --- /dev/null +++ b/includes/misc-functions.php @@ -0,0 +1,156 @@ + $length + 3 ) ? ( substr( $string, 0, $length ) . $omission ) : $string; +} + + +/** + * Use trailingslashit unless the string is empty + * + * @return string + */ +function sist_trailingslashit_unless_blank( $string ) { + return $string === '' ? $string : trailingslashit( $string ); +} + +/** + * Dump an object to error_log + * + * @return string + */ +function sist_error_log( $object=null ){ + ob_start(); + var_dump( $object ); + $contents = ob_get_contents(); + ob_end_clean(); + error_log( $contents ); +} + + +/** + * Given a URL extracted from a page, return an absolute URL + * + * Takes a URL (e.g. /test) extracted from a page (e.g. http://foo.com/bar/) and + * returns an absolute URL (e.g. http://foo.com/bar/test). Absolute URLs are + * returned as-is. + * + * A null value is returned in the event that the extracted_url is blank or it's + * unable to be parsed. + * + * @param string $extracted_url Relative or absolute URL extracted from page + * @param string $page_url URL of page + * @return string|null Absolute URL, or null + */ +function sist_relative_to_absolute_url( $extracted_url, $page_url ) { + + $extracted_url = trim( $extracted_url ); + + if ( $extracted_url === '' ) { + return null; + } + + $parsed_extracted_url = parse_url( $extracted_url ); + + // parse_url can sometimes return false; bail if it does + if ( $parsed_extracted_url === false ) { + return null; + } + + if ( isset( $parsed_extracted_url['host'] ) ) { + + return $extracted_url; + + } elseif ( isset( $parsed_extracted_url['scheme'] ) ) { + + // examples of schemes without hosts: java:, data: + return $extracted_url; + + } else { // no host on extracted page (might be relative url) + + $path = isset( $parsed_extracted_url['path'] ) ? $parsed_extracted_url['path'] : ''; + + // Check for a bug in PHP <= 5.4.7 where URLs starting + // with '//' are identified as a path + // http://php.net/manual/en/function.parse-url.php#example-4617 + if ( substr( $path, 0, 2 ) === '//' ) { + + return $extracted_url; + + } else { + + $query = isset( $parsed_extracted_url['query'] ) ? '?' . $parsed_extracted_url['query'] : ''; + $fragment = isset( $parsed_extracted_url['fragment'] ) ? '#' . $parsed_extracted_url['fragment'] : ''; + + // turn our relative url into an absolute url + $extracted_url = phpUri::parse( $page_url )->join( $path . $query . $fragment ); + + return $extracted_url; + + } + } +} + +/** + * Check if URL starts with same URL as WordPress installation + * + * @param string $url URL to check + * @return boolean true if URL is local, false otherwise + */ +function sist_is_local_url( $url ) { + return ( stripos( $url, home_url() ) === 0 ); +} + +/** + * Returns a URL w/o the query string or fragment (i.e. nothing after the path) + * + * @param string $url URL to remove query string/fragment from + * @return string URL without query string/fragment + */ +function sist_remove_params_and_fragment( $url ) { + return preg_replace('/(\?|#).*/', '', $url); +} diff --git a/index.php b/index.php new file mode 100755 index 0000000..e71af0e --- /dev/null +++ b/index.php @@ -0,0 +1 @@ + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100755 index 0000000..a524c6c --- /dev/null +++ b/readme.txt @@ -0,0 +1,3 @@ +Based on https://github.com/kennu/simply-static-s3 + +Updated for modern v4 API support and generic S3 providers. \ No newline at end of file diff --git a/simply-static.php b/simply-static.php new file mode 100755 index 0000000..fa50baa --- /dev/null +++ b/simply-static.php @@ -0,0 +1,18 @@ + + +

slug ); ?>

+ +
+ + export_log ) && count( $this->export_log ) ): ?> +

slug ); ?>

+
+
export_log ) ?>
+
+ + +
+ + system_requirements_check_failed ) : ?> +

slug ); ?>

+ +

slug ); ?>

+

slug ); ?>

+ + +

+ slug ) ?> + system_requirements_check_failed ) { echo 'disabled=disabled'; } ?> /> +

+ +
+ +
diff --git a/views/layouts/admin.phtml b/views/layouts/admin.phtml new file mode 100644 index 0000000..04fe928 --- /dev/null +++ b/views/layouts/admin.phtml @@ -0,0 +1,54 @@ + + +flashes as $flash ) : ?> +
+

+ +

+
+ + +
+
+ +
+ template; ?> +
+ + +
+ +
+ + +
+ +
+ diff --git a/views/options.phtml b/views/options.phtml new file mode 100644 index 0000000..07504ce --- /dev/null +++ b/views/options.phtml @@ -0,0 +1,200 @@ + + +
+ +

slug ); ?>

+ +
+ + slug ) ?> + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +

the General Settings page.", $this->slug ), admin_url( '/options-general.php' ) ); ?>

+
+ + + + +

slug ); ?>

+
+ + +
+

slug ); ?>

+
+

slug ); ?>

+
+ + + + +

slug ); ?>

+
+

slug ); ?>

+
+ + + +

slug ); ?>

+
+ + + +

slug ); ?>

+
+ + + +

slug ); ?>

+
+ + + +

slug ); ?>

+
+ + + +

slug ); ?>

+
+

+ +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + +

slug ); ?>

+
+ +

+ slug ); ?> +

+
+ + + +

+ aren't linked to, add the URLs here (one per line). Examples: %s or %s", $this->slug), + $this->origin_scheme . '://' . $this->origin_host, + $this->origin_scheme . '://' . $this->origin_host . '/hidden-page', + $this->origin_scheme . '://' . $this->origin_host . '/images/secret.jpg' ); ?> +

+
+

+ +

+
+ +
+ +
+ +
diff --git a/views/redirect.phtml b/views/redirect.phtml new file mode 100644 index 0000000..e73953b --- /dev/null +++ b/views/redirect.phtml @@ -0,0 +1,16 @@ + + + + <?php _e( 'Redirecting...', $this->slug ); ?> + + + + + +

You are being redirected to redirect_url; ?>

+ +