├── .gitignore ├── index.php ├── includes ├── index.php ├── load.php ├── tasks │ ├── class-ss-cancel-task.php │ ├── class-ss-task.php │ ├── class-ss-create-zip-archive.php │ ├── class-ss-wrapup-task.php │ ├── class-ss-transfer-files-locally-task.php │ ├── class-ss-setup-task.php │ └── class-ss-fetch-urls-task.php ├── libraries │ ├── wp-background-processing │ │ ├── wp-background-processing.php │ │ ├── classes │ │ │ ├── wp-async-request.php │ │ │ └── wp-background-process.php │ │ └── license.txt │ ├── PhpSimple │ │ └── HtmlDomParser.php │ └── phpuri.php ├── shims.php ├── class-ss-sql-permissions.php ├── class-ss-options.php ├── models │ ├── class-ss-page.php │ └── class-ss-model.php ├── class-ss-view.php ├── class-ss-upgrade-handler.php ├── class-ss-url-fetcher.php ├── class-ss-diagnostic.php ├── class-ss-archive-creation-job.php ├── class-ss-query.php └── class-ss-util.php ├── languages ├── simply-static-de_DE.mo ├── simply-static-fr_FR.mo ├── simply-static-de_DE.po └── simply-static-fr_FR.po ├── views ├── _activity_log.php ├── redirect.php ├── generate.php ├── _pagination.php ├── layouts │ └── admin.php ├── _export_log.php └── diagnostics.php ├── composer.json ├── .editorconfig ├── uninstall.php ├── README.md ├── simply-static.php ├── js ├── admin-settings.js └── admin-generate.js ├── composer.lock └── css └── admin.css /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | status_messages as $state_name => $status ) : ?> 5 |
'>[]
6 | 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grrr-amsterdam/simply-static", 3 | "type": "wordpress-plugin", 4 | "license": "GPL-2.0+", 5 | "description": "Simply Static site generator for wordpress", 6 | "require": { 7 | "php": ">=5.3.3", 8 | "composer/installers": "~1.0" 9 | } 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.{js,jsx,json,yml,ini,rb,cap}] 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /includes/tasks/class-ss-cancel-task.php: -------------------------------------------------------------------------------- 1 | save_status_message( __( 'Cancelling job', 'simply-static' ) ); 13 | 14 | $wrapup_task = new Wrapup_Task(); 15 | $wrapup_task->perform(); 16 | 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /views/redirect.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <?php _e( 'Redirecting...', 'simply-static' ); ?> 5 | 6 | 7 | 8 | 11 | 12 |

redirect_url . '">' . $this->redirect_url . '' ); ?>

13 | 14 | 15 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | 4 | 5 |

6 | 7 |
8 | 9 | 10 | 11 |
12 | ' type='submit' name='generate' value='' /> 13 | 14 | ' type='submit' name='cancel' value='' /> 15 | 16 | '> 17 |
18 | 19 |

20 |
21 | activity_log; ?> 22 |
23 | 24 |

25 |
26 | export_log; ?> 27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /views/_pagination.php: -------------------------------------------------------------------------------- 1 |
2 |
http_status_codes['1']; ?> | 3 | http_status_codes['2']; ?> | 4 | http_status_codes['3']; ?> | 5 | http_status_codes['4']; ?> | 6 | http_status_codes['5']; ?> | 7 | More info on HTTP status codes", 'simply-static' ); ?>
8 |
9 | 10 |
11 | total_static_pages );?> 12 | '?page=%#%', 15 | 'total' => $this->total_pages, 16 | 'current' => $this->current_page, 17 | 'prev_text' => '‹', 18 | 'next_text' => '›' 19 | ); 20 | echo paginate_links( $args ); 21 | ?> 22 |
23 | -------------------------------------------------------------------------------- /includes/shims.php: -------------------------------------------------------------------------------- 1 | options = Options::instance(); 23 | } 24 | 25 | /** 26 | * Add a message to the array of status messages for the job 27 | * 28 | * Providing a unique key for the message is optional. If one isn't 29 | * provided, the state_name will be used. Using the same key more than once 30 | * will overwrite previous messages. 31 | * @param string $message Message to display about the status of the job 32 | * @param string $key Unique key for the message 33 | * @return void 34 | */ 35 | protected function save_status_message( $message, $key = null ) { 36 | $task_name = $key ?: static::$task_name; 37 | $messages = $this->options->get( 'archive_status_messages' ); 38 | Util::debug_log( 'Status message: [' . $task_name . '] ' . $message ); 39 | 40 | $messages = Util::add_archive_status_message( $messages, $task_name, $message ); 41 | 42 | $this->options 43 | ->set( 'archive_status_messages', $messages ) 44 | ->save(); 45 | } 46 | 47 | /* 48 | * Override this method to perform the task action. 49 | * @return boolean|WP_Error true if done, false if not done, WP_Error if error 50 | */ 51 | abstract public function perform(); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /simply-static.php: -------------------------------------------------------------------------------- 1 | Simply Static requires PHP 5.3 or higher, and the plugin has now deactivated itself.', 'simply-static' ) . 27 | '
' . 28 | __( 'Contact your hosting company or your system administrator and ask for an upgrade to version 5.3 of PHP.', 'simply-static' ); 29 | printf( "

%s

", $message ); 30 | exit(); 31 | } 32 | 33 | deactivate_plugins( __FILE__ ); 34 | } 35 | } else { 36 | // Loading up Simply Static in a separate file so that there's nothing to 37 | // trigger a PHP error in this file (e.g. by using namespacing) 38 | require_once plugin_dir_path( __FILE__ ) . 'includes/load.php'; 39 | } 40 | -------------------------------------------------------------------------------- /includes/tasks/class-ss-create-zip-archive.php: -------------------------------------------------------------------------------- 1 | create_zip(); 15 | if ( is_wp_error( $download_url ) ) { 16 | return $download_url; 17 | } else { 18 | $message = __( 'ZIP archive created: ', 'simply-static' ); 19 | $message .= ' ' . __( 'Click here to download', 'simply-static' ) . ''; 20 | $this->save_status_message( $message ); 21 | return true; 22 | } 23 | } 24 | 25 | /** 26 | * Create a ZIP file using the archive directory 27 | * @return string|\WP_Error $temporary_zip The path to the archive zip file 28 | */ 29 | public function create_zip() { 30 | $archive_dir = $this->options->get_archive_dir(); 31 | 32 | $zip_filename = untrailingslashit( $archive_dir ) . '.zip'; 33 | $zip_archive = new \PclZip( $zip_filename ); 34 | 35 | Util::debug_log( "Fetching list of files to include in zip" ); 36 | $files = array(); 37 | $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $archive_dir, \RecursiveDirectoryIterator::SKIP_DOTS ) ); 38 | foreach ( $iterator as $file_name => $file_object ) { 39 | $files[] = realpath( $file_name ); 40 | } 41 | 42 | Util::debug_log( "Creating zip archive" ); 43 | if ( $zip_archive->create( $files, PCLZIP_OPT_REMOVE_PATH, $archive_dir ) === 0 ) { 44 | return new \WP_Error( 'create_zip_failed', __( 'Unable to create ZIP archive', 'simply-static' ) ); 45 | } 46 | 47 | $download_url = get_admin_url( null, 'admin.php' ) . '?' . Plugin::SLUG . '_zip_download=' . basename( $zip_filename ); 48 | 49 | return $download_url; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /includes/tasks/class-ss-wrapup-task.php: -------------------------------------------------------------------------------- 1 | options->get( 'delete_temp_files' ) === '1' ) { 13 | Util::debug_log( "Deleting temporary files" ); 14 | $this->save_status_message( __( 'Wrapping up', 'simply-static' ) ); 15 | $deleted_successfully = $this->delete_temp_static_files(); 16 | } else { 17 | Util::debug_log( "Keeping temporary files" ); 18 | } 19 | 20 | return true; 21 | } 22 | 23 | /** 24 | * Delete temporary, generated static files 25 | * @return true|\WP_Error True on success, WP_Error otherwise 26 | */ 27 | public function delete_temp_static_files() { 28 | $archive_dir = $this->options->get_archive_dir(); 29 | 30 | if ( file_exists( $archive_dir ) ) { 31 | $directory_iterator = new \RecursiveDirectoryIterator( $archive_dir, \FilesystemIterator::SKIP_DOTS ); 32 | $recursive_iterator = new \RecursiveIteratorIterator( $directory_iterator, \RecursiveIteratorIterator::CHILD_FIRST ); 33 | 34 | // recurse through the entire directory and delete all files / subdirectories 35 | foreach ( $recursive_iterator as $item ) { 36 | $success = $item->isDir() ? rmdir( $item ) : unlink( $item ); 37 | if ( ! $success ) { 38 | $message = sprintf( __( "Could not delete temporary file or directory: %s", 'simply-static' ), $item ); 39 | $this->save_status_message( $message ); 40 | return true; 41 | } 42 | } 43 | 44 | // must make sure to delete the original directory at the end 45 | $success = rmdir( $archive_dir ); 46 | if ( ! $success ) { 47 | $message = sprintf( __( "Could not delete temporary file or directory: %s", 'simply-static' ), $archive_dir ); 48 | $this->save_status_message( $message ); 49 | return true; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /views/layouts/admin.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | flashes as $flash ) : ?> 6 |
7 |

8 | 9 |

10 |
11 | 12 | 13 |
14 |
15 | 16 |
17 | template; ?> 18 |
19 | 20 | 21 |
22 | 46 |
47 | 48 | 49 |
50 | 51 |
52 | 53 | -------------------------------------------------------------------------------- /views/_export_log.php: -------------------------------------------------------------------------------- 1 | static_pages ) && count( $this->static_pages ) ) : ?> 5 | 6 | static_pages, function($p) { return $p->error_message != false; } ) ); ?> 7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 ) : ?> 19 | 20 | 21 | 22 | 23 | 24 | 25 | static_pages as $static_page ) : ?> 26 | 27 | http_status_code, Page::$processable_status_codes ); ?> 28 | 31 | 32 | 47 | 0 ) : ?> 48 | 51 | 52 | 53 | 54 | 55 |
'> 29 | http_status_code; ?> 30 | url; ?> 33 | parent_static_page(); 36 | if ( $parent_static_page ) { 37 | $display_url = Util::get_path_from_local_url( $parent_static_page->url ); 38 | $msg .= "" .sprintf( __( 'Found on %s', 'simply-static' ), $display_url ). ""; 39 | } 40 | if ( $msg !== '' && $static_page->status_message ) { 41 | $msg .= '; '; 42 | } 43 | $msg .= $static_page->status_message; 44 | echo $msg; 45 | ?> 46 | 49 | error_message; ?> 50 |
56 | 57 |
58 | 59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /includes/class-ss-sql-permissions.php: -------------------------------------------------------------------------------- 1 | false, 28 | 'update' => false, 29 | 'insert' => false, 30 | 'delete' => false, 31 | 'alter' => false, 32 | 'create' => false, 33 | 'drop' => false 34 | ); 35 | 36 | /** 37 | * Disable usage of "new" 38 | * @return void 39 | */ 40 | protected function __construct() {} 41 | 42 | /** 43 | * Disable cloning of the class 44 | * @return void 45 | */ 46 | protected function __clone() {} 47 | 48 | /** 49 | * Disable unserializing of the class 50 | * @return void 51 | */ 52 | public function __wakeup() {} 53 | 54 | /** 55 | * Return an instance of Simply_Static\Sql_Permissions 56 | * @return Simply_Static\Sql_Permissions 57 | */ 58 | public static function instance() { 59 | if ( null === self::$instance ) { 60 | self::$instance = new self(); 61 | 62 | global $wpdb; 63 | $rows = $wpdb->get_results( 'SHOW GRANTS FOR current_user()', ARRAY_N ); 64 | 65 | // Loop through all of the grants and set permissions to true where 66 | // we're able to find them. 67 | foreach ( $rows as $row ) { 68 | // Find the database name 69 | preg_match( '/GRANT (.+) ON (.+) TO/', $row[0], $matches ); 70 | // Removing backticks and backslashes for easier matching 71 | $db_name = preg_replace('/[\\\`]/', '', $matches[2]); 72 | 73 | if ( substr( $db_name, -3 ) == '%.*' ) { 74 | // Check for a wildcard match on the database 75 | $db_name = substr( $db_name, 0, -3 ); 76 | $db_name_match = ( stripos( $wpdb->dbname, $db_name ) === 0 ); 77 | } else { 78 | // Check for matches for all dbs (*.*) or this specific WP db 79 | $db_name_match = in_array( $db_name, array( '*.*', $wpdb->dbname . '.*' ) ); 80 | } 81 | 82 | if ( $db_name_match ) { 83 | foreach ( explode( ',', $matches[1] ) as $permission ) { 84 | $permission = str_replace( ' ', '_', trim( strtolower( $permission ) ) ); 85 | if ( $permission === 'all_privileges' ) { 86 | foreach ( self::$instance->permissions as $key => $value ) { 87 | self::$instance->permissions[ $key ] = true; 88 | } 89 | } 90 | self::$instance->permissions[ $permission ] = true; 91 | } 92 | } 93 | } 94 | } 95 | 96 | return self::$instance; 97 | } 98 | 99 | /** 100 | * Check if the MySQL user is able to perform the provided permission 101 | */ 102 | public function can( $permission ) { 103 | return ( isset( $this->permissions[ $permission ] ) && $this->permissions[ $permission ] === true ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /includes/class-ss-options.php: -------------------------------------------------------------------------------- 1 | options = $options; 58 | } 59 | 60 | return self::$instance; 61 | } 62 | 63 | /** 64 | * Return a fresh instance of Simply_Static\Options 65 | * @return Simply_Static 66 | */ 67 | public static function reinstance() { 68 | self::$instance = null; 69 | return self::instance(); 70 | } 71 | 72 | /** 73 | * Updates the option identified by $name with the value provided in $value 74 | * @param string $name The option name 75 | * @param mixed $value The option value 76 | * @return Simply_Static\Options 77 | */ 78 | public function set( $name, $value ) { 79 | $this->options[ $name ] = $value; 80 | return $this; 81 | } 82 | 83 | /** 84 | * Returns a value of the option identified by $name 85 | * @param string $name The option name 86 | * @return mixed|null 87 | */ 88 | public function get( $name ) { 89 | return array_key_exists( $name, $this->options ) ? $this->options[ $name ] : null; 90 | } 91 | 92 | /** 93 | * Destroy an option 94 | * @param string $name The option name to destroy 95 | * @return boolean true if the key existed, false if it didn't 96 | */ 97 | public function destroy( $name ) { 98 | if ( array_key_exists( $name, $this->options ) ) { 99 | unset( $this->options[ $name] ); 100 | return true; 101 | } else { 102 | return false; 103 | } 104 | } 105 | 106 | /** 107 | * Returns all options as an array 108 | * @return array 109 | */ 110 | public function get_as_array() { 111 | return $this->options; 112 | } 113 | 114 | /** 115 | * Saves the internal options data to the wp_options table 116 | * @return boolean 117 | */ 118 | public function save() { 119 | return update_option( Plugin::SLUG, $this->options ); 120 | } 121 | 122 | /** 123 | * Get the current path to the temp static archive directory 124 | * @return string The path to the temp static archive directory 125 | */ 126 | public function get_archive_dir() { 127 | return Util::add_trailing_directory_separator( $this->get( 'temp_files_dir' ) . $this->get( 'archive_name' ) ); 128 | } 129 | 130 | /** 131 | * Get the destination URL (scheme + host) 132 | * @return string The destination URL 133 | */ 134 | public function get_destination_url() { 135 | return $this->get( 'destination_scheme' ) . $this->get( 'destination_host' ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /includes/libraries/wp-background-processing/classes/wp-async-request.php: -------------------------------------------------------------------------------- 1 | identifier = $this->prefix . '_' . $this->action; 60 | 61 | add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); 62 | add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); 63 | } 64 | 65 | /** 66 | * Set data used during the request 67 | * 68 | * @param array $data Data. 69 | * 70 | * @return $this 71 | */ 72 | public function data( $data ) { 73 | $this->data = $data; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Dispatch the async request 80 | * 81 | * @return array|WP_Error 82 | */ 83 | public function dispatch() { 84 | $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); 85 | $args = $this->get_post_args(); 86 | 87 | return wp_remote_post( esc_url_raw( $url ), $args ); 88 | } 89 | 90 | /** 91 | * Get query args 92 | * 93 | * @return array 94 | */ 95 | protected function get_query_args() { 96 | if ( property_exists( $this, 'query_args' ) ) { 97 | return $this->query_args; 98 | } 99 | 100 | return array( 101 | 'action' => $this->identifier, 102 | 'nonce' => wp_create_nonce( $this->identifier ), 103 | ); 104 | } 105 | 106 | /** 107 | * Get query URL 108 | * 109 | * @return string 110 | */ 111 | protected function get_query_url() { 112 | if ( property_exists( $this, 'query_url' ) ) { 113 | return $this->query_url; 114 | } 115 | 116 | return admin_url( 'admin-ajax.php' ); 117 | } 118 | 119 | /** 120 | * Get post args 121 | * 122 | * @return array 123 | */ 124 | protected function get_post_args() { 125 | if ( property_exists( $this, 'post_args' ) ) { 126 | return $this->post_args; 127 | } 128 | 129 | return array( 130 | 'timeout' => 0.01, 131 | 'blocking' => false, 132 | 'body' => $this->data, 133 | 'cookies' => $_COOKIE, 134 | 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 135 | ); 136 | } 137 | 138 | /** 139 | * Maybe handle 140 | * 141 | * Check for correct nonce and pass to handler. 142 | */ 143 | public function maybe_handle() { 144 | // Don't lock up other requests while processing 145 | session_write_close(); 146 | 147 | check_ajax_referer( $this->identifier, 'nonce' ); 148 | 149 | $this->handle(); 150 | 151 | wp_die(); 152 | } 153 | 154 | /** 155 | * Handle 156 | * 157 | * Override this method to perform any actions required 158 | * during the async request. 159 | */ 160 | abstract protected function handle(); 161 | 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /includes/tasks/class-ss-transfer-files-locally-task.php: -------------------------------------------------------------------------------- 1 | options->get( 'local_dir' ); 18 | 19 | list( $pages_processed, $total_pages ) = $this->copy_static_files( $local_dir ); 20 | 21 | if ( $pages_processed !== 0 ) { 22 | $message = sprintf( __( "Copied %d of %d files", 'simply-static' ), $pages_processed, $total_pages ); 23 | $this->save_status_message( $message ); 24 | } 25 | 26 | if ( $pages_processed >= $total_pages ) { 27 | if ( $this->options->get( 'destination_url_type' ) == 'absolute' ) { 28 | $destination_url = trailingslashit( $this->options->get_destination_url() ); 29 | $message = __( 'Destination URL:', 'simply-static' ) . ' ' . $destination_url . ''; 30 | $this->save_status_message( $message, 'destination_url' ); 31 | } 32 | } 33 | 34 | // return true when done (no more pages) 35 | return $pages_processed >= $total_pages; 36 | 37 | } 38 | 39 | /** 40 | * Copy temporary static files to a local directory 41 | * @param string $destination_dir The directory to put the files 42 | * @return array (# pages processed, # pages remaining) 43 | */ 44 | public function copy_static_files( $destination_dir ) { 45 | $batch_size = 100; 46 | 47 | $archive_dir = $this->options->get_archive_dir(); 48 | $archive_start_time = $this->options->get( 'archive_start_time' ); 49 | 50 | // TODO: also check for recent modification time 51 | // last_modified_at > ? AND 52 | $static_pages = Page::query() 53 | ->where( "file_path IS NOT NULL" ) 54 | ->where( "file_path != ''" ) 55 | ->where( "( last_transferred_at < ? OR last_transferred_at IS NULL )", $archive_start_time ) 56 | ->limit( $batch_size ) 57 | ->find(); 58 | $pages_remaining = count( $static_pages ); 59 | $total_pages = Page::query() 60 | ->where( "file_path IS NOT NULL" ) 61 | ->where( "file_path != ''" ) 62 | ->count(); 63 | $pages_processed = $total_pages - $pages_remaining; 64 | Util::debug_log( "Total pages: " . $total_pages . '; Pages remaining: ' . $pages_remaining ); 65 | 66 | while ( $static_page = array_shift( $static_pages ) ) { 67 | $path_info = Util::url_path_info( $static_page->file_path ); 68 | $path = $destination_dir . $path_info['dirname']; 69 | $create_dir = wp_mkdir_p( $path ); 70 | if ( $create_dir === false ) { 71 | Util::debug_log( "Cannot create directory: " . $destination_dir . $path_info['dirname'] ); 72 | $static_page->set_error_message( 'Unable to create destination directory' ); 73 | } else { 74 | chmod( $path, 0755 ); 75 | $origin_file_path = $archive_dir . $static_page->file_path; 76 | $destination_file_path = $destination_dir . $static_page->file_path; 77 | 78 | // check that destination file doesn't exist OR exists but is writeable 79 | if ( ! file_exists( $destination_file_path ) || is_writable( $destination_file_path ) ) { 80 | $copy = copy( $origin_file_path, $destination_file_path ); 81 | if ( $copy === false ) { 82 | Util::debug_log( "Cannot copy " . $origin_file_path . " to " . $destination_file_path ); 83 | $static_page->set_error_message( 'Unable to copy file to destination' ); 84 | } 85 | } else { 86 | Util::debug_log( "File exists and is unwriteable: " . $destination_file_path ); 87 | $static_page->set_error_message( 'Destination file exists and is unwriteable' ); 88 | } 89 | } 90 | 91 | $static_page->last_transferred_at = Util::formatted_datetime(); 92 | $static_page->save(); 93 | } 94 | 95 | return array( $pages_processed, $total_pages ); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /js/admin-settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | jQuery( document ).ready( function( $ ) { 3 | // show / hide tabs: 4 | $( '#sistContainer #sistTabs' ).find( 'a' ).click( function() { 5 | $( '#sistContainer #sistTabs' ).find( 'a' ).removeClass( 'nav-tab-active' ); 6 | $( '#sistContainer .tab-pane' ).removeClass( 'active' ); 7 | 8 | var id = $( this ).attr( 'id' ).replace( '-tab', '' ); 9 | $( '#sistContainer #' + id ).addClass( 'active' ); 10 | $( this ).addClass( 'nav-tab-active' ); 11 | } ); 12 | 13 | // set active tab on page load: 14 | var activeTab = window.location.hash.replace( '#tab-', '' ); 15 | 16 | // if no tab hash, default to the first tab 17 | if ( activeTab === '' ) { 18 | activeTab = $( '#sistContainer .tab-pane' ).attr( 'id' ); 19 | } 20 | 21 | $( '#sistContainer #' + activeTab ).addClass( 'active' ); 22 | $( '#sistContainer #' + activeTab + '-tab' ).addClass( 'nav-tab-active' ); 23 | 24 | // pretend the user clicked on the active tab 25 | $( '#sistContainer .nav-tab-active' ).click(); 26 | 27 | // ---------------------------------------------------------------------- // 28 | 29 | // delivery method selection: 30 | $( '#sistContainer #deliveryMethod' ).change( function() { 31 | var selected = $( this ).val(); 32 | $( '#sistContainer .delivery-method' ).removeClass( 'active' ); 33 | $( '#sistContainer .' + selected + '.delivery-method' ).addClass( 'active '); 34 | } ); 35 | 36 | // pretend the user selected a value 37 | $( '#sistContainer #deliveryMethod' ).change(); 38 | 39 | // ---------------------------------------------------------------------- // 40 | 41 | $( 'td.url-dest-option' ).click( function() { 42 | destination_url_type_change( $( this ) ); 43 | } ); 44 | 45 | $( '#sistContainer input[type=radio][name=destination_url_type]' ).change( function() { 46 | destination_url_type_change( $( this ).closest( 'td.url-dest-option' ) ); 47 | } ); 48 | 49 | // pretend the user selected a value on page load 50 | $( '#sistContainer input[type=radio][name=destination_url_type]:checked' ).change(); 51 | 52 | function destination_url_type_change( $this ) { 53 | $( 'td.url-dest-option' ).removeClass( 'active' ); 54 | $this.addClass( 'active' ); 55 | var $radio = $this.find( 'input[type=radio][name=destination_url_type]' ); 56 | $radio.prop( 'checked', true ); 57 | 58 | if ( $radio.val() == 'absolute' ) { 59 | $( '#destinationHost' ) 60 | .prop( 'disabled', false ); 61 | $( '#destinationScheme' ) 62 | .prop( 'disabled', false ); 63 | } else { 64 | $( '#destinationHost' ) 65 | .val('') 66 | .prop( 'disabled', true ); 67 | $( '#destinationScheme' ) 68 | .prop( 'disabled', true ) 69 | } 70 | 71 | if ( $radio.val() == 'relative' ) { 72 | $( '#relativePath' ) 73 | .prop( 'disabled', false ); 74 | } else { 75 | $( '#relativePath' ) 76 | .val('') 77 | .prop( 'disabled', true ); 78 | } 79 | } 80 | 81 | // ---------------------------------------------------------------------- // 82 | 83 | $( '#AddUrlToExclude' ).click( function() { 84 | var $last_row = $( '.excludable-url-row' ).last(); 85 | var $clone_row = $( '#excludableUrlRowTemplate' ).clone().removeAttr( 'id' ); 86 | 87 | var timestamp = new Date().getTime(); 88 | var regex = /excludable\[0\]/g; 89 | 90 | $clone_row.html( $clone_row.html().replace( regex, 'excludable[' + timestamp + ']' ) ); 91 | $clone_row.insertAfter( $last_row ); 92 | } ); 93 | 94 | $( '#excludableUrlRows' ).on( 'click', '.remove-excludable-url-row', function() { 95 | var $row = $( this ).closest( '.excludable-url-row' ); 96 | $row.remove(); 97 | } ); 98 | 99 | // ---------------------------------------------------------------------- // 100 | 101 | $( '#basicAuthCredentialsSaved > a' ).click( function(e) { 102 | e.preventDefault(); 103 | $( '#basicAuthSet' ).addClass( 'hide' ); 104 | $( '#basicAuthUserPass').removeClass( 'hide' ).find( 'input' ).prop( 'disabled', false ); 105 | }); 106 | } ); 107 | -------------------------------------------------------------------------------- /js/admin-generate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | jQuery( document ).ready( function( $ ) { 3 | var REFRESH_EVERY_X_SECONDS = 2; 4 | var STATIC_PAGES_PER_PAGE = 50; // max number of pages to show at once 5 | var done = true; 6 | var refreshTimer = null; 7 | 8 | // display the export and activity log on page load 9 | display_export_log(); 10 | display_activity_log(); 11 | initiate_action(); 12 | 13 | $( '#sistContainer #generate' ).click( function( e ) { 14 | $( '#sistContainer #activityLog' ).html(''); 15 | initiate_action( 'start' ); 16 | } ); 17 | 18 | $( '#sistContainer #cancel' ).click( function( e ) { 19 | initiate_action( 'cancel' ); 20 | } ); 21 | 22 | // disable all actions and show spinner 23 | function initiate_action( action ) { 24 | if ( action == null ) { 25 | action = 'ping'; 26 | } else { 27 | $( '#sistContainer .actions input' ).attr( 'disabled', 'disabled' ); 28 | $( '#sistContainer .actions .spinner' ).addClass( 'is-active' ); 29 | } 30 | 31 | // cancel existing timer 32 | if ( refreshTimer != null ) { 33 | clearInterval( refreshTimer ); 34 | } 35 | // send action now 36 | send_action_to_archive_manager( action ); 37 | // set loop for pinging server 38 | refreshTimer = setInterval( function() { 39 | send_action_to_archive_manager( 'ping' ); 40 | }, REFRESH_EVERY_X_SECONDS * 1000 ); 41 | } 42 | 43 | // where action is one of 'start', 'continue', 'cancel' 44 | function send_action_to_archive_manager( action ) { 45 | var data = { 46 | '_ajax_nonce': $('#_wpnonce').val(), 47 | 'action': 'static_archive_action', 48 | 'perform': action 49 | }; 50 | 51 | $.post( window.ajaxurl, data, function( response ) { 52 | handle_response_from_archive_manager( response ); 53 | } ); 54 | } 55 | 56 | function handle_response_from_archive_manager( response ) { 57 | // loop through the responses and create an .activity div for each one 58 | // in #activityLog 59 | var $activityLog = $( '#activityLog' ); 60 | $activityLog.html( response.activity_log_html ) 61 | .scrollTop( $activityLog.prop( 'scrollHeight' ) ); 62 | if ( response.done == true && done == false ) { 63 | display_export_log(); 64 | } 65 | 66 | done = response.done; 67 | 68 | // only adjust the button/spinner state on a 'ping' 69 | // (ensures that the job has had time to process the action) 70 | if ( response.action == 'ping' ) { 71 | // re-enable and hide all actions 72 | $( '#sistContainer .actions input' ) 73 | .removeAttr( 'disabled' ) 74 | .addClass( 'hide' ); 75 | 76 | if ( done == true ) { 77 | // remove spinner and show #generate 78 | $( '#sistContainer .actions .spinner' ).removeClass( 'is-active' ); 79 | $( '#sistContainer #generate' ).removeClass( 'hide' ); 80 | } else { 81 | $( '#sistContainer #cancel' ).removeClass( 'hide' ); 82 | } 83 | } 84 | } 85 | 86 | function display_export_log() { 87 | var data = { 88 | '_ajax_nonce': $('#_wpnonce').val(), 89 | 'action': 'render_export_log', 90 | 'page': 1, 91 | 'per_page': STATIC_PAGES_PER_PAGE 92 | }; 93 | 94 | var $exportLog = $( '#exportLog' ); 95 | $exportLog.html( "" ); 96 | 97 | $.post( window.ajaxurl, data, function( response ) { 98 | $exportLog.html( response.html ); 99 | } ); 100 | } 101 | 102 | function display_activity_log() { 103 | var data = { 104 | '_ajax_nonce': $('#_wpnonce').val(), 105 | 'action': 'render_activity_log' 106 | }; 107 | 108 | var $activityLog = $( '#activityLog' ); 109 | $activityLog.html( "" ); 110 | 111 | $.post( window.ajaxurl, data, function( response ) { 112 | $activityLog.html( response.html ) 113 | .scrollTop( $activityLog.prop( 'scrollHeight' ) ); 114 | } ); 115 | } 116 | 117 | // -- AJAX pagination ----------------------------------------------------// 118 | $( '#sistContainer #exportLog' ).on( 'click', 'a.page-numbers', function( e ) { 119 | e.preventDefault(); 120 | 121 | var url = $( this ).attr( 'href' ); 122 | var re = /page=(\d+)/; 123 | var matches = re.exec( url ); 124 | 125 | var page = 1; 126 | if ( matches ) { 127 | page = matches[1]; 128 | } 129 | 130 | var data = { 131 | '_ajax_nonce': $('#_wpnonce').val(), 132 | 'action': 'render_export_log', 133 | 'page': page, 134 | 'per_page': STATIC_PAGES_PER_PAGE 135 | }; 136 | 137 | $.post( window.ajaxurl, data, function( response ) { 138 | $( '#exportLog' ).html( response.html ); 139 | } ); 140 | } ); 141 | 142 | } ); 143 | -------------------------------------------------------------------------------- /includes/models/class-ss-page.php: -------------------------------------------------------------------------------- 1 | 'BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT', 25 | 'found_on_id' => 'BIGINT(20) UNSIGNED NULL', 26 | 'url' => 'VARCHAR(255) NOT NULL', 27 | 'redirect_url' => 'TEXT NULL', 28 | 'file_path' => 'VARCHAR(255) NULL', 29 | 'http_status_code' => 'SMALLINT(20) NULL', 30 | 'content_type' => 'VARCHAR(255) NULL', 31 | 'content_hash' => 'BINARY(20) NULL', 32 | 'error_message' => 'VARCHAR(255) NULL', 33 | 'status_message' => 'VARCHAR(255) NULL', 34 | 'last_checked_at' => "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'", 35 | 'last_modified_at' => "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'", 36 | 'last_transferred_at' => "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'", 37 | 'created_at' => "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'", 38 | 'updated_at' => "DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'" 39 | ); 40 | 41 | /** @const */ 42 | protected static $indexes = array( 43 | 'PRIMARY KEY (id)', 44 | 'KEY url (url)', 45 | 'KEY last_checked_at (last_checked_at)', 46 | 'KEY last_modified_at (last_modified_at)', 47 | 'KEY last_transferred_at (last_transferred_at)' 48 | ); 49 | 50 | /** @const */ 51 | protected static $primary_key = 'id'; 52 | 53 | /** 54 | * Get the number of pages for each group of status codes, e.g. 1xx, 2xx, 3xx 55 | * @return array Assoc. array of status code to number of pages, e.g. '2' => 183 56 | */ 57 | public static function get_http_status_codes_summary() { 58 | global $wpdb; 59 | 60 | $query = 'SELECT LEFT(http_status_code, 1) AS status, COUNT(*) AS count'; 61 | $query .= ' FROM ' . self::table_name(); 62 | $query .= ' GROUP BY LEFT(http_status_code, 1)'; 63 | $query .= ' ORDER BY status'; 64 | 65 | $rows = $wpdb->get_results( 66 | $query, 67 | ARRAY_A 68 | ); 69 | 70 | $http_codes = array( '1' => 0, '2' => 0, '3' => 0, '4' => 0, '5' => 0 ); 71 | foreach ( $rows as $row ) { 72 | $http_codes[ $row['status'] ] = $row['count']; 73 | } 74 | 75 | return $http_codes; 76 | } 77 | 78 | /** 79 | * Return the static page that this page belongs to (if any) 80 | * @return Page The parent Page 81 | */ 82 | public function parent_static_page() { 83 | return self::query()->find_by( 'id', $this->found_on_id ); 84 | } 85 | 86 | /** 87 | * Check if the hash for the content matches the prior hash for the page 88 | * @param string $content The content of the page/file 89 | * @return boolean Is the hash a match? 90 | */ 91 | public function is_content_identical( $sha1 ) { 92 | return $sha1 === $this->content_hash; 93 | } 94 | 95 | /** 96 | * Set the hash for the content and update the last_modified_at value 97 | * @param string $content The content of the page/file 98 | */ 99 | public function set_content_hash( $sha1 ) { 100 | $this->content_hash = $sha1; 101 | $this->last_modified_at = Util::formatted_datetime(); 102 | } 103 | 104 | /** 105 | * Set an error message 106 | * 107 | * An error indicates that something bad happened when fetching the page, or 108 | * saving the page, or during some other activity related to the page. 109 | * @param string $message The error message 110 | */ 111 | public function set_error_message( $message ) { 112 | if ( $this->error_message ) { 113 | $this->error_message = $this->error_message . '; ' . $message; 114 | } else { 115 | $this->error_message = $message; 116 | } 117 | } 118 | 119 | /** 120 | * Set a status message 121 | * 122 | * A status message is used to indicate things that happened to the page 123 | * that weren't errors, such as not following links or not saving the page. 124 | * @param string $message The status message 125 | */ 126 | public function set_status_message( $message ) { 127 | if ( $this->status_message ) { 128 | $this->status_message = $this->status_message . '; ' . $message; 129 | } else { 130 | $this->status_message = $message; 131 | } 132 | } 133 | 134 | public function is_type( $content_type ) { 135 | return stripos( $this->content_type, $content_type ) !== false; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "481f3002b19ca02bb34a43ea8c42beef", 8 | "packages": [ 9 | { 10 | "name": "composer/installers", 11 | "version": "v1.7.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/composer/installers.git", 15 | "reference": "141b272484481432cda342727a427dc1e206bfa0" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/composer/installers/zipball/141b272484481432cda342727a427dc1e206bfa0", 20 | "reference": "141b272484481432cda342727a427dc1e206bfa0", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "composer-plugin-api": "^1.0" 25 | }, 26 | "replace": { 27 | "roundcube/plugin-installer": "*", 28 | "shama/baton": "*" 29 | }, 30 | "require-dev": { 31 | "composer/composer": "1.0.*@dev", 32 | "phpunit/phpunit": "^4.8.36" 33 | }, 34 | "type": "composer-plugin", 35 | "extra": { 36 | "class": "Composer\\Installers\\Plugin", 37 | "branch-alias": { 38 | "dev-master": "1.0-dev" 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Composer\\Installers\\": "src/Composer/Installers" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Kyle Robinson Young", 53 | "email": "kyle@dontkry.com", 54 | "homepage": "https://github.com/shama" 55 | } 56 | ], 57 | "description": "A multi-framework Composer library installer", 58 | "homepage": "https://composer.github.io/installers/", 59 | "keywords": [ 60 | "Craft", 61 | "Dolibarr", 62 | "Eliasis", 63 | "Hurad", 64 | "ImageCMS", 65 | "Kanboard", 66 | "Lan Management System", 67 | "MODX Evo", 68 | "Mautic", 69 | "Maya", 70 | "OXID", 71 | "Plentymarkets", 72 | "Porto", 73 | "RadPHP", 74 | "SMF", 75 | "Thelia", 76 | "Whmcs", 77 | "WolfCMS", 78 | "agl", 79 | "aimeos", 80 | "annotatecms", 81 | "attogram", 82 | "bitrix", 83 | "cakephp", 84 | "chef", 85 | "cockpit", 86 | "codeigniter", 87 | "concrete5", 88 | "croogo", 89 | "dokuwiki", 90 | "drupal", 91 | "eZ Platform", 92 | "elgg", 93 | "expressionengine", 94 | "fuelphp", 95 | "grav", 96 | "installer", 97 | "itop", 98 | "joomla", 99 | "known", 100 | "kohana", 101 | "laravel", 102 | "lavalite", 103 | "lithium", 104 | "magento", 105 | "majima", 106 | "mako", 107 | "mediawiki", 108 | "modulework", 109 | "modx", 110 | "moodle", 111 | "osclass", 112 | "phpbb", 113 | "piwik", 114 | "ppi", 115 | "puppet", 116 | "pxcms", 117 | "reindex", 118 | "roundcube", 119 | "shopware", 120 | "silverstripe", 121 | "sydes", 122 | "symfony", 123 | "typo3", 124 | "wordpress", 125 | "yawik", 126 | "zend", 127 | "zikula" 128 | ], 129 | "time": "2019-08-12T15:00:31+00:00" 130 | } 131 | ], 132 | "packages-dev": [], 133 | "aliases": [], 134 | "minimum-stability": "stable", 135 | "stability-flags": [], 136 | "prefer-stable": false, 137 | "prefer-lowest": false, 138 | "platform": { 139 | "php": ">=5.3.3" 140 | }, 141 | "platform-dev": [] 142 | } 143 | -------------------------------------------------------------------------------- /css/admin.css: -------------------------------------------------------------------------------- 1 | #sistContainer { 2 | /*margin-right: 310px;*/ 3 | } 4 | 5 | #sistContainer #sistContent { 6 | width: 100%; 7 | float: left; 8 | max-width: calc( 100% - 300px ); 9 | } 10 | 11 | #sistContainer #sistSidebar { 12 | float: left; 13 | width: 280px; 14 | } 15 | 16 | /* Generate page */ 17 | 18 | #sistContainer #activityLog { 19 | border: 1px solid #000; 20 | background-color: #EFF; 21 | color: #000; 22 | display: block; 23 | height: 100px; 24 | overflow: auto; 25 | padding: 5px; 26 | } 27 | 28 | #sistContainer #activityLog .error-state { 29 | color: #dc143c; 30 | } 31 | 32 | #sistContainer .spinner { 33 | float: none; 34 | } 35 | 36 | #sistContainer .actions .spinner { 37 | margin-top: 12px; 38 | } 39 | 40 | /* Adding this to ensure table fits within parent div */ 41 | #sistContainer #exportLog table { 42 | word-break: break-all; 43 | } 44 | 45 | #sistContainer #exportLog td.status-code { 46 | min-width: 35px; 47 | } 48 | 49 | #sistContainer #exportLog td.status-code.unprocessable { 50 | font-weight: bold; 51 | color: #000; 52 | } 53 | 54 | #sistContainer .hide { 55 | display: none; 56 | } 57 | 58 | #sistContainer .button.button-destroy { 59 | color: #fff; 60 | background-color: #c9302c; 61 | border-color: #ac2925; 62 | } 63 | 64 | #sistContainer .button.button-destroy:hover { 65 | background-color: #d9534f; 66 | border-color: #d43f3a; 67 | } 68 | 69 | #sistContainer .tablenav .page-numbers { 70 | padding: 6px 4px; 71 | font-size: 13px; 72 | } 73 | 74 | #sistContainer .tablenav .page-numbers.prev, #sistContainer .tablenav .page-numbers.next { 75 | padding: 3px 4px 6px; 76 | font-size: 16px; 77 | } 78 | 79 | #sistContainer .tablenav .page-numbers.current { 80 | display: inline-block; 81 | min-width: 17px; 82 | border: 1px solid #ccc; 83 | padding: 6px 4px; 84 | background: #e5e5e5; 85 | font-size: 13px; 86 | line-height: 1; 87 | font-weight: 400; 88 | text-align: center; 89 | } 90 | 91 | #sistContainer .tablenav .http-status { 92 | margin-top: 3px; 93 | line-height: 1.4em; 94 | } 95 | 96 | /* Settings page */ 97 | 98 | #sistContainer .tab-pane, tr.delivery-method { 99 | display: none; 100 | } 101 | 102 | #sistContainer .tab-pane.active, tr.delivery-method.active { 103 | display: table-row; 104 | } 105 | 106 | #sistContainer select:disabled, select.disabled { 107 | border-color: rgba(222,222,222,.75); 108 | background: rgba(255,255,255,.5); 109 | color: rgba(51,51,51,.5); 110 | } 111 | 112 | #sistContainer .help-block { 113 | display: block; 114 | margin-top: 5px; 115 | margin-bottom: 10px; 116 | color: #737373; 117 | } 118 | 119 | #sistContainer .scheme-entry { 120 | height: 28px; 121 | margin-right: 0; 122 | vertical-align: bottom; 123 | border-right: 0; 124 | } 125 | 126 | #sistContainer .host-entry { 127 | height: 28px; 128 | width: 300px; 129 | margin-left: 0; 130 | padding: 2px 5px; 131 | vertical-align: bottom; 132 | } 133 | 134 | #sistContainer .url-dest-option { 135 | border: 1px solid #f1f1f1; 136 | display: table; 137 | cursor: pointer; 138 | width: 100%; 139 | } 140 | 141 | #sistContainer .url-dest-option:hover { 142 | background-color: #ececec; 143 | border: 1px solid #ddd; 144 | } 145 | 146 | #sistContainer .url-dest-option.active { 147 | background-color: #e5e5e5; 148 | border: 1px solid #ccc; 149 | } 150 | 151 | #sistContainer .url-dest-option > span { 152 | display: table-cell; 153 | vertical-align: middle; 154 | padding: 15px 9px; 155 | } 156 | 157 | #sistContainer .url-dest-option > span:first-child { 158 | width: 10px; 159 | } 160 | 161 | #sistContainer #excludableUrlRowTemplate { 162 | display: none; 163 | } 164 | 165 | #sistContainer .excludable-url-row input[type='checkbox'], 166 | #sistContainer .excludable-url-row .button { 167 | margin-left: 10px; 168 | } 169 | 170 | /* Diagnostics page */ 171 | 172 | #sistContainer #diagnosticsPage table.striped { 173 | margin-bottom: 20px; 174 | } 175 | 176 | #sistContainer #diagnosticsPage td.label { 177 | width: 75%; 178 | } 179 | 180 | #sistContainer #diagnosticsPage td.test.success, #sistContainer #diagnosticsPage td.enabled { 181 | font-weight: bold; 182 | width: 25%; 183 | color: #008000; 184 | } 185 | 186 | #sistContainer #diagnosticsPage td.test.success::before { 187 | content: "\2713 "; 188 | } 189 | 190 | #sistContainer #diagnosticsPage td.test.error { 191 | font-weight: bold; 192 | width: 25%; 193 | color: #dc143c; 194 | } 195 | 196 | #sistContainer #diagnosticsPage td.disabled { 197 | width: 25%; 198 | } 199 | 200 | #sistContainer #diagnosticsPage td.test.error::before { 201 | content: "\2717 "; 202 | } 203 | 204 | /* Mailchimp Form CSS */ 205 | 206 | /* Mailchimp original CSS at: 207 | http://cdn-images.mailchimp.com/embedcode/slim-081711.css 208 | */ 209 | #sistContainer #mc_embed_signup input {border:1px solid #999; -webkit-appearance:none;} 210 | #sistContainer #mc_embed_signup input:focus {border-color:#333;} 211 | #sistContainer #mc_embed_signup .clear {clear:none; display:inline;} 212 | #sistContainer #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%;} 213 | -------------------------------------------------------------------------------- /includes/class-ss-view.php: -------------------------------------------------------------------------------- 1 | path = implode( '/', $path_array ); 61 | } 62 | 63 | /** 64 | * Sets a layout that will be used later in render() method 65 | * @param string $template The template filename, without extension 66 | * @return Simply_Static\View 67 | */ 68 | public function set_layout( $layout ) { 69 | $this->layout = trailingslashit( $this->path ) . 'layouts/' . $layout . self::EXTENSION; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Sets a template that will be used later in render() method 76 | * @param string $template The template filename, without extension 77 | * @return Simply_Static\View 78 | */ 79 | public function set_template( $template ) { 80 | $this->template = trailingslashit( $this->path ) . $template . self::EXTENSION; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Returns a value of the option identified by $name 87 | * @param string $name The option name 88 | * @return mixed|null 89 | */ 90 | public function __get( $name ) { 91 | $value = array_key_exists( $name, $this->variables ) ? $this->variables[ $name ] : null; 92 | return $value; 93 | } 94 | 95 | /** 96 | * Updates the view variable identified by $name with the value provided in $value 97 | * @param string $name The variable name 98 | * @param mixed $value The variable value 99 | * @return Simply_Static\View 100 | */ 101 | public function __set( $name, $value ) { 102 | $this->variables[ $name ] = $value; 103 | return $this; 104 | } 105 | 106 | /** 107 | * Updates the view variable identified by $name with the value provided in $value 108 | * @param string $name The variable name 109 | * @param mixed $value The variable value 110 | * @return Simply_Static\View 111 | */ 112 | public function assign( $name, $value ) { 113 | return $this->__set( $name, $value ); 114 | } 115 | 116 | /** 117 | * Add a flash message to be displayed at the top of the page 118 | * 119 | * Available types: 'updated' (green), 'error' (red), 'notice' (no color) 120 | * 121 | * @param string $type The type of message to be displayed 122 | * @param string $message The message to be displayed 123 | * @return void 124 | */ 125 | public function add_flash( $type, $message ) { 126 | array_push( $this->flashes, array( 'type' => $type, 'message' => $message ) ); 127 | } 128 | 129 | /** 130 | * Returns the layout (if available) or template 131 | * 132 | * Checks to make sure that the file exists and is readable. 133 | * 134 | * @return string|\WP_Error 135 | */ 136 | private function get_renderable_file() { 137 | 138 | // must include a template 139 | if ( ! is_readable( $this->template ) ) { 140 | return new \WP_Error( 'invalid_template', sprintf( __( "Can't find view template: %s", 'simply-static' ), $this->template ) ); 141 | } 142 | 143 | // layouts are optional. if no layout provided, use the template by itself. 144 | if ( $this->layout ) { 145 | if ( ! is_readable( $this->layout ) ) { 146 | return new \WP_Error( 'invalid_layout', sprintf( __( "Can't find view layout: %s", 'simply-static' ), $this->layout ) ); 147 | } else { 148 | // the layout should include the template 149 | return $this->layout; 150 | } 151 | } else { 152 | return $this->template; 153 | } 154 | 155 | } 156 | 157 | /** 158 | * Renders the view script. 159 | * 160 | * @return Simply_Static\View|\WP_Error 161 | */ 162 | public function render() { 163 | 164 | $file = $this->get_renderable_file(); 165 | 166 | if ( is_wp_error( $file ) ) { 167 | return $file; 168 | } else { 169 | include $file; 170 | return $this; 171 | } 172 | 173 | } 174 | 175 | /** 176 | * Returns the view as a string. 177 | * 178 | * @return string|\WP_Error 179 | */ 180 | public function render_to_string() { 181 | 182 | $file = $this->get_renderable_file(); 183 | 184 | if ( is_wp_error( $file ) ) { 185 | return $file; 186 | } else { 187 | ob_start(); 188 | include $file; 189 | return ob_get_clean(); 190 | } 191 | 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /includes/libraries/phpuri.php: -------------------------------------------------------------------------------- 1 | 7 | * echo phpUri::parse('https://www.google.com/')->join('foo'); 8 | * //==> https://www.google.com/foo 9 | * 10 | * 11 | * Licensed under The MIT License 12 | * Redistributions of files must retain the above copyright notice. 13 | * 14 | * @author P Guardiario 15 | * @version 1.0 16 | */ 17 | 18 | /** 19 | * phpUri 20 | */ 21 | class phpUri 22 | { 23 | 24 | /** 25 | * http(s):// 26 | * @var string 27 | */ 28 | public $scheme; 29 | 30 | /** 31 | * www.example.com 32 | * @var string 33 | */ 34 | public $authority; 35 | 36 | /** 37 | * /search 38 | * @var string 39 | */ 40 | public $path; 41 | 42 | /** 43 | * ?q=foo 44 | * @var string 45 | */ 46 | public $query; 47 | 48 | /** 49 | * #bar 50 | * @var string 51 | */ 52 | public $fragment; 53 | 54 | private function __construct( $string ) 55 | { 56 | preg_match_all( '/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/', $string, $m ); 57 | $this->scheme = $m[ 2 ][ 0 ]; 58 | $this->authority = $m[ 4 ][ 0 ]; 59 | 60 | /** 61 | * CHANGE: 62 | * @author Dominik Habichtsberg 63 | * @since 24 Mai 2015 10:02 Uhr 64 | * 65 | * Former code: $this->path = ( empty( $m[ 5 ][ 0 ] ) ) ? '/' : $m[ 5 ][ 0 ]; 66 | * No tests failed, when the path is empty. 67 | * With the former code, the relative urls //g and #s failed 68 | */ 69 | $this->path = $m[ 5 ][ 0 ]; 70 | $this->query = $m[ 7 ][ 0 ]; 71 | $this->fragment = $m[ 9 ][ 0 ]; 72 | } 73 | 74 | private function to_str() 75 | { 76 | $ret = ''; 77 | if ( !empty( $this->scheme ) ) 78 | { 79 | $ret .= "{$this->scheme}:"; 80 | } 81 | 82 | if ( !empty( $this->authority ) ) 83 | { 84 | $ret .= "//{$this->authority}"; 85 | } 86 | 87 | $ret .= $this->normalize_path( $this->path ); 88 | 89 | if ( !empty( $this->query ) ) 90 | { 91 | $ret .= "?{$this->query}"; 92 | } 93 | 94 | if ( !empty( $this->fragment ) ) 95 | { 96 | $ret .= "#{$this->fragment}"; 97 | } 98 | 99 | return $ret; 100 | } 101 | 102 | private function normalize_path( $path ) 103 | { 104 | if ( empty( $path ) ) 105 | { 106 | return ''; 107 | } 108 | 109 | $normalized_path = $path; 110 | $normalized_path = preg_replace( '`//+`', '/', $normalized_path, -1, $c0 ); 111 | $normalized_path = preg_replace( '`^/\\.\\.?/`', '/', $normalized_path, -1, $c1 ); 112 | $normalized_path = preg_replace( '`/\\.(/|$)`', '/', $normalized_path, -1, $c2 ); 113 | 114 | /** 115 | * CHANGE: 116 | * @author Dominik Habichtsberg 117 | * @since 24 Mai 2015 10:05 Uhr 118 | * changed limit form -1 to 1, because climbing up the directory-tree failed 119 | */ 120 | $normalized_path = preg_replace( '`/[^/]*?/\\.\\.(/|$)`', '/', $normalized_path, 1, $c3 ); 121 | $num_matches = $c0 + $c1 + $c2 + $c3; 122 | 123 | return ( $num_matches > 0 ) ? $this->normalize_path( $normalized_path ) : $normalized_path; 124 | } 125 | 126 | /** 127 | * Parse an url string 128 | * 129 | * @param string $url the url to parse 130 | * 131 | * @return phpUri 132 | */ 133 | public static function parse( $url ) 134 | { 135 | $uri = new phpUri( $url ); 136 | 137 | /** 138 | * CHANGE: 139 | * @author Dominik Habichtsberg 140 | * @since 24 Mai 2015 10:25 Uhr 141 | * The base-url should always have a path 142 | */ 143 | if ( empty( $uri->path ) ) 144 | { 145 | $uri->path = '/'; 146 | } 147 | 148 | return $uri; 149 | } 150 | 151 | /** 152 | * Join with a relative url 153 | * 154 | * @param string $relative the relative url to join 155 | * 156 | * @return string 157 | */ 158 | public function join( $relative ) 159 | { 160 | $uri = new phpUri( $relative ); 161 | switch ( TRUE ) 162 | { 163 | case !empty( $uri->scheme ): 164 | break; 165 | 166 | case !empty( $uri->authority ): 167 | break; 168 | 169 | case empty( $uri->path ): 170 | $uri->path = $this->path; 171 | if ( empty( $uri->query ) ) 172 | { 173 | $uri->query = $this->query; 174 | } 175 | break; 176 | 177 | case strpos( $uri->path, '/' ) === 0: 178 | break; 179 | 180 | default: 181 | $base_path = $this->path; 182 | if ( strpos( $base_path, '/' ) === FALSE ) 183 | { 184 | $base_path = ''; 185 | } 186 | else 187 | { 188 | $base_path = preg_replace( '/\/[^\/]+$/', '/', $base_path ); 189 | } 190 | if ( empty( $base_path ) && empty( $this->authority ) ) 191 | { 192 | $base_path = '/'; 193 | } 194 | $uri->path = $base_path . $uri->path; 195 | } 196 | 197 | if ( empty( $uri->scheme ) ) 198 | { 199 | $uri->scheme = $this->scheme; 200 | if ( empty( $uri->authority ) ) 201 | { 202 | $uri->authority = $this->authority; 203 | } 204 | } 205 | 206 | return $uri->to_str(); 207 | } 208 | } -------------------------------------------------------------------------------- /includes/tasks/class-ss-setup-task.php: -------------------------------------------------------------------------------- 1 | save_status_message( $message ); 18 | 19 | $archive_dir = $this->options->get_archive_dir(); 20 | 21 | // create temp archive directory 22 | if ( ! file_exists( $archive_dir ) ) { 23 | Util::debug_log( 'Creating archive directory: ' . $archive_dir ); 24 | $create_dir = wp_mkdir_p( $archive_dir ); 25 | if ( $create_dir === false ) { 26 | return new \WP_Error( 'cannot_create_archive_dir' ); 27 | } 28 | } 29 | 30 | // TODO: Add a way for the user to perform this, optionally, so that we 31 | // don't need to do it every time. Then enable the two commented-out 32 | // sections below. 33 | Page::query()->delete_all(); 34 | 35 | // clear out any saved error messages on pages 36 | //Page::query() 37 | // ->update_all( 'error_message', null ); 38 | 39 | // delete pages that we can't process 40 | //Page::query() 41 | // ->where( 'http_status_code IS NULL OR http_status_code NOT IN (?)', implode( ',', Page::$processable_status_codes ) ) 42 | // ->delete_all(); 43 | 44 | // add origin url and additional urls/files to database 45 | self::add_origin_and_additional_urls_to_db( $this->options->get( 'additional_urls' ) ); 46 | self::add_additional_files_to_db( $this->options->get( 'additional_files' ) ); 47 | 48 | return true; 49 | } 50 | 51 | /** 52 | * Ensure the Origin URL and user-specified Additional URLs are in the DB 53 | * @return void 54 | */ 55 | public static function add_origin_and_additional_urls_to_db( $additional_urls ) { 56 | $origin_url = trailingslashit( Util::origin_url() ); 57 | Util::debug_log( 'Adding origin URL to queue: ' . $origin_url ); 58 | $static_page = Page::query()->find_or_initialize_by( 'url', $origin_url ); 59 | $static_page->set_status_message( __( "Origin URL", 'simply-static' ) ); 60 | // setting to 0 for "not found anywhere" since it's either the origin 61 | // or something the user specified 62 | $static_page->found_on_id = 0; 63 | $static_page->save(); 64 | 65 | $urls = array_unique( Util::string_to_array( $additional_urls ) ); 66 | foreach ( $urls as $url ) { 67 | if ( Util::is_local_url( $url ) ) { 68 | Util::debug_log( 'Adding additional URL to queue: ' . $url ); 69 | $static_page = Page::query()->find_or_initialize_by( 'url', $url ); 70 | $static_page->set_status_message( __( "Additional URL", 'simply-static' ) ); 71 | $static_page->found_on_id = 0; 72 | $static_page->save(); 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Convert Additional Files/Directories to URLs and add them to the database 79 | * @return void 80 | */ 81 | public static function add_additional_files_to_db( $additional_files ) { 82 | // Convert additional files to URLs and add to queue 83 | foreach ( Util::string_to_array( $additional_files ) as $item ) { 84 | 85 | // If item is a file, convert to url and insert into database. 86 | // If item is a directory, recursively iterate and grab all files, 87 | // and for each file, convert to url and insert into database. 88 | if ( file_exists( $item ) ) { 89 | if ( is_file( $item ) ) { 90 | $url = self::convert_path_to_url( $item ); 91 | Util::debug_log( "File " . $item . ' exists; adding to queue as: ' . $url ); 92 | $static_page = Page::query() 93 | ->find_or_create_by( 'url', $url ); 94 | $static_page->set_status_message( __( "Additional File", 'simply-static' ) ); 95 | // setting found_on_id to 0 since this was user-specified 96 | $static_page->found_on_id = 0; 97 | $static_page->save(); 98 | } else { 99 | Util::debug_log( "Adding files from directory: " . $item ); 100 | $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $item, \RecursiveDirectoryIterator::SKIP_DOTS ) ); 101 | 102 | foreach ( $iterator as $file_name => $file_object ) { 103 | $url = self::convert_path_to_url( $file_name ); 104 | Util::debug_log( "Adding file " . $file_name . ' to queue as: ' . $url ); 105 | $static_page = Page::query()->find_or_initialize_by( 'url', $url ); 106 | $static_page->set_status_message( __( "Additional Dir", 'simply-static' ) ); 107 | $static_page->found_on_id = 0; 108 | $static_page->save(); 109 | } 110 | } 111 | } else { 112 | Util::debug_log( "File doesn't exist: " . $item ); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Convert a directory path into a valid WordPress URL 119 | * @param string $path The path to a directory or a file 120 | * @return string The WordPress URL for the given path 121 | */ 122 | private static function convert_path_to_url( $path ) { 123 | $url = $path; 124 | if ( stripos( $path, WP_PLUGIN_DIR ) === 0 ) { 125 | $url = str_replace( WP_PLUGIN_DIR, WP_PLUGIN_URL, $path ); 126 | } elseif ( stripos( $path, WP_CONTENT_DIR ) === 0 ) { 127 | $url = str_replace( WP_CONTENT_DIR, WP_CONTENT_URL, $path ); 128 | } elseif ( stripos( $path, get_home_path() ) === 0 ) { 129 | $url = str_replace( untrailingslashit( get_home_path() ), Util::origin_url(), $path ); 130 | } 131 | return $url; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /views/diagnostics.php: -------------------------------------------------------------------------------- 1 | 4 | 5 |

6 | 7 |
8 | 9 | results as $title => $tests ) : ?> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | themes as $theme ) : ?> 42 | 43 | 44 | 45 | 46 | get( 'Name') === $this->current_theme_name ) : ?> 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
get( 'Name'); ?>'>get( 'ThemeURI'); ?>get( 'Version'); ?>
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | plugins as $plugin_path => $plugin_data ) : ?> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
'>
80 | 81 |

82 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 101 | 102 | 103 | 104 | 109 | 110 | 111 |
93 | 97 |

98 | 99 |

100 |
105 |

106 | ' /> 107 |

108 |
112 | 113 |
114 | 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | 134 | 142 | 143 | 144 |
125 | debug_file_exists ) : ?> 126 |

a debug log.", 'simply-static' ), $this->debug_file_url ); ?>

127 | 128 |

129 | 130 |
135 | debug_file_exists ) : ?> 136 | 137 | "/> 138 | 139 |

140 | 141 |
145 | 146 |
147 | 148 |
149 | 150 | -------------------------------------------------------------------------------- /languages/simply-static-de_DE.po: -------------------------------------------------------------------------------- 1 | # Translation of Development (trunk) in German 2 | # This file is distributed under the same license as the Development (trunk) package. 3 | msgid "" 4 | msgstr "" 5 | "PO-Revision-Date: 2016-05-03 00:19:37+0000\n" 6 | "MIME-Version: 1.0\n" 7 | "Content-Type: text/plain; charset=UTF-8\n" 8 | "Content-Transfer-Encoding: 8bit\n" 9 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 10 | "X-Generator: GlotPress/2.1.0-alpha\n" 11 | "Project-Id-Version: Development (trunk)\n" 12 | 13 | #: includes/class-simply-static.php:415 14 | msgid "An Additional File or Directory is not located within an expected directory: %s
It should be in one of these directories (or a subdirectory):
%s
%s
%s" 15 | msgstr "Eine zusätzliche Datei oder Verzeichnis ist nicht in dem erwarteten Verzeichnis: %s
Diese sollten in einem dieser Verzeichnisse (oder einem Untervezeichnis) sein:
%s
%s
%s" 16 | 17 | #: includes/class-simply-static.php:406 18 | msgid "An Additional URL does not start with %s: %s" 19 | msgstr "Eine zusätzliche URL darf nicht mit %s beginnen: %s" 20 | 21 | #: includes/class-simply-static-archive-creator.php:275 22 | msgid "Unable to create ZIP archive" 23 | msgstr "ZIP Archiv konnte nicht erstellt werden." 24 | 25 | #: includes/class-simply-static-archive-creator.php:296 26 | msgid "Could not create file or directory: %s" 27 | msgstr "Konnte die Datei bzw. das Verzeichnis nicht erstellen: %s" 28 | 29 | #: includes/class-simply-static-archive-creator.php:317 30 | #: includes/class-simply-static-archive-creator.php:324 31 | msgid "Could not delete temporary file or directory: %s" 32 | msgstr "Konnte die temporäre Datei bzw. das Verzeichnis nicht löschen: %s" 33 | 34 | #: includes/class-simply-static-url-fetcher.php:25 35 | msgid "Attempting to fetch remote URL: %s" 36 | msgstr "Versuche die entfernte URL abzurufen: %s" 37 | 38 | #: includes/class-simply-static-view.php:144 39 | msgid "Can't find view template: %s" 40 | msgstr "Konnte „Template Ansicht“ nicht finden: %s" 41 | 42 | #: includes/class-simply-static-view.php:150 43 | msgid "Can't find view layout: %s" 44 | msgstr "Konnte „Layout Ansicht“ nicht finden: %s" 45 | 46 | #: includes/class-simply-static.php:196 includes/class-simply-static.php:215 47 | msgid "Simply Static Settings" 48 | msgstr "Simply Static Einstellungen" 49 | 50 | #. #-#-#-#-# tmp-simply-static.pot (Simply Static 1.2.4) #-#-#-#-# 51 | #. Plugin Name of the plugin/theme 52 | #: includes/class-simply-static.php:197 53 | msgid "Simply Static" 54 | msgstr "Simply Static" 55 | 56 | #: includes/class-simply-static.php:206 57 | msgid "Generate Static Site" 58 | msgstr "Generiere eine Statische Website" 59 | 60 | #: includes/class-simply-static.php:207 61 | msgid "Generate" 62 | msgstr "" 63 | 64 | #: includes/class-simply-static.php:216 65 | msgid "Settings" 66 | msgstr "Einstellungen" 67 | 68 | #: includes/class-simply-static.php:253 69 | msgid "ZIP archive created: " 70 | msgstr "ZIP Archiv erstellt:" 71 | 72 | #: includes/class-simply-static.php:254 73 | msgid "Click here to download" 74 | msgstr "Klicke hier zum Download." 75 | 76 | #: includes/class-simply-static.php:296 77 | msgid "Settings saved." 78 | msgstr "Einstellungen gespeichert." 79 | 80 | #: includes/class-simply-static.php:360 81 | msgid "Destination URL cannot be blank" 82 | msgstr "Die Ziel URL darf nicht leer sein." 83 | 84 | #: includes/class-simply-static.php:365 85 | msgid "Temporary Files Directory cannot be blank" 86 | msgstr "Das Verzeichnis für temporäre Dateien darf nicht leer sein." 87 | 88 | #: includes/class-simply-static.php:369 89 | msgid "Temporary Files Directory is not writeable: %s" 90 | msgstr "Das Verzeichnis für temporäre Dateien ist nicht beschreibbar: %s" 91 | 92 | #: includes/class-simply-static.php:372 93 | msgid "Temporary Files Directory does not exist: %s" 94 | msgstr "Das Verzeichnis für temporäre Dateien existiert nicht: %s" 95 | 96 | #: includes/class-simply-static.php:378 97 | msgid "Your site does not have a permalink structure set. You can select one on the Permalink Settings page." 98 | msgstr "Deine Website hat keine Permalink Struktur gesetzt. Du kannst eine solche unter Einstellungen > Permalinks auswählen und speichern." 99 | 100 | #: includes/class-simply-static.php:383 101 | msgid "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." 102 | msgstr "Dein Server hat die PHP zip Erweiterung nicht aktiviert. Schau dir bitte die PHP zip-Erweiterungs-Seite (engl.) für weiterführende Informationen an, und wie du diese Erweiterung aktivieren kannst." 103 | 104 | #: includes/class-simply-static.php:391 105 | msgid "Local Directory cannot be blank" 106 | msgstr "Das lokale Verzeichnis darf nicht leer sein." 107 | 108 | #: includes/class-simply-static.php:395 109 | msgid "Local Directory is not writeable: %s" 110 | msgstr "Das lokale Verzeichnis ist nicht beschreibbar: %s" 111 | 112 | #: includes/class-simply-static.php:398 113 | msgid "Local Directory does not exist: %s" 114 | msgstr "Das lokale Verzeichnis existiert nicht: %s" 115 | 116 | #. Plugin URI of the plugin/theme 117 | msgid "http://codeofconduct.co/simply-static" 118 | msgstr "http://codeofconduct.co/simply-static" 119 | 120 | #. Description of the plugin/theme 121 | msgid "Produces a static HTML version of your WordPress install and adjusts URLs accordingly." 122 | msgstr "Erstellt von deiner WordPress Installation eine statische HTML Version, und passt die URLS entsprechend an." 123 | 124 | #. Author of the plugin/theme 125 | msgid "Code of Conduct" 126 | msgstr "Code of Conduct" 127 | 128 | #. Author URI of the plugin/theme 129 | msgid "http://codeofconduct.co/" 130 | msgstr "http://codeofconduct.co/" -------------------------------------------------------------------------------- /includes/class-ss-upgrade-handler.php: -------------------------------------------------------------------------------- 1 | Util::origin_scheme(), 54 | 'destination_host' => Util::origin_host(), 55 | 'temp_files_dir' => trailingslashit( plugin_dir_path( dirname( __FILE__ ) ) . 'static-files' ), 56 | 'additional_urls' => '', 57 | 'additional_files' => '', 58 | 'urls_to_exclude' => array(), 59 | 'delivery_method' => 'zip', 60 | 'local_dir' => '', 61 | 'delete_temp_files' => '1', 62 | 'relative_path' => '', 63 | 'destination_url_type' => 'relative', 64 | 'archive_status_messages' => array(), 65 | 'archive_name' => null, 66 | 'archive_start_time' => null, 67 | 'archive_end_time' => null, 68 | 'debugging_mode' => '0', 69 | 'http_basic_auth_digest' => null, 70 | ); 71 | 72 | $save_changes = false; 73 | $version = self::$options->get( 'version' ); 74 | 75 | // Never installed or options key changed 76 | if ( null === $version ) { 77 | $save_changes = true; 78 | 79 | // checking for legacy options key 80 | $old_ss_options = get_option( 'simply_static' ); 81 | 82 | if ( $old_ss_options ) { // options key changed 83 | update_option( 'simply-static', $old_ss_options ); 84 | delete_option( 'simply_static' ); 85 | 86 | // update Simply_Static\Options again to pull in updated data 87 | self::$options = new Options(); 88 | } 89 | } 90 | 91 | // sync the database on any install/upgrade/downgrade 92 | if ( version_compare( $version, Plugin::VERSION, '!=' ) ) { 93 | $save_changes = true; 94 | 95 | Page::create_or_update_table(); 96 | self::set_default_options(); 97 | 98 | // perform migrations if our saved version # is older than 99 | // the current version 100 | if ( version_compare( $version, Plugin::VERSION, '<' ) ) { 101 | 102 | if ( version_compare( $version, '1.4.0', '<' ) ) { 103 | // check for, and add, the WP emoji url if it's missing 104 | $emoji_url = includes_url( 'js/wp-emoji-release.min.js' ); 105 | $additional_urls = self::$options->get( 'additional_urls' ); 106 | $urls_array = Util::string_to_array( $additional_urls ); 107 | 108 | if ( ! in_array( $emoji_url, $urls_array ) ) { 109 | $additional_urls = $additional_urls . "\n" . $emoji_url; 110 | self::$options->set( 'additional_urls', $additional_urls ); 111 | } 112 | } 113 | 114 | if ( version_compare( $version, '1.7.0', '<' ) ) { 115 | $scheme = self::$options->get( 'destination_scheme' ); 116 | if ( strpos( $scheme, '://' ) === false ) { 117 | $scheme = $scheme . '://'; 118 | self::$options->set( 'destination_scheme', $scheme ); 119 | } 120 | 121 | $host = self::$options->get( 'destination_host' ); 122 | if ( $host == Util::origin_host() ) { 123 | self::$options->set( 'destination_url_type', 'relative' ); 124 | } else { 125 | self::$options->set( 'destination_url_type', 'absolute' ); 126 | } 127 | } 128 | 129 | if ( version_compare( $version, '1.7.1', '<' ) ) { 130 | // check for, and add, the WP uploads dir if it's missing 131 | $upload_dir = wp_upload_dir(); 132 | if ( isset( $upload_dir['basedir'] ) ) { 133 | $upload_dir = trailingslashit( $upload_dir['basedir'] ); 134 | 135 | $additional_files = self::$options->get( 'additional_files' ); 136 | $files_array = Util::string_to_array( $additional_files ); 137 | 138 | if ( ! in_array( $upload_dir, $files_array ) ) { 139 | $additional_files = $additional_files . "\n" . $upload_dir; 140 | self::$options->set( 'additional_files', $additional_files ); 141 | } 142 | } 143 | } 144 | 145 | // setting the temp dir back to the one within /simply-static/ 146 | if ( version_compare( $version, '2.0.4', '<' ) ) { 147 | $old_tmp_dir = trailingslashit( trailingslashit( get_temp_dir() ) . 'static-files' ); 148 | if ( self::$options->get( 'temp_files_dir' ) === $old_tmp_dir ) { 149 | self::$options->set( 'temp_files_dir', self::$default_options['temp_files_dir'] ); 150 | } 151 | } 152 | } 153 | 154 | self::remove_old_options(); 155 | } 156 | 157 | if ( $save_changes ) { 158 | // update the version and save options 159 | self::$options 160 | ->set( 'version', Plugin::VERSION ) 161 | ->save(); 162 | } 163 | } 164 | 165 | /** 166 | * Add default options where they don't exist 167 | * @return void 168 | */ 169 | protected static function set_default_options() { 170 | foreach ( self::$default_options as $option_key => $option_value ) { 171 | if ( self::$options->get( $option_key ) === null ) { 172 | self::$options->set( $option_key, $option_value ); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Remove any unused (old) options 179 | * @return void 180 | */ 181 | protected static function remove_old_options() { 182 | $all_options = self::$options->get_as_array(); 183 | 184 | foreach ( $all_options as $option_key => $option_value ) { 185 | if ( ! array_key_exists( $option_key, self::$default_options ) ) { 186 | self::$options->destroy( $option_key ); 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /includes/models/class-ss-model.php: -------------------------------------------------------------------------------- 1 | 'col_definition', e.g. 27 | * 'id' => 'BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY' 28 | * @var array 29 | */ 30 | protected static $columns = array(); 31 | 32 | /** 33 | * A list of the indexes for the model 34 | * 35 | * In the format of 'index_name' => 'index_def', e.g. 36 | * 'url' => 'url' 37 | * @var array 38 | */ 39 | protected static $indexes = array(); 40 | 41 | /** 42 | * The name of the primary key for the model 43 | * @var string 44 | */ 45 | protected static $primary_key = null; 46 | 47 | /**************************************************************************/ 48 | 49 | /** 50 | * The stored data for this instance of the model. 51 | * @var array 52 | */ 53 | private $data = array(); 54 | 55 | /** 56 | * Track if this record has had changed made to it 57 | * @var boolean 58 | */ 59 | private $dirty_fields = array(); 60 | 61 | /** 62 | * Retrieve the value of a field for the model 63 | * 64 | * Returns an exception if you try to retrieve a field that isn't set. 65 | * @param string $field_name The name of the field to retrieve 66 | * @return mixed The value for the field 67 | */ 68 | public function __get( $field_name ) { 69 | if ( ! array_key_exists( $field_name, $this->data ) ) { 70 | throw new \Exception( 'Undefined variable for ' . get_called_class() ); 71 | } else { 72 | return $this->data[ $field_name ]; 73 | } 74 | } 75 | 76 | /** 77 | * Set the value of a field for the model 78 | * 79 | * Returns an exception if you try to set a field that isn't one of the 80 | * model's columns. 81 | * @param string $field_name The name of the field to set 82 | * @param mixed $field_value The value for the field 83 | * @return mixed The value of the field that was set 84 | */ 85 | public function __set( $field_name, $field_value ) { 86 | if ( ! array_key_exists( $field_name, static::$columns ) ) { 87 | throw new \Exception( 'Column doesn\'t exist for ' . get_called_class() ); 88 | } else { 89 | if ( ! array_key_exists( $field_name, $this->data ) || $this->data[ $field_name ] !== $field_value ) { 90 | array_push( $this->dirty_fields, $field_name ); 91 | } 92 | return $this->data[ $field_name ] = $field_value; 93 | } 94 | } 95 | 96 | /** 97 | * Returns the name of the table 98 | * 99 | * Note that MySQL doesn't allow anything other than alphanumerics, 100 | * underscores, and $, so dashes in the slug are replaced with underscores. 101 | * @return string The name of the table 102 | */ 103 | public static function table_name() { 104 | global $wpdb; 105 | 106 | return $wpdb->prefix . 'simply_static_' . static::$table_name; 107 | } 108 | 109 | /** 110 | * Used for finding models matching certain criteria 111 | * @return Simply_Static\Query 112 | */ 113 | public static function query() 114 | { 115 | $query = new Query( get_called_class() ); 116 | return $query; 117 | } 118 | 119 | /** 120 | * Initialize an instance of the class and set its attributes 121 | * @param array $attributes Array of attributes to set for the class 122 | * @return static An instance of the class 123 | */ 124 | public static function initialize( $attributes ) { 125 | $obj = new static(); 126 | foreach ( array_keys( static::$columns ) as $column ) { 127 | $obj->data[ $column ] = null; 128 | } 129 | $obj->attributes( $attributes ); 130 | return $obj; 131 | } 132 | 133 | /** 134 | * Set the attributes of the model 135 | * @param array $attributes Array of attributes to set 136 | * @return static An instance of the class 137 | */ 138 | public function attributes( $attributes ) { 139 | foreach ( $attributes as $name => $value ) { 140 | $this->$name = $value; 141 | } 142 | return $this; 143 | } 144 | 145 | /** 146 | * Save the model to the database 147 | * 148 | * If the model is new a record gets created in the database, otherwise the 149 | * existing record gets updated. 150 | * @param array $attributes Array of attributes to set 151 | * @return boolean An instance of the class 152 | */ 153 | public function save() { 154 | global $wpdb; 155 | 156 | // autoset created_at/updated_at upon save 157 | if ( $this->created_at === null ) { 158 | $this->created_at = Util::formatted_datetime(); 159 | } 160 | $this->updated_at = Util::formatted_datetime(); 161 | 162 | // If we haven't changed anything, don't bother updating the DB, and 163 | // return that saving was successful. 164 | if ( empty( $this->dirty_fields ) ) { 165 | return true; 166 | } else { 167 | // otherwise, create a new array with just the fields we're updating, 168 | // then set the dirty fields back to empty 169 | $fields = array_intersect_key( $this->data, array_flip( $this->dirty_fields ) ); 170 | $this->dirty_fields = array(); 171 | } 172 | 173 | if ( $this->exists() ) { 174 | $primary_key = static::$primary_key; 175 | $rows_updated = $wpdb->update( self::table_name(), $fields, array( $primary_key => $this->$primary_key ) ); 176 | return $rows_updated !== false; 177 | } else { 178 | $rows_updated = $wpdb->insert( self::table_name(), $fields ); 179 | if ( $rows_updated === false ) { 180 | return false; 181 | } else { 182 | $this->id = $wpdb->insert_id; 183 | return true; 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Check if the model exists in the database 190 | * 191 | * Technically this is checking whether the model has its primary key set. 192 | * If it is set, we assume the record exists in the database. 193 | * @return boolean Does this model exist in the db? 194 | */ 195 | public function exists() { 196 | $primary_key = static::$primary_key; 197 | return $this->$primary_key !== null; 198 | } 199 | 200 | /** 201 | * Create or update the table for the model 202 | * 203 | * Uses the static::$table_name and loops through all of the columns in 204 | * static::$columns and the indexes in static::$indexes to create a SQL 205 | * query for creating the table. 206 | * 207 | * http://wordpress.stackexchange.com/questions/78667/dbdelta-alter-table-syntax 208 | * @return void 209 | */ 210 | public static function create_or_update_table() { 211 | global $wpdb; 212 | 213 | $charset_collate = $wpdb->get_charset_collate(); 214 | $sql = 'CREATE TABLE ' . self::table_name() . ' (' . "\n"; 215 | 216 | foreach ( static::$columns as $column_name => $column_definition ) { 217 | $sql .= $column_name . ' ' . $column_definition . ', ' . "\n"; 218 | } 219 | foreach ( static::$indexes as $index ) { 220 | $sql .= $index . ', ' . "\n"; 221 | } 222 | 223 | // remove trailing newline 224 | $sql = rtrim( $sql, "\n" ); 225 | // remove trailing comma 226 | $sql = rtrim( $sql, ', ' ); 227 | $sql .= "\n" . ') ' . "\n" . $charset_collate; 228 | 229 | require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 230 | dbDelta( $sql ); 231 | } 232 | 233 | /** 234 | * Drop the table for the model 235 | * @return void 236 | */ 237 | public static function drop_table() { 238 | global $wpdb; 239 | 240 | $wpdb->query( 'DROP TABLE IF EXISTS ' . self::table_name() ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /includes/class-ss-url-fetcher.php: -------------------------------------------------------------------------------- 1 | archive_dir = Options::instance()->get_archive_dir(); 58 | } 59 | 60 | return self::$instance; 61 | } 62 | 63 | /** 64 | * Fetch the URL and return a \WP_Error if we get one, otherwise a Response class. 65 | * @param Simply_Static\Page $static_page URL to fetch 66 | * @return boolean Was the fetch successful? 67 | */ 68 | public function fetch( Page $static_page ) { 69 | $url = $static_page->url; 70 | 71 | $static_page->last_checked_at = Util::formatted_datetime(); 72 | 73 | // Don't process URLs that don't match the URL of this WordPress installation 74 | if ( ! Util::is_local_url( $url ) ) { 75 | Util::debug_log( "Not fetching URL because it is not a local URL" ); 76 | $static_page->http_status_code = null; 77 | $message = sprintf( __( "An error occurred: %s", 'simply-static' ), __( "Attempted to fetch a remote URL", 'simply-static' ) ); 78 | $static_page->set_error_message( $message ); 79 | $static_page->save(); 80 | return false; 81 | } 82 | 83 | $temp_filename = wp_tempnam(); 84 | 85 | Util::debug_log( "Fetching URL and saving it to: " . $temp_filename ); 86 | $response = self::remote_get( $url, $temp_filename ); 87 | 88 | $filesize = file_exists( $temp_filename ) ? filesize( $temp_filename ) : 0; 89 | Util::debug_log( "Filesize: " . $filesize . ' bytes' ); 90 | 91 | if ( is_wp_error( $response ) ) { 92 | Util::debug_log( "We encountered an error when fetching: " . $response->get_error_message() ); 93 | Util::debug_log( $response ); 94 | $static_page->http_status_code = null; 95 | $message = sprintf( __( "An error occurred: %s", 'simply-static' ), $response->get_error_message() ); 96 | $static_page->set_error_message( $message ); 97 | $static_page->save(); 98 | return false; 99 | } else { 100 | $static_page->http_status_code = $response['response']['code']; 101 | $static_page->content_type = $response['headers']['content-type']; 102 | $static_page->redirect_url = isset( $response['headers']['location'] ) ? $response['headers']['location'] : null; 103 | 104 | Util::debug_log( "http_status_code: " . $static_page->http_status_code . " | content_type: " . $static_page->content_type ); 105 | 106 | $relative_filename = null; 107 | if ( $static_page->http_status_code == 200 ) { 108 | // pclzip doesn't like 0 byte files (fread error), so we're 109 | // going to fix that by putting a single space into the file 110 | if ( $filesize === 0 ) { 111 | file_put_contents( $temp_filename, ' ' ); 112 | } 113 | 114 | $relative_filename = $this->create_directories_for_static_page( $static_page ); 115 | } 116 | 117 | if ( $relative_filename !== null ) { 118 | $static_page->file_path = $relative_filename; 119 | $file_path = $this->archive_dir . $relative_filename; 120 | Util::debug_log( "Renaming temp file from " . $temp_filename . " to " . $file_path ); 121 | rename( $temp_filename, $file_path ); 122 | } else { 123 | Util::debug_log( "We weren't able to establish a filename; deleting temp file" ); 124 | unlink( $temp_filename ); 125 | } 126 | 127 | $static_page->save(); 128 | 129 | return true; 130 | } 131 | } 132 | 133 | /** 134 | * Given a Static_Page, return a relative filename based on the URL 135 | * 136 | * This will also create directories as needed so that a file could be 137 | * created at the returned file path. 138 | * 139 | * @param Simply_Static\Page $static_page The Simply_Static\Page 140 | * @return string|null The relative file path of the file 141 | */ 142 | public function create_directories_for_static_page( $static_page ) { 143 | $url_parts = parse_url( $static_page->url ); 144 | // a domain with no trailing slash has no path, so we're giving it one 145 | $path = isset( $url_parts['path'] ) ? $url_parts['path'] : '/'; 146 | 147 | $origin_path_length = strlen( parse_url( Util::origin_url(), PHP_URL_PATH ) ); 148 | if ( $origin_path_length > 1 ) { // prevents removal of '/' 149 | $path = substr( $path, $origin_path_length ); 150 | } 151 | 152 | $path_info = Util::url_path_info( $path ); 153 | 154 | $relative_file_dir = $path_info['dirname']; 155 | $relative_file_dir = Util::remove_leading_directory_separator( $relative_file_dir ); 156 | 157 | // If there's no extension, we're going to create a directory with the 158 | // filename and place an index.html/xml file in there. 159 | if ( $path_info['extension'] === '' ) { 160 | if ( $path_info['filename'] !== '' ) { 161 | // the filename would be blank for the root url, in that 162 | // instance we don't want to add an extra slash 163 | $relative_file_dir .= $path_info['filename']; 164 | $relative_file_dir = Util::add_trailing_directory_separator( $relative_file_dir ); 165 | } 166 | $path_info['filename'] = 'index'; 167 | if ( $static_page->is_type( 'xml' ) ) { 168 | $path_info['extension'] = 'xml'; 169 | } else { 170 | $path_info['extension'] = 'html'; 171 | } 172 | } 173 | 174 | $create_dir = wp_mkdir_p( $this->archive_dir . $relative_file_dir ); 175 | if ( $create_dir === false ) { 176 | Util::debug_log( "Unable to create temporary directory: " . $this->archive_dir . $relative_file_dir ); 177 | $static_page->set_error_message( 'Unable to create temporary directory' ); 178 | } else { 179 | $relative_filename = $relative_file_dir . $path_info['filename'] . '.' . $path_info['extension']; 180 | Util::debug_log( "New filename for static page: " . $relative_filename ); 181 | 182 | // check that file doesn't exist OR exists but is writeable 183 | // (generally, we'd expect it to never exist) 184 | if ( ! file_exists( $relative_filename ) || is_writable( $relative_filename ) ) { 185 | return $relative_filename; 186 | } else { 187 | Util::debug_log( "File exists and is unwriteable" ); 188 | $static_page->set_error_message( 'File exists and is unwriteable' ); 189 | } 190 | } 191 | 192 | return null; 193 | } 194 | 195 | public static function remote_get( $url, $filename = null ) { 196 | $basic_auth_digest = Options::instance()->get( 'http_basic_auth_digest' ); 197 | 198 | $args = array( 199 | 'timeout' => self::TIMEOUT, 200 | 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 201 | 'redirection' => 0, // disable redirection 202 | 'blocking' => true // do not execute code until this call is complete 203 | ); 204 | 205 | if ( $filename ) { 206 | $args['stream'] = true; // stream body content to a file 207 | $args['filename'] = $filename; 208 | } 209 | 210 | if ( $basic_auth_digest ) { 211 | $args['headers'] = array( 'Authorization' => 'Basic ' . $basic_auth_digest ); 212 | } 213 | 214 | $response = wp_remote_get( $url, $args ); 215 | return $response; 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /includes/class-ss-diagnostic.php: -------------------------------------------------------------------------------- 1 | '5.3.0', 20 | 'curl' => '7.15.0' 21 | ); 22 | 23 | /** 24 | * Assoc. array of categories, and then functions to check 25 | * @var array 26 | */ 27 | protected $description = array( 28 | 'URLs' => array(), 29 | 'Filesystem' => array( 30 | array( 'function' => 'is_temp_files_dir_readable' ), 31 | array( 'function' => 'is_temp_files_dir_writeable' ) 32 | ), 33 | 'WordPress' => array( 34 | array( 'function' => 'is_permalink_structure_set' ), 35 | array( 'function' => 'can_wp_make_requests_to_itself' ) 36 | ), 37 | 'MySQL' => array( 38 | array( 'function' => 'user_can_delete' ), 39 | array( 'function' => 'user_can_insert' ), 40 | array( 'function' => 'user_can_select' ), 41 | array( 'function' => 'user_can_create' ), 42 | array( 'function' => 'user_can_alter' ), 43 | array( 'function' => 'user_can_drop' ) 44 | ), 45 | 'PHP' => array( 46 | array( 'function' => 'php_version' ), 47 | array( 'function' => 'has_curl' ) 48 | ) 49 | ); 50 | 51 | /** 52 | * Assoc. array of results of the diagnostic check 53 | * @var array 54 | */ 55 | public $results = array(); 56 | 57 | /** 58 | * An instance of the options structure containing all options for this plugin 59 | * @var Simply_Static\Options 60 | */ 61 | protected $options = null; 62 | 63 | public function __construct() { 64 | $this->options = Options::instance(); 65 | 66 | if ( $this->options->get( 'destination_url_type' ) == 'absolute' ) { 67 | $this->description['URLs'][] = array( 68 | 'function' => 'is_destination_host_a_valid_url' 69 | ); 70 | } 71 | 72 | if ( $this->options->get( 'delivery_method' ) == 'local' ) { 73 | $this->description['Filesystem'][] = array( 74 | 'function' => 'is_local_dir_writeable' 75 | ); 76 | } 77 | 78 | $additional_urls = Util::string_to_array( $this->options->get( 'additional_urls' ) ); 79 | foreach ( $additional_urls as $url ) { 80 | $this->description['URLs'][] = array( 81 | 'function' => 'is_additional_url_valid', 82 | 'param' => $url 83 | ); 84 | } 85 | 86 | $additional_files = Util::string_to_array( $this->options->get( 'additional_files' ) ); 87 | foreach ( $additional_files as $file ) { 88 | $this->description['Filesystem'][] = array( 89 | 'function' => 'is_additional_file_valid', 90 | 'param' => $file 91 | ); 92 | } 93 | 94 | foreach ( $this->description as $title => $tests ) { 95 | $this->results[ $title ] = array(); 96 | foreach ( $tests as $test ) { 97 | $param = isset( $test['param'] ) ? $test['param'] : null; 98 | $result = $this->{$test['function']}( $param ); 99 | 100 | if ( ! isset( $result['message'] ) ) { 101 | $result['message'] = $result['test'] ? __( 'OK', 'simply-static' ) : __( 'FAIL', 'simply-static' ); 102 | } 103 | 104 | $this->results[ $title ][] = $result; 105 | } 106 | } 107 | } 108 | 109 | public function is_destination_host_a_valid_url() { 110 | $destination_scheme = $this->options->get( 'destination_scheme' ); 111 | $destination_host = $this->options->get( 'destination_host' ); 112 | $destination_url = $destination_scheme . $destination_host; 113 | $label = sprintf( __( 'Checking if Destination URL %s is valid', 'simply-static' ), $destination_url ); 114 | return array( 115 | 'label' => $label, 116 | 'test' => filter_var( $destination_url, FILTER_VALIDATE_URL ) !== false 117 | ); 118 | } 119 | 120 | public function is_additional_url_valid( $url ) { 121 | $label = sprintf( __( 'Checking if Additional URL %s is valid', 'simply-static' ), $url ); 122 | if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) { 123 | $test = false; 124 | $message = __( 'Not a valid URL', 'simply-static' ); 125 | } else if ( ! Util::is_local_url( $url ) ) { 126 | $test = false; 127 | $message = __( 'Not a local URL', 'simply-static' ); 128 | } else { 129 | $test = true; 130 | $message = null; 131 | } 132 | 133 | return array( 134 | 'label' => $label, 135 | 'test' => $test, 136 | 'message' => $message 137 | ); 138 | } 139 | 140 | public function is_additional_file_valid( $file ) { 141 | $label = sprintf( __( 'Checking if Additional File/Dir %s is valid', 'simply-static' ), $file ); 142 | if ( stripos( $file, get_home_path() ) !== 0 && stripos( $file, WP_PLUGIN_DIR ) !== 0 && stripos( $file, WP_CONTENT_DIR ) !== 0 ) { 143 | $test = false; 144 | $message = __( 'Not a valid path', 'simply-static' ); 145 | } else if ( ! is_readable( $file ) ) { 146 | $test = false; 147 | $message = __( 'Not readable', 'simply-static' );; 148 | } else { 149 | $test = true; 150 | $message = null; 151 | } 152 | 153 | return array( 154 | 'label' => $label, 155 | 'test' => $test, 156 | 'message' => $message 157 | ); 158 | } 159 | 160 | public function is_permalink_structure_set() { 161 | $label = __( 'Checking if WordPress permalink structure is set', 'simply-static' ); 162 | return array( 163 | 'label' => $label, 164 | 'test' => strlen( get_option( 'permalink_structure' ) ) !== 0 165 | ); 166 | } 167 | 168 | public function can_wp_make_requests_to_itself() { 169 | $ip_address = getHostByName( getHostName() ); 170 | $label = sprintf( __( "Checking if WordPress can make requests to itself from %s", 'simply-static' ), $ip_address ); 171 | 172 | $url = Util::origin_url(); 173 | $response = Url_Fetcher::remote_get( $url ); 174 | 175 | if ( is_wp_error( $response ) ) { 176 | $test = false; 177 | $message = null; 178 | } else { 179 | $code = $response['response']['code']; 180 | if ( $code == 200 ) { 181 | $test = true; 182 | $message = $code; 183 | } else if ( in_array( $code, Page::$processable_status_codes ) ) { 184 | $test = false; 185 | $message = sprintf( __( "Received a %s response. This might indicate a problem.", 'simply-static' ), $code ); 186 | } else { 187 | $test = false; 188 | $message = sprintf( __( "Received a %s response.", 'simply-static' ), $code );; 189 | } 190 | } 191 | 192 | return array( 193 | 'label' => $label, 194 | 'test' => $test, 195 | 'message' => $message 196 | ); 197 | } 198 | 199 | public function is_temp_files_dir_readable() { 200 | $temp_files_dir = $this->options->get( 'temp_files_dir' ); 201 | $label = sprintf( __( "Checking if web server can read from Temp Files Directory: %s", 'simply-static' ), $temp_files_dir ); 202 | return array( 203 | 'label' => $label, 204 | 'test' => is_readable( $temp_files_dir ) 205 | ); 206 | } 207 | 208 | public function is_temp_files_dir_writeable() { 209 | $temp_files_dir = $this->options->get( 'temp_files_dir' ); 210 | $label = sprintf( __( "Checking if web server can write to Temp Files Directory: %s", 'simply-static' ), $temp_files_dir ); 211 | return array( 212 | 'label' => $label, 213 | 'test' => is_writable( $temp_files_dir ) 214 | ); 215 | } 216 | 217 | public function is_local_dir_writeable() { 218 | $local_dir = $this->options->get( 'local_dir' ); 219 | $label = sprintf( __( "Checking if web server can write to Local Directory: %s", 'simply-static' ), $local_dir ); 220 | return array( 221 | 'label' => $label, 222 | 'test' => is_writable( $local_dir ) 223 | ); 224 | } 225 | 226 | public function user_can_delete() { 227 | $label = __( 'Checking if MySQL user has DELETE privilege', 'simply-static' ); 228 | return array( 229 | 'label' => $label, 230 | 'test' => Sql_Permissions::instance()->can( 'delete' ) 231 | ); 232 | } 233 | 234 | public function user_can_insert() { 235 | $label = __( 'Checking if MySQL user has INSERT privilege', 'simply-static' ); 236 | return array( 237 | 'label' => $label, 238 | 'test' => Sql_Permissions::instance()->can( 'insert' ) 239 | ); 240 | } 241 | 242 | public function user_can_select() { 243 | $label = __( 'Checking if MySQL user has SELECT privilege', 'simply-static' ); 244 | return array( 245 | 'label' => $label, 246 | 'test' => Sql_Permissions::instance()->can( 'select' ) 247 | ); 248 | } 249 | 250 | public function user_can_create() { 251 | $label = __( 'Checking if MySQL user has CREATE privilege', 'simply-static' ); 252 | return array( 253 | 'label' => $label, 254 | 'test' => Sql_Permissions::instance()->can( 'create' ) 255 | ); 256 | } 257 | 258 | public function user_can_alter() { 259 | $label = __( 'Checking if MySQL user has ALTER privilege', 'simply-static' ); 260 | return array( 261 | 'label' => $label, 262 | 'test' => Sql_Permissions::instance()->can( 'alter' ) 263 | ); 264 | } 265 | 266 | public function user_can_drop() { 267 | $label = __( 'Checking if MySQL user has DROP privilege', 'simply-static' ); 268 | return array( 269 | 'label' => $label, 270 | 'test' => Sql_Permissions::instance()->can( 'drop' ) 271 | ); 272 | } 273 | 274 | public function php_version() { 275 | $label = sprintf( __( 'Checking if PHP version >= %s', 'simply-static' ), self::$min_version['php'] ); 276 | return array( 277 | 'label' => $label, 278 | 'test' => version_compare( phpversion(), self::$min_version['php'], '>=' ), 279 | 'message' => phpversion(), 280 | ); 281 | } 282 | 283 | public function has_curl() { 284 | $label = __( 'Checking for cURL support', 'simply-static' ); 285 | 286 | if ( is_callable( 'curl_version' ) ) { 287 | $version = curl_version(); 288 | $test = version_compare( $version['version'], self::$min_version['curl'], '>=' ); 289 | $message = $version['version']; 290 | } else { 291 | $test = false; 292 | $message = null; 293 | } 294 | 295 | return array( 296 | 'label' => $label, 297 | 'test' => $test, 298 | 'message' => $message, 299 | ); 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /includes/tasks/class-ss-fetch-urls-task.php: -------------------------------------------------------------------------------- 1 | archive_dir = $this->options->get_archive_dir(); 18 | $this->archive_start_time = $this->options->get( 'archive_start_time' ); 19 | } 20 | 21 | /** 22 | * Fetch and save pages for the static archive 23 | * @return boolean|WP_Error true if done, false if not done, WP_Error if error 24 | */ 25 | public function perform() { 26 | $batch_size = 10; 27 | 28 | $static_pages = Page::query() 29 | ->where( 'last_checked_at < ? OR last_checked_at IS NULL', $this->archive_start_time ) 30 | ->limit( $batch_size ) 31 | ->find(); 32 | $pages_remaining = Page::query() 33 | ->where( 'last_checked_at < ? OR last_checked_at IS NULL', $this->archive_start_time ) 34 | ->count(); 35 | $total_pages = Page::query()->count(); 36 | $pages_processed = $total_pages - $pages_remaining; 37 | Util::debug_log( "Total pages: " . $total_pages . '; Pages remaining: ' . $pages_remaining ); 38 | 39 | while ( $static_page = array_shift( $static_pages ) ) { 40 | Util::debug_log( "URL: " . $static_page->url ); 41 | 42 | $excludable = $this->find_excludable( $static_page ); 43 | if ( $excludable !== false ) { 44 | $save_file = $excludable['do_not_save'] !== '1'; 45 | $follow_urls = $excludable['do_not_follow'] !== '1'; 46 | Util::debug_log( "Excludable found: URL: " . $excludable['url'] . ' DNS: ' . $excludable['do_not_save'] . ' DNF: ' .$excludable['do_not_follow'] ); 47 | } else { 48 | $save_file = true; 49 | $follow_urls = true; 50 | Util::debug_log( "URL is not being excluded" ); 51 | } 52 | 53 | // If we're not saving a copy of the page or following URLs on that 54 | // page, then we don't need to bother fetching it. 55 | if ( $save_file === false && $follow_urls === false ) { 56 | Util::debug_log( "Skipping URL because it is no-save and no-follow" ); 57 | $static_page->last_checked_at = Util::formatted_datetime(); 58 | $static_page->set_status_message( __( "Do not save or follow", 'simply-static' ) ); 59 | $static_page->save(); 60 | continue; 61 | } else { 62 | $success = Url_Fetcher::instance()->fetch( $static_page ); 63 | } 64 | 65 | if ( ! $success ) { 66 | continue; 67 | } 68 | 69 | // If we get a 30x redirect... 70 | if ( in_array( $static_page->http_status_code, array( 301, 302, 303, 307, 308 ) ) ) { 71 | $this->handle_30x_redirect( $static_page, $save_file, $follow_urls ); 72 | continue; 73 | } 74 | 75 | // Not a 200 for the response code? Move on. 76 | if ( $static_page->http_status_code != 200 ) { 77 | continue; 78 | } 79 | 80 | $this->handle_200_response( $static_page, $save_file, $follow_urls ); 81 | } 82 | 83 | $message = sprintf( __( "Fetched %d of %d pages/files", 'simply-static' ), $pages_processed, $total_pages ); 84 | $this->save_status_message( $message ); 85 | 86 | // if we haven't processed any additional pages, we're done 87 | return $pages_remaining == 0; 88 | } 89 | 90 | /** 91 | * Process the response for a 200 response (success) 92 | * @param Simply_Static\Page $static_page Record to update 93 | * @param boolean $save_file Save a static copy of the page? 94 | * @param boolean $follow_urls Save found URLs to database? 95 | * @return void 96 | */ 97 | protected function handle_200_response( $static_page, $save_file, $follow_urls ) { 98 | if ( $save_file || $follow_urls ) { 99 | Util::debug_log( "Extracting URLs and replacing URLs in the static file" ); 100 | // Fetch all URLs from the page and add them to the queue... 101 | $extractor = new Url_Extractor( $static_page ); 102 | $urls = $extractor->extract_and_update_urls(); 103 | } 104 | 105 | if ( $follow_urls ) { 106 | Util::debug_log( "Adding " . sizeof( $urls ) . " URLs to the queue" ); 107 | foreach ( $urls as $url ) { 108 | $this->set_url_found_on( $static_page, $url ); 109 | } 110 | } else { 111 | Util::debug_log( "Not following URLs from this page" ); 112 | $static_page->set_status_message( __( "Do not follow", 'simply-static' ) ); 113 | } 114 | 115 | $file = $this->archive_dir . $static_page->file_path; 116 | if ( $save_file ) { 117 | Util::debug_log( "We're saving this URL; keeping the static file" ); 118 | $sha1 = sha1_file( $file ); 119 | 120 | // if the content is identical, move on to the next file 121 | if ( $static_page->is_content_identical( $sha1 ) ) { 122 | // continue; 123 | } else { 124 | $static_page->set_content_hash( $sha1 ); 125 | } 126 | } else { 127 | Util::debug_log( "Not saving this URL; deleting the static file" ); 128 | unlink( $file ); // delete saved file 129 | $static_page->file_path = null; 130 | $static_page->set_status_message( __( "Do not save", 'simply-static' ) ); 131 | } 132 | 133 | $static_page->save(); 134 | } 135 | 136 | /** 137 | * Process the response to a 30x redirection 138 | * @param Simply_Static\Page $static_page Record to update 139 | * @param boolean $save_file Save a static copy of the page? 140 | * @param boolean $follow_urls Save redirect URL to database? 141 | * @return void 142 | */ 143 | protected function handle_30x_redirect( $static_page, $save_file, $follow_urls ) { 144 | $origin_url = Util::origin_url(); 145 | $destination_url = $this->options->get_destination_url(); 146 | $current_url = $static_page->url; 147 | $redirect_url = $static_page->redirect_url; 148 | 149 | Util::debug_log( "redirect_url: " . $redirect_url ); 150 | 151 | // convert our potentially relative URL to an absolute URL 152 | $redirect_url = Util::relative_to_absolute_url( $redirect_url, $current_url ); 153 | 154 | if ( $redirect_url ) { 155 | // WP likes to 301 redirect `/path` to `/path/` -- we want to 156 | // check for this and just add the trailing slashed version 157 | if ( $redirect_url === trailingslashit( $current_url ) ) { 158 | 159 | Util::debug_log( "This is a redirect to a trailing slashed version of the same page; adding new URL to the queue" ); 160 | $this->set_url_found_on( $static_page, $redirect_url ); 161 | 162 | // Don't create a redirect page if it's just a redirect from 163 | // http to https. Instead just add the new url to the queue. 164 | // TODO: Make this less horrible. 165 | } else if ( 166 | Util::strip_index_filenames_from_url( Util::remove_params_and_fragment( Util::strip_protocol_from_url( $redirect_url ) ) ) === 167 | Util::strip_index_filenames_from_url( Util::remove_params_and_fragment( Util::strip_protocol_from_url( $current_url ) ) ) ) { 168 | 169 | Util::debug_log( "This looks like a redirect from http to https (or visa versa); adding new URL to the queue" ); 170 | $this->set_url_found_on( $static_page, $redirect_url ); 171 | 172 | } else { 173 | // check if this is a local URL 174 | if ( Util::is_local_url( $redirect_url ) ) { 175 | 176 | if ( $follow_urls ) { 177 | Util::debug_log( "Redirect URL is on the same domain; adding the URL to the queue" ); 178 | $this->set_url_found_on( $static_page, $redirect_url ); 179 | } else { 180 | Util::debug_log( "Not following the redirect URL for this page" ); 181 | $static_page->set_status_message( __( "Do not follow", 'simply-static' ) ); 182 | } 183 | // and update the URL 184 | $redirect_url = str_replace( $origin_url, $destination_url, $redirect_url ); 185 | 186 | } 187 | 188 | if ( $save_file ) { 189 | Util::debug_log( "Creating a redirect page" ); 190 | 191 | $view = new View(); 192 | 193 | $content = $view->set_template( 'redirect' ) 194 | ->assign( 'redirect_url', $redirect_url ) 195 | ->render_to_string(); 196 | 197 | $filename = $this->save_static_page_content_to_file( $static_page, $content ); 198 | if ( $filename ) { 199 | $static_page->file_path = $filename; 200 | } 201 | 202 | $sha1 = sha1_file( $this->archive_dir . $filename ); 203 | 204 | // if the content is identical, move on to the next file 205 | if ( $static_page->is_content_identical( $sha1 ) ) { 206 | // continue; 207 | } else { 208 | $static_page->set_content_hash( $sha1 ); 209 | } 210 | } else { 211 | Util::debug_log( "Not creating a redirect page" ); 212 | $static_page->set_status_message( __( "Do not save", 'simply-static' ) ); 213 | } 214 | 215 | $static_page->save(); 216 | } 217 | } 218 | } 219 | 220 | protected function find_excludable( $static_page ) { 221 | $url = $static_page->url; 222 | $excludables = array(); 223 | foreach ( $this->options->get( 'urls_to_exclude' ) as $excludable ) { 224 | // using | as the delimiter for regex instead of the traditional / 225 | // because | won't show up in a path (it would have to be url-encoded) 226 | $regex = '|' . $excludable['url'] . '|'; 227 | $result = preg_match( $regex, $url ); 228 | if ( $result === 1 ) { 229 | return $excludable; 230 | } 231 | } 232 | return false; 233 | } 234 | 235 | /** 236 | * Set ID for which page a URL was found on (& create page if not in DB yet) 237 | * 238 | * Given a URL, find the associated Simply_Static\Page, and then set the ID 239 | * for which page it was found on if the ID isn't yet set or if the record 240 | * hasn't been updated in this instance of static generation yet. 241 | * @param Simply_Static\Page $static_page The record for the parent page 242 | * @param string $child_url The URL of the child page 243 | * @param string $start_time Static generation start time 244 | * @return void 245 | */ 246 | protected function set_url_found_on( $static_page, $child_url ) { 247 | $child_static_page = Page::query()->find_or_create_by( 'url' , $child_url ); 248 | if ( $child_static_page->found_on_id === null || $child_static_page->updated_at < $this->archive_start_time ) { 249 | $child_static_page->found_on_id = $static_page->id; 250 | $child_static_page->save(); 251 | } 252 | } 253 | 254 | /** 255 | * Save the contents of a page to a file in our archive directory 256 | * @param Simply_Static\Page $static_page The Simply_Static\Page record 257 | * @param string $content The content of the page we want to save 258 | * @return string|null The file path of the saved file 259 | */ 260 | protected function save_static_page_content_to_file( $static_page, $content ) { 261 | $relative_filename = Url_Fetcher::instance()->create_directories_for_static_page( $static_page ); 262 | 263 | if ( $relative_filename ) { 264 | $file_path = $this->archive_dir . $relative_filename; 265 | 266 | $write = file_put_contents( $file_path, $content ); 267 | if ( $write === false ) { 268 | $static_page->set_error_message( 'Unable to write temporary file' ); 269 | } else { 270 | return $relative_filename; 271 | } 272 | } else { 273 | return null; 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /includes/class-ss-archive-creation-job.php: -------------------------------------------------------------------------------- 1 | options = Options::instance(); 46 | $this->task_list = apply_filters( 'simplystatic.archive_creation_job.task_list', array(), $this->options->get( 'delivery_method' ) ); 47 | 48 | if ( ! $this->is_job_done() ) { 49 | register_shutdown_function( array( $this, 'shutdown_handler' ) ); 50 | } 51 | 52 | parent::__construct(); 53 | } 54 | 55 | /** 56 | * Helper method for starting the Archive_Creation_Job 57 | * @return boolean true if we were able to successfully start generating an archive 58 | */ 59 | public function start() { 60 | if ( $this->is_job_done() ) { 61 | Util::debug_log( "Starting a job; no job is presently running" ); 62 | Util::debug_log( "Here's our task list: " . implode( ', ', $this->task_list ) ); 63 | 64 | global $blog_id; 65 | 66 | $first_task = $this->task_list[0]; 67 | $archive_name = join( '-', array( Plugin::SLUG, $blog_id, time() ) ); 68 | 69 | $this->options 70 | ->set( 'archive_status_messages', array() ) 71 | ->set( 'archive_name', $archive_name ) 72 | ->set( 'archive_start_time', Util::formatted_datetime() ) 73 | ->set( 'archive_end_time', null ) 74 | ->save(); 75 | 76 | Util::debug_log( "Pushing first task to queue: " . $first_task ); 77 | 78 | $this->push_to_queue( $first_task ) 79 | ->save() 80 | ->dispatch(); 81 | 82 | return true; 83 | } else { 84 | Util::debug_log( "Not starting; we're already in the middle of a job" ); 85 | // looks like we're in the middle of creating an archive... 86 | return false; 87 | } 88 | } 89 | 90 | /** 91 | * Perform the task at hand 92 | * 93 | * The way Archive_Creation_Job works is by taking a task name, performing 94 | * that task, and then either (a) returnning the current task name to 95 | * continue processing it (e.g. fetch more urls), (b) returning the next 96 | * task name if we're done with the current one, or (c) returning false if 97 | * we're done with our job, which then runs complete(). 98 | * 99 | * @param string $task Task name to process 100 | * @return false|string task name to process, or false if done 101 | */ 102 | protected function task( $task_name ) { 103 | $this->set_current_task( $task_name ); 104 | 105 | Util::debug_log( "Current task: " . $task_name ); 106 | 107 | // convert 'an_example' to 'An_Example_Task' 108 | $class_name = 'Simply_Static\\' . ucwords( $task_name ) . '_Task'; 109 | 110 | // this shouldn't ever happen, but just in case... 111 | if ( ! class_exists( $class_name ) ) { 112 | $this->save_status_message( "Class doesn't exist: " . $class_name, 'error' ); 113 | return false; 114 | } 115 | 116 | $task = new $class_name(); 117 | 118 | // attempt to perform the task 119 | try { 120 | Util::debug_log( "Performing task: " . $task_name ); 121 | $is_done = $task->perform(); 122 | } catch ( \Error $e ) { 123 | Util::debug_log( "Caught an error" ); 124 | return $this->error_occurred( $e ); 125 | } catch ( \Exception $e ) { 126 | Util::debug_log( "Caught an exception" ); 127 | return $this->exception_occurred( $e ); 128 | } 129 | 130 | if ( is_wp_error( $is_done ) ) { 131 | // we've hit an error, time to quit 132 | Util::debug_log( "We encountered a WP_Error" ); 133 | return $this->error_occurred( $is_done ); 134 | } else if ( $is_done === true ) { 135 | // finished current task, try to find the next one 136 | $next_task = $this->find_next_task(); 137 | if ( $next_task === null ) { 138 | Util::debug_log( "This task is done and there are no more tasks, time to complete the job" ); 139 | // we're done; returning false to remove item from queue 140 | return false; 141 | } else { 142 | Util::debug_log( "We've found our next task: " . $next_task ); 143 | // start the next task 144 | return $next_task; 145 | } 146 | } else { // $is_done === false 147 | Util::debug_log( "We're not done with the " . $task_name . " task yet" ); 148 | // returning current task name to continue processing 149 | return $task_name; 150 | } 151 | 152 | Util::debug_log( "We shouldn't have gotten here; returning false to remove the " . $task_name . " task from the queue" ); 153 | return false; // remove item from queue 154 | } 155 | 156 | /** 157 | * This is run at the end of the job, after task() has returned false 158 | * @return void 159 | */ 160 | protected function complete() { 161 | Util::debug_log( "Completing the job" ); 162 | 163 | $this->set_current_task( 'done' ); 164 | 165 | $end_time = Util::formatted_datetime(); 166 | $start_time = $this->options->get( 'archive_start_time' ); 167 | $duration = strtotime( $end_time ) - strtotime( $start_time ); 168 | $time_string = gmdate( "H:i:s", $duration ); 169 | 170 | $this->options->set( 'archive_end_time', $end_time ); 171 | 172 | $this->save_status_message( sprintf( __( 'Done! Finished in %s', 'simply-static' ), $time_string ) ); 173 | parent::complete(); 174 | } 175 | 176 | 177 | /** 178 | * Cancel the currently running job 179 | * @return void 180 | */ 181 | public function cancel() { 182 | if ( ! $this->is_job_done() ) { 183 | Util::debug_log( "Cancelling job; job is not done" ); 184 | 185 | if ( $this->is_queue_empty() ) { 186 | Util::debug_log( "The queue is empty, pushing the cancel task" ); 187 | // generally the queue shouldn't be empty when we get a request to 188 | // cancel, but if we do, add a cancel task and start processing it. 189 | // that should get the job back into a state where it can be 190 | // started again. 191 | $this->push_to_queue( 'cancel' ) 192 | ->save(); 193 | } else { 194 | Util::debug_log( "The queue isn't empty; overwriting current task with a cancel task" ); 195 | // unlock the process so that we can force our cancel task to process 196 | $this->unlock_process(); 197 | 198 | // overwrite whatever the current task is with the cancel task 199 | $batch = $this->get_batch(); 200 | $batch->data = array( 'cancel' ); 201 | $this->update( $batch->key, $batch->data ); 202 | } 203 | 204 | $this->dispatch(); 205 | } else { 206 | Util::debug_log( "Can't cancel; job is done" ); 207 | } 208 | } 209 | 210 | /** 211 | * Is the job done? 212 | * @return boolean True if done, false if not 213 | */ 214 | public function is_job_done() { 215 | $start_time = $this->options->get( 'archive_start_time' ); 216 | $end_time = $this->options->get( 'archive_end_time' ); 217 | // we're done if the start and end time are null (never run) or if 218 | // the start and end times are both set 219 | return ( $start_time == null && $end_time == null ) || ( $start_time != null && $end_time != null); 220 | } 221 | 222 | /** 223 | * Return the current task 224 | * @return string The current task 225 | */ 226 | public function get_current_task() { 227 | return $this->current_task; 228 | } 229 | 230 | /** 231 | * Set the current task name 232 | * @param stroing $task_name The name of the current task 233 | */ 234 | protected function set_current_task( $task_name ) { 235 | $this->current_task = $task_name; 236 | } 237 | 238 | /** 239 | * Find the next task on our task list 240 | * @return string|null The name of the next task, or null if none 241 | */ 242 | protected function find_next_task() { 243 | $task_name = $this->get_current_task(); 244 | $index = array_search( $task_name, $this->task_list ); 245 | if ( $index === false ) { 246 | return null; 247 | } 248 | 249 | $index += 1; 250 | if ( ( $index ) >= count( $this->task_list ) ) { 251 | return null; 252 | } else { 253 | return $this->task_list[ $index ]; 254 | } 255 | } 256 | 257 | /** 258 | * Add a message to the array of status messages for the job 259 | * 260 | * Providing a unique key for the message is optional. If one isn't 261 | * provided, the state_name will be used. Using the same key more than once 262 | * will overwrite previous messages. 263 | * 264 | * @param string $message Message to display about the status of the job 265 | * @param string $key Unique key for the message 266 | * @return void 267 | */ 268 | protected function save_status_message( $message, $key = null ) { 269 | $task_name = $key ?: $this->get_current_task(); 270 | $messages = $this->options->get( 'archive_status_messages' ); 271 | Util::debug_log( 'Status message: [' . $task_name . '] ' . $message ); 272 | 273 | $messages = Util::add_archive_status_message( $messages, $task_name, $message ); 274 | 275 | $this->options 276 | ->set( 'archive_status_messages', $messages ) 277 | ->save(); 278 | } 279 | 280 | /** 281 | * Add a status message about the exception and cancel the job 282 | * @param Exception $exception The exception that occurred 283 | * @return void 284 | */ 285 | protected function exception_occurred( $exception ) { 286 | Util::debug_log( "An exception occurred: " . $exception->getMessage() ); 287 | Util::debug_log( $exception ); 288 | $message = sprintf( __( "An exception occurred: %s", 'simply-static' ), $exception->getMessage() ); 289 | $this->save_status_message( $message, 'error' ); 290 | return 'cancel'; 291 | } 292 | 293 | /** 294 | * Add a status message about the error and cancel the job 295 | * @param WP_Error $wp_error The error that occurred 296 | * @return void 297 | */ 298 | protected function error_occurred( $wp_error ) { 299 | Util::debug_log( "An error occurred: " . $wp_error->get_error_message() ); 300 | Util::debug_log( $wp_error ); 301 | $message = sprintf( __( "An error occurred: %s", 'simply-static' ), $wp_error->get_error_message() ); 302 | $this->save_status_message( $message, 'error' ); 303 | return 'cancel'; 304 | } 305 | 306 | /** 307 | * Shutdown handler for fatal error reporting 308 | * @return void 309 | */ 310 | public function shutdown_handler() { 311 | // Note: this function must be public in order to function properly. 312 | $error = error_get_last(); 313 | // only trigger on actual errors, not warnings or notices 314 | if ( $error && in_array( $error['type'], array( E_ERROR, E_CORE_ERROR, E_USER_ERROR ) ) ) { 315 | $this->clear_scheduled_event(); 316 | $this->unlock_process(); 317 | $this->cancel_process(); 318 | 319 | $end_time = Util::formatted_datetime(); 320 | $this->options 321 | ->set( 'archive_end_time', $end_time ) 322 | ->save(); 323 | 324 | $error_message = '(' . $error['type'] . ') ' . $error['message']; 325 | $error_message .= ' in ' . $error['file'] . ''; 326 | $error_message .= ' on line ' . $error['line'] . ''; 327 | 328 | $message = sprintf( __( "Error: %s", 'simply-static' ), $error_message ); 329 | Util::debug_log( $message ); 330 | $this->save_status_message( $message, 'error' ); 331 | } 332 | } 333 | 334 | } 335 | -------------------------------------------------------------------------------- /includes/class-ss-query.php: -------------------------------------------------------------------------------- 1 | model = $model; 47 | } 48 | 49 | /** 50 | * Execute the query and return an array of models 51 | * @return array 52 | */ 53 | public function find() { 54 | global $wpdb; 55 | 56 | $model = $this->model; 57 | $query = $this->compose_select_query(); 58 | 59 | $rows = $wpdb->get_results( 60 | $query, 61 | ARRAY_A 62 | ); 63 | 64 | if ( $rows === null ) { 65 | return null; 66 | } else { 67 | $records = array(); 68 | 69 | foreach ( $rows as $row ) { 70 | $records[] = $model::initialize( $row ); 71 | } 72 | 73 | return $records; 74 | } 75 | } 76 | 77 | /** 78 | * First and return the first record matching the conditions 79 | * @return static|null An instance of the class, or null 80 | */ 81 | public function first() { 82 | global $wpdb; 83 | 84 | $model = $this->model; 85 | 86 | $this->limit(1); 87 | $query = $this->compose_select_query(); 88 | 89 | $attributes = $wpdb->get_row( 90 | $query, 91 | ARRAY_A 92 | ); 93 | 94 | if ( $attributes === null ) { 95 | return null; 96 | } else { 97 | return $model::initialize( $attributes ); 98 | } 99 | } 100 | 101 | /** 102 | * Find and return the first record matching the column name/value 103 | * 104 | * Example: find_by( 'id', 123 ) 105 | * @param string $column_name The name of the column to search on 106 | * @param string $value The value that the column should contain 107 | * @return static|null An instance of the class, or null 108 | */ 109 | public function find_by( $column_name, $value ) { 110 | global $wpdb; 111 | 112 | $model = $this->model; 113 | $this->where( array( $column_name => $value ) ); 114 | 115 | $query = $this->compose_select_query(); 116 | 117 | $attributes = $wpdb->get_row( 118 | $query, 119 | ARRAY_A 120 | ); 121 | 122 | if ( $attributes === null ) { 123 | return null; 124 | } else { 125 | return $model::initialize( $attributes ); 126 | } 127 | } 128 | 129 | /** 130 | * Find or initialize the first record with the given column name/value 131 | * 132 | * Finds the first record with the given column name/value, or initializes 133 | * an instance of the model if one is not found. 134 | * @param string $column_name The name of the column to search on 135 | * @param string $value The value that the column should contain 136 | * @return static An instance of the class (might not exist in db yet) 137 | */ 138 | public function find_or_initialize_by( $column_name, $value ) { 139 | global $wpdb; 140 | 141 | $model = $this->model; 142 | 143 | $obj = $this->find_by( $column_name, $value ); 144 | if ( ! $obj ) { 145 | $obj = $model::initialize( array( $column_name => $value ) ); 146 | } 147 | return $obj; 148 | } 149 | 150 | /** 151 | * Find the first record with the given column name/value, or create it 152 | * @param string $column_name The name of the column to search on 153 | * @param string $value The value that the column should contain 154 | * @return static An instance of the class (might not exist in db yet) 155 | */ 156 | public function find_or_create_by( $column_name, $value ) { 157 | $obj = $this->find_or_initialize_by( $column_name, $value ); 158 | if ( ! $obj->exists() ) { 159 | $obj->save(); 160 | } 161 | return $obj; 162 | } 163 | 164 | /** 165 | * Update all records to set the column name equal to the value 166 | * 167 | * string: 168 | * A single string, without additional args, is passed as-is to the query. 169 | * update_all( "widget_id = 2" ) 170 | * 171 | * assoc. array: 172 | * An associative array will use the keys as fields and the values as the 173 | * values to be updated. 174 | * update_all( array( 'widget_id' => 2, 'type' => 'sprocket' ) ) 175 | * @param mixed $arg See description 176 | * @return int|null The number of rows updated, or null if failure 177 | */ 178 | public function update_all( $arg ) { 179 | if ( func_num_args() > 1 ) { 180 | throw new \Exception( "Too many arguments passed" ); 181 | } 182 | 183 | global $wpdb; 184 | 185 | $query = $this->compose_update_query( $arg ); 186 | $rows_updated = $wpdb->query( $query ); 187 | 188 | return $rows_updated; 189 | } 190 | 191 | /** 192 | * Delete records matching a where query, replacing ? with $args 193 | * @return int|null The number of rows deleted, or null if failure 194 | */ 195 | public function delete_all() { 196 | global $wpdb; 197 | 198 | $query = $this->compose_query( 'DELETE FROM ' ); 199 | $rows_deleted = $wpdb->query( $query ); 200 | 201 | return $rows_deleted; 202 | } 203 | 204 | /** 205 | * Execute the query and return a count of records 206 | * @return int|null 207 | */ 208 | public function count() { 209 | global $wpdb; 210 | 211 | $query = $this->compose_select_query( 'COUNT(*)' ); 212 | 213 | return $wpdb->get_var( $query ); 214 | } 215 | 216 | /** 217 | * Set the maximum number of rows to return 218 | * @param integer $limit 219 | * @return self 220 | */ 221 | public function limit( $limit ) { 222 | $this->limit = $limit; 223 | return $this; 224 | } 225 | 226 | /** 227 | * Set the number of rows to skip before returning results 228 | * @param integer $offset 229 | * @return self 230 | */ 231 | public function offset( $offset ) { 232 | if ( $this->limit === null ) { 233 | throw new \Exception( "Cannot offset without limit" ); 234 | } 235 | 236 | $this->offset = $offset; 237 | return $this; 238 | } 239 | 240 | /** 241 | * Set the ordering for results 242 | * @param string $order 243 | * @return self 244 | */ 245 | public function order( $order ) { 246 | $this->order = $order; 247 | return $this; 248 | } 249 | 250 | /** 251 | * Add a where clause to the query 252 | * 253 | * string: 254 | * A single string, without additional args, is passed as-is to the query. 255 | * where( "widget_id = 2" ) 256 | * 257 | * assoc. array: 258 | * An associative array will use the keys as fields and the values as the 259 | * values to be searched for to create a condition. 260 | * where( array( 'widget_id' => 2, 'type' => 'sprocket' ) ) 261 | * 262 | * string + args: 263 | * A string with placeholders '?' and additional args will have the string 264 | * treated as a template and the remaining args inserted into the template 265 | * to create a condition. 266 | * where( 'widget_id > ? AND widget_id < ?', 12, 18 ) 267 | * @param mixed $arg See description 268 | * @return self 269 | */ 270 | public function where( $arg ) { 271 | if ( func_num_args() == 1 ) { 272 | if ( is_array( $arg ) ) { 273 | // add array of conditions to the "where" array 274 | foreach ( $arg as $column_name => $value ) { 275 | $this->where[] = self::where_sql( $column_name, $value ); 276 | } 277 | } else if ( is_string( $arg ) ) { 278 | // pass the string as-is to our "where" array 279 | $this->where[] = $arg; 280 | } else { 281 | throw new \Exception( "One argument provided and it was not a string or array" ); 282 | } 283 | } else if ( func_num_args() > 1 ) { 284 | $where_values = func_get_args(); 285 | $condition = array_shift( $where_values ); 286 | 287 | if ( is_string( $condition ) ) { 288 | // check that the number of args and ?'s matches 289 | if ( substr_count( $condition, '?' ) != sizeof( $where_values ) ) { 290 | throw new \Exception( "Number of arguments does not match number of placeholders (?'s)" ); 291 | } else { 292 | // create a condition to add to the "where" array 293 | foreach ( $where_values as $value ) { 294 | $condition = preg_replace( '/\?/', self::escape_and_quote( $value ), $condition, 1 ); 295 | } 296 | 297 | $this->where[] = $condition; 298 | } 299 | } else { 300 | throw new \Exception( "Multiple arguments provided but first arg was not a string" ); 301 | } 302 | } else { 303 | throw new \Exception( "No arguments provided" ); 304 | } 305 | 306 | return $this; 307 | } 308 | 309 | /** 310 | * Generate a SQL query for selecting records 311 | * @param string $fields Fields to select (null = all records) 312 | * @return string The SQL query for selecting records 313 | */ 314 | private function compose_select_query( $fields = null ) { 315 | $select = ''; 316 | 317 | if ( $fields ) { 318 | $select = $fields; 319 | } else { 320 | $select = '*'; 321 | } 322 | 323 | $statement = "SELECT {$select} FROM "; 324 | return $this->compose_query( $statement ); 325 | } 326 | 327 | /** 328 | * Generate a SQL query for updating records 329 | * 330 | * string: 331 | * A single string, without additional args, is passed as-is to the query. 332 | * compose_update_query( "widget_id = 2" ) 333 | * 334 | * assoc. array: 335 | * An associative array will use the keys as fields and the values as the 336 | * values to be updated to create a condition. 337 | * compose_update_query( array( 'widget_id' => 2, 'type' => 'sprocket' ) ) 338 | * @param mixed $arg See description 339 | * @return The SQL query for updating records 340 | */ 341 | private function compose_update_query( $arg ) { 342 | $values = ' SET '; 343 | 344 | if ( is_array( $arg ) ) { 345 | // add array of conditions to the "where" array 346 | foreach ( $arg as $column_name => $value ) { 347 | $value = self::escape_and_quote( $value ); 348 | $values .= "{$column_name} = $value "; 349 | } 350 | } else if ( is_string( $arg ) ) { 351 | // pass the string as-is to our "where" array 352 | $values .= $arg . ' '; 353 | } else { 354 | throw new \Exception( "Argument provided was not a string or array" ); 355 | } 356 | 357 | return $this->compose_query( 'UPDATE ', $values ); 358 | } 359 | 360 | /** 361 | * Generate a SQL query 362 | * $param string $statement SELECT *, UPDATE, etc. 363 | * @return string 364 | */ 365 | private function compose_query( $statement, $values = '' ) { 366 | $model = $this->model; 367 | $table = ' ' . $model::table_name(); 368 | $where = ''; 369 | $order = ''; 370 | $limit = ''; 371 | $offset = ''; 372 | 373 | foreach ( $this->where as $condition ) { 374 | $where .= ' AND ' . $condition; 375 | } 376 | 377 | if ( $where !== '' ) { 378 | $where = ' WHERE 1=1' . $where; 379 | } 380 | 381 | if ( $this->order ) { 382 | $order = ' ORDER BY ' . $this->order; 383 | } 384 | 385 | if ( $this->limit ) { 386 | $limit = ' LIMIT ' . $this->limit; 387 | } 388 | 389 | if ( $this->offset ) { 390 | $offset = ' OFFSET ' . $this->offset; 391 | } 392 | 393 | $query = "{$statement}{$table}{$values}${where}{$order}{$limit}{$offset}"; 394 | return $query; 395 | } 396 | 397 | /** 398 | * Generate a SQL fragment for use in WHERE x=y 399 | * @param string $column_name The name of the column 400 | * @param mixed $value The value for the column 401 | * @return string The SQL fragment to be used in WHERE x=y 402 | */ 403 | private static function where_sql( $column_name, $value ) { 404 | $where_sql = $column_name; 405 | $where_sql .= ( $value === null ) ? ' IS ' : ' = '; 406 | $where_sql .= self::escape_and_quote( $value ); 407 | return $where_sql; 408 | } 409 | 410 | private static function escape_and_quote( $value ) { 411 | if ( $value === null ) { 412 | return 'NULL'; 413 | } else { 414 | $value = esc_sql( $value ); 415 | 416 | if ( is_string( $value ) ) { 417 | return "'{$value}'"; 418 | } else { 419 | return $value; 420 | } 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /includes/libraries/wp-background-processing/classes/wp-background-process.php: -------------------------------------------------------------------------------- 1 | cron_hook_identifier = $this->identifier . '_cron'; 61 | $this->cron_interval_identifier = $this->identifier . '_cron_interval'; 62 | 63 | add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); 64 | add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); 65 | } 66 | 67 | /** 68 | * Dispatch 69 | * 70 | * @access public 71 | * @return void 72 | */ 73 | public function dispatch() { 74 | // Schedule the cron healthcheck. 75 | $this->schedule_event(); 76 | 77 | // Perform remote post. 78 | return parent::dispatch(); 79 | } 80 | 81 | /** 82 | * Push to queue 83 | * 84 | * @param mixed $data Data. 85 | * 86 | * @return $this 87 | */ 88 | public function push_to_queue( $data ) { 89 | $this->data[] = $data; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Save queue 96 | * 97 | * @return $this 98 | */ 99 | public function save() { 100 | $key = $this->generate_key(); 101 | 102 | if ( ! empty( $this->data ) ) { 103 | update_site_option( $key, $this->data ); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Update queue 111 | * 112 | * @param string $key Key. 113 | * @param array $data Data. 114 | * 115 | * @return $this 116 | */ 117 | public function update( $key, $data ) { 118 | if ( ! empty( $data ) ) { 119 | update_site_option( $key, $data ); 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Delete queue 127 | * 128 | * @param string $key Key. 129 | * 130 | * @return $this 131 | */ 132 | public function delete( $key ) { 133 | delete_site_option( $key ); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Generate key 140 | * 141 | * Generates a unique key based on microtime. Queue items are 142 | * given a unique key so that they can be merged upon save. 143 | * 144 | * @param int $length Length. 145 | * 146 | * @return string 147 | */ 148 | protected function generate_key( $length = 64 ) { 149 | $unique = md5( microtime() . rand() ); 150 | $prepend = $this->identifier . '_batch_'; 151 | 152 | return substr( $prepend . $unique, 0, $length ); 153 | } 154 | 155 | /** 156 | * Maybe process queue 157 | * 158 | * Checks whether data exists within the queue and that 159 | * the process is not already running. 160 | */ 161 | public function maybe_handle() { 162 | // Don't lock up other requests while processing 163 | session_write_close(); 164 | 165 | if ( $this->is_process_running() ) { 166 | // Background process already running. 167 | wp_die(); 168 | } 169 | 170 | if ( $this->is_queue_empty() ) { 171 | // No data to process. 172 | wp_die(); 173 | } 174 | 175 | check_ajax_referer( $this->identifier, 'nonce' ); 176 | 177 | $this->handle(); 178 | 179 | wp_die(); 180 | } 181 | 182 | /** 183 | * Is queue empty 184 | * 185 | * @return bool 186 | */ 187 | protected function is_queue_empty() { 188 | global $wpdb; 189 | 190 | $table = $wpdb->options; 191 | $column = 'option_name'; 192 | 193 | if ( is_multisite() ) { 194 | $table = $wpdb->sitemeta; 195 | $column = 'meta_key'; 196 | } 197 | 198 | $key = $this->identifier . '_batch_%'; 199 | 200 | $count = $wpdb->get_var( $wpdb->prepare( " 201 | SELECT COUNT(*) 202 | FROM {$table} 203 | WHERE {$column} LIKE %s 204 | ", $key ) ); 205 | 206 | return ( $count > 0 ) ? false : true; 207 | } 208 | 209 | /** 210 | * Is process running 211 | * 212 | * Check whether the current process is already running 213 | * in a background process. 214 | */ 215 | protected function is_process_running() { 216 | if ( get_site_transient( $this->identifier . '_process_lock' ) ) { 217 | // Process already running. 218 | return true; 219 | } 220 | 221 | return false; 222 | } 223 | 224 | /** 225 | * Lock process 226 | * 227 | * Lock the process so that multiple instances can't run simultaneously. 228 | * Override if applicable, but the duration should be greater than that 229 | * defined in the time_exceeded() method. 230 | */ 231 | protected function lock_process() { 232 | $this->start_time = time(); // Set start time of current process. 233 | 234 | $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute 235 | $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); 236 | 237 | set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); 238 | } 239 | 240 | /** 241 | * Unlock process 242 | * 243 | * Unlock the process so that other instances can spawn. 244 | * 245 | * @return $this 246 | */ 247 | protected function unlock_process() { 248 | delete_site_transient( $this->identifier . '_process_lock' ); 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Get batch 255 | * 256 | * @return stdClass Return the first batch from the queue 257 | */ 258 | protected function get_batch() { 259 | global $wpdb; 260 | 261 | $table = $wpdb->options; 262 | $column = 'option_name'; 263 | $key_column = 'option_id'; 264 | $value_column = 'option_value'; 265 | 266 | if ( is_multisite() ) { 267 | $table = $wpdb->sitemeta; 268 | $column = 'meta_key'; 269 | $key_column = 'meta_id'; 270 | $value_column = 'meta_value'; 271 | } 272 | 273 | $key = $this->identifier . '_batch_%'; 274 | 275 | $query = $wpdb->get_row( $wpdb->prepare( " 276 | SELECT * 277 | FROM {$table} 278 | WHERE {$column} LIKE %s 279 | ORDER BY {$key_column} ASC 280 | LIMIT 1 281 | ", $key ) ); 282 | 283 | $batch = new stdClass(); 284 | $batch->key = $query->$column; 285 | $batch->data = maybe_unserialize( $query->$value_column ); 286 | 287 | return $batch; 288 | } 289 | 290 | /** 291 | * Handle 292 | * 293 | * Pass each queue item to the task handler, while remaining 294 | * within server memory and time limit constraints. 295 | */ 296 | protected function handle() { 297 | $this->lock_process(); 298 | 299 | do { 300 | $batch = $this->get_batch(); 301 | 302 | foreach ( $batch->data as $key => $value ) { 303 | $task = $this->task( $value ); 304 | 305 | if ( false !== $task ) { 306 | $batch->data[ $key ] = $task; 307 | } else { 308 | unset( $batch->data[ $key ] ); 309 | } 310 | 311 | if ( $this->time_exceeded() || $this->memory_exceeded() ) { 312 | // Batch limits reached. 313 | break; 314 | } 315 | } 316 | 317 | // Update or delete current batch. 318 | if ( ! empty( $batch->data ) ) { 319 | $this->update( $batch->key, $batch->data ); 320 | } else { 321 | $this->delete( $batch->key ); 322 | } 323 | } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); 324 | 325 | $this->unlock_process(); 326 | 327 | // Start next batch or complete process. 328 | if ( ! $this->is_queue_empty() ) { 329 | $this->dispatch(); 330 | } else { 331 | $this->complete(); 332 | } 333 | 334 | wp_die(); 335 | } 336 | 337 | /** 338 | * Memory exceeded 339 | * 340 | * Ensures the batch process never exceeds 90% 341 | * of the maximum WordPress memory. 342 | * 343 | * @return bool 344 | */ 345 | protected function memory_exceeded() { 346 | $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory 347 | $current_memory = memory_get_usage( true ); 348 | $return = false; 349 | 350 | if ( $current_memory >= $memory_limit ) { 351 | $return = true; 352 | } 353 | 354 | return apply_filters( $this->identifier . '_memory_exceeded', $return ); 355 | } 356 | 357 | /** 358 | * Get memory limit 359 | * 360 | * @return int 361 | */ 362 | protected function get_memory_limit() { 363 | if ( function_exists( 'ini_get' ) ) { 364 | $memory_limit = ini_get( 'memory_limit' ); 365 | } else { 366 | // Sensible default. 367 | $memory_limit = '128M'; 368 | } 369 | 370 | if ( ! $memory_limit || -1 === $memory_limit ) { 371 | // Unlimited, set to 32GB. 372 | $memory_limit = '32000M'; 373 | } 374 | 375 | return intval( $memory_limit ) * 1024 * 1024; 376 | } 377 | 378 | /** 379 | * Time exceeded. 380 | * 381 | * Ensures the batch never exceeds a sensible time limit. 382 | * A timeout limit of 30s is common on shared hosting. 383 | * 384 | * @return bool 385 | */ 386 | protected function time_exceeded() { 387 | $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds 388 | $return = false; 389 | 390 | if ( time() >= $finish ) { 391 | $return = true; 392 | } 393 | 394 | return apply_filters( $this->identifier . '_time_exceeded', $return ); 395 | } 396 | 397 | /** 398 | * Complete. 399 | * 400 | * Override if applicable, but ensure that the below actions are 401 | * performed, or, call parent::complete(). 402 | */ 403 | protected function complete() { 404 | // Unschedule the cron healthcheck. 405 | $this->clear_scheduled_event(); 406 | } 407 | 408 | /** 409 | * Schedule cron healthcheck 410 | * 411 | * @access public 412 | * @param mixed $schedules Schedules. 413 | * @return mixed 414 | */ 415 | public function schedule_cron_healthcheck( $schedules ) { 416 | $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); 417 | 418 | if ( property_exists( $this, 'cron_interval' ) ) { 419 | $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval_identifier ); 420 | } 421 | 422 | // Adds every 5 minutes to the existing schedules. 423 | $schedules[ $this->identifier . '_cron_interval' ] = array( 424 | 'interval' => MINUTE_IN_SECONDS * $interval, 425 | 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), 426 | ); 427 | 428 | return $schedules; 429 | } 430 | 431 | /** 432 | * Handle cron healthcheck 433 | * 434 | * Restart the background process if not already running 435 | * and data exists in the queue. 436 | */ 437 | public function handle_cron_healthcheck() { 438 | if ( $this->is_process_running() ) { 439 | // Background process already running. 440 | exit; 441 | } 442 | 443 | if ( $this->is_queue_empty() ) { 444 | // No data to process. 445 | $this->clear_scheduled_event(); 446 | exit; 447 | } 448 | 449 | $this->handle(); 450 | 451 | exit; 452 | } 453 | 454 | /** 455 | * Schedule event 456 | */ 457 | protected function schedule_event() { 458 | if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { 459 | wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); 460 | } 461 | } 462 | 463 | /** 464 | * Clear scheduled event 465 | */ 466 | protected function clear_scheduled_event() { 467 | $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); 468 | 469 | if ( $timestamp ) { 470 | wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); 471 | } 472 | } 473 | 474 | /** 475 | * Cancel Process 476 | * 477 | * Stop processing queue items, clear cronjob and delete batch. 478 | * 479 | */ 480 | public function cancel_process() { 481 | if ( ! $this->is_queue_empty() ) { 482 | $batch = $this->get_batch(); 483 | 484 | $this->delete( $batch->key ); 485 | 486 | wp_clear_scheduled_hook( $this->cron_hook_identifier ); 487 | } 488 | 489 | } 490 | 491 | /** 492 | * Task 493 | * 494 | * Override this method to perform any actions required on each 495 | * queue item. Return the modified item for further processing 496 | * in the next pass through. Or, return false to remove the 497 | * item from the queue. 498 | * 499 | * @param mixed $item Queue item to iterate over. 500 | * 501 | * @return mixed 502 | */ 503 | abstract protected function task( $item ); 504 | 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /includes/libraries/wp-background-processing/license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Library General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /languages/simply-static-fr_FR.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 Code of Conduct 2 | # This file is distributed under the GPL-2.0+. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Simply Static 1.3.5\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/simply-static\n" 7 | "POT-Creation-Date: 2016-04-19 21:23:17+00:00\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "PO-Revision-Date: 2016-04-20 16:48+0100\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: Tips02 \n" 14 | "X-Generator: Poedit 1.5.7\n" 15 | "X-Poedit-KeywordsList: __;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;" 16 | "_nx_noop:1,2,3c;esc_attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;" 17 | "esc_html_x:1,2c\n" 18 | "Language: fr\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | "X-Poedit-SourceCharset: UTF-8\n" 21 | "X-Poedit-Basepath: ../\n" 22 | "X-Textdomain-Support: yes\n" 23 | "X-Poedit-SearchPath-0: .\n" 24 | 25 | #: includes/class-simply-static-archive-creator.php:232 26 | msgid "Unable to create ZIP archive" 27 | msgstr "Impossible de créer l'archive ZIP" 28 | 29 | #: includes/class-simply-static-archive-creator.php:295 30 | #: includes/class-simply-static-archive-creator.php:302 31 | msgid "Could not delete temporary file or directory: %s" 32 | msgstr "Impossible de supprimer les fichiers temporaires ou le répertoire : %s" 33 | 34 | #: includes/class-simply-static-archive-manager.php:105 35 | msgid "An unknown error has occurred" 36 | msgstr "Une erreur inconnue est survenue" 37 | 38 | #: includes/class-simply-static-archive-manager.php:280 39 | msgid "Setting up" 40 | msgstr "Mise en place" 41 | 42 | #: includes/class-simply-static-archive-manager.php:320 43 | msgid "Fetched %d of %d pages/files" 44 | msgstr "%d pages/fichiers sur %d générés" 45 | 46 | #: includes/class-simply-static-archive-manager.php:349 47 | msgid "ZIP archive created: " 48 | msgstr "Archive ZIP créée : " 49 | 50 | #: includes/class-simply-static-archive-manager.php:350 51 | msgid "Click here to download" 52 | msgstr "Cliquez-ici pour la télécharger" 53 | 54 | #: includes/class-simply-static-archive-manager.php:362 55 | msgid "Copied %d of %d files" 56 | msgstr "%d fichiers sur %d copiés" 57 | 58 | #: includes/class-simply-static-archive-manager.php:381 59 | msgid "Wrapping up" 60 | msgstr "Fin du processus" 61 | 62 | #: includes/class-simply-static-archive-manager.php:408 63 | msgid "Done! Finished in %s" 64 | msgstr "Effectué ! Fini en %s" 65 | 66 | #: includes/class-simply-static-archive-manager.php:418 67 | msgid "Cancelled" 68 | msgstr "Annulé" 69 | 70 | #: includes/class-simply-static-archive-manager.php:438 71 | msgid "Error: %s" 72 | msgstr "Erreur : %s" 73 | 74 | #: includes/class-simply-static-url-fetcher.php:22 75 | msgid "Attempting to fetch remote URL: %s" 76 | msgstr "Tentative de parcours de l'URL distante : %s" 77 | 78 | #: includes/class-simply-static-view.php:132 79 | msgid "Can't find view template: %s" 80 | msgstr "Ne peut trouver le modèle de vue : %s" 81 | 82 | #: includes/class-simply-static-view.php:138 83 | msgid "Can't find view layout: %s" 84 | msgstr "Ne peut trouver la mise en page : %s" 85 | 86 | #: includes/class-simply-static.php:198 includes/class-simply-static.php:217 87 | msgid "Simply Static Settings" 88 | msgstr "Paramètres Simply Static" 89 | 90 | #. Plugin Name of the plugin/theme 91 | msgid "Simply Static" 92 | msgstr "" 93 | 94 | #: includes/class-simply-static.php:208 95 | msgid "Generate Static Site" 96 | msgstr "Générer le Site Statique" 97 | 98 | #: includes/class-simply-static.php:209 99 | msgid "Generate" 100 | msgstr "Générer" 101 | 102 | #: includes/class-simply-static.php:218 103 | msgid "Settings" 104 | msgstr "Paramètres" 105 | 106 | #: includes/class-simply-static.php:318 107 | msgid "Settings saved." 108 | msgstr "Paramètres sauvegardés." 109 | 110 | #: includes/class-simply-static.php:378 111 | msgid "Destination URL cannot be blank" 112 | msgstr "Le répertoire de destination ne peut être vide" 113 | 114 | #: includes/class-simply-static.php:383 115 | msgid "Temporary Files Directory cannot be blank" 116 | msgstr "Le répertoire temporaire ne peut être vide" 117 | 118 | #: includes/class-simply-static.php:387 119 | msgid "Temporary Files Directory is not writeable: %s" 120 | msgstr "Impossible d'écrire dans le répertoire des fichiers : %s" 121 | 122 | #: includes/class-simply-static.php:390 123 | msgid "Temporary Files Directory does not exist: %s" 124 | msgstr "Le répertoire temporaire n'existe pas : %s" 125 | 126 | #: includes/class-simply-static.php:396 127 | msgid "" 128 | "Your site does not have a permalink structure set. You can select one on the Permalink Settings page." 130 | msgstr "" 131 | "Votre site n'a pas la structure des permaliens activée. Vous pouvez en " 132 | "sélectionner une sur la page des paramètres des Permaliens." 133 | 134 | #: includes/class-simply-static.php:401 135 | msgid "" 136 | "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." 139 | msgstr "" 140 | "Votre serveur n'a pas l'extension PHP zip activé. Rendez vous sur la page PHP de l'extension " 142 | "zip pour plus d'information pour l'activer." 143 | 144 | #: includes/class-simply-static.php:409 145 | msgid "Local Directory cannot be blank" 146 | msgstr "Le répertoire local ne peut pas être vide" 147 | 148 | #: includes/class-simply-static.php:413 149 | msgid "Local Directory is not writeable: %s" 150 | msgstr "Il n'est pas possible d'écrire dans le répertoire local : %s" 151 | 152 | #: includes/class-simply-static.php:416 153 | msgid "Local Directory does not exist: %s" 154 | msgstr "Le répertoire local n'existe pas : %s" 155 | 156 | #: includes/class-simply-static.php:424 157 | msgid "An Additional URL does not start with %s: %s" 158 | msgstr "Une URL additionnelle ne commence pas par %s: %s" 159 | 160 | #: includes/class-simply-static.php:433 161 | msgid "" 162 | "An Additional File or Directory is not located within an expected directory: " 163 | "%s
It should be in one of these directories (or a subdirectory):
%s
%s
%s" 165 | msgstr "" 166 | "Un fichier ou un répertoire additionnel ne peut être localisé dans le " 167 | "répertoire attendu : %s
Cela pourrait être un de ces répertoires (ou " 168 | "sous-répertoire) :
%s
%s
" 169 | "%s" 170 | 171 | #: views/_export_log.php:8 172 | msgid "Code" 173 | msgstr "" 174 | 175 | #: views/_export_log.php:9 176 | msgid "URL" 177 | msgstr "" 178 | 179 | #: views/_export_log.php:10 180 | msgid "Found on" 181 | msgstr "Trouvé sur" 182 | 183 | #: views/_export_log.php:12 184 | msgid "Errors (%d)" 185 | msgstr "Erreurs (%d)" 186 | 187 | #: views/_export_log.php:61 188 | msgid "1xx Informational:" 189 | msgstr "1xx Informationnel :" 190 | 191 | #: views/_export_log.php:62 192 | msgid "2xx Success:" 193 | msgstr "2xx Succès :" 194 | 195 | #: views/_export_log.php:63 196 | msgid "3xx Redirection:" 197 | msgstr "3xx Redirection :" 198 | 199 | #: views/_export_log.php:64 200 | msgid "4xx Client Error:" 201 | msgstr "4xx Erreur Client :" 202 | 203 | #: views/_export_log.php:65 204 | msgid "5xx Server Error:" 205 | msgstr "5xx Erruer Serveur :" 206 | 207 | #: views/_export_log.php:66 208 | msgid "" 209 | "More information on HTTP status codes is available on Wikipedia." 211 | msgstr "" 212 | "Plus d'infos sur les codes de statuts HTTP disponibles sur Wikipedia." 214 | 215 | #: views/generate.php:7 216 | msgid "Simply Static › Generate" 217 | msgstr "Simply Static › Générer" 218 | 219 | #: views/generate.php:15 220 | msgid "" 221 | "Uh oh, we've found an issue that prevents us from creating a static copy of " 222 | "your site." 223 | msgstr "" 224 | "Oups, nous avons eu un problème qui nous a empêché de créer une copie " 225 | "statique de votre site." 226 | 227 | #: views/generate.php:19 228 | msgid "Generate Static Files" 229 | msgstr "Générer les Fichiers Statiques" 230 | 231 | #: views/generate.php:21 232 | msgid "Pause" 233 | msgstr "" 234 | 235 | #: views/generate.php:23 236 | msgid "Resume" 237 | msgstr "Reprendre" 238 | 239 | #: views/generate.php:25 240 | msgid "Cancel" 241 | msgstr "Annuler" 242 | 243 | #: views/generate.php:30 244 | msgid "Activity Log" 245 | msgstr "Journal d'activité" 246 | 247 | #: views/generate.php:35 248 | msgid "Export Log" 249 | msgstr "Exporter le Journal" 250 | 251 | #: views/layouts/admin.php:26 252 | msgid "Like this plugin?" 253 | msgstr "Vous aimez ce plugin ?" 254 | 255 | #: views/layouts/admin.php:29 256 | msgid "" 257 | "Join the mailing list to be notified when new features and plugins are " 258 | "released." 259 | msgstr "" 260 | "Rejoignez la mailing list pour être informé des nouvelles fonctionnalités et " 261 | "versions du plugin." 262 | 263 | #: views/layouts/admin.php:35 264 | msgid "email address" 265 | msgstr "Adresse email" 266 | 267 | #: views/layouts/admin.php:38 268 | msgid "Subscribe" 269 | msgstr "S'inscrire" 270 | 271 | #: views/redirect.php:4 272 | msgid "Redirecting..." 273 | msgstr "Redirection..." 274 | 275 | #: views/settings.php:7 276 | msgid "Simply Static › Settings" 277 | msgstr "Simply Static › Paramètres " 278 | 279 | #: views/settings.php:17 280 | msgid "General" 281 | msgstr "Général" 282 | 283 | #: views/settings.php:18 284 | msgid "Advanced" 285 | msgstr "Avancé" 286 | 287 | #: views/settings.php:27 288 | msgid "Origin URL" 289 | msgstr "URL d'origine" 290 | 291 | #: views/settings.php:34 292 | msgid "" 293 | "This is the URL of your WordPress installation. You can edit the URL on the General Settings page." 295 | msgstr "" 296 | "Ceci est l'URL de votre installation WordPress. Vous pouvez éditer l'URL sur " 297 | "la page des Paramètres généraux." 298 | 299 | #: views/settings.php:39 300 | msgid "Destination URL" 301 | msgstr "URL de Destination" 302 | 303 | #: views/settings.php:47 304 | msgid "" 305 | "This is the URL where your static site will live. When generating your " 306 | "static site, all links to the Origin URL will be replaced with the " 307 | "Destination URL." 308 | msgstr "" 309 | "Ceci est l'URL où se trouve votre site statique. Quand vous générez votre " 310 | "site statique, tous les liens de l'URL d'Origine seront remplacés par ceux " 311 | "de l'URL de Destination." 312 | 313 | #: views/settings.php:52 314 | msgid "Delivery Method" 315 | msgstr "Méthode utilisée" 316 | 317 | #: views/settings.php:55 318 | msgid "ZIP Archive" 319 | msgstr "Archive ZIP" 320 | 321 | #: views/settings.php:56 views/settings.php:74 322 | msgid "Local Directory" 323 | msgstr "Répertoire local" 324 | 325 | #: views/settings.php:63 326 | msgid "" 327 | "Saving your static files to a ZIP archive is Simply Static's default " 328 | "delivery method. After generating your static files you will be prompted to " 329 | "download the ZIP archive." 330 | msgstr "" 331 | "La sauvegarde de vos fichiers dans une archive ZIP est la méthode par défaut " 332 | "de Simply Static. Après avoir généré vos fichiers statiques il vous sera " 333 | "demandé de télécharger l'archive ZIP." 334 | 335 | #: views/settings.php:69 336 | msgid "" 337 | "Saving your static files to a local directory is useful if you want to serve " 338 | "your static files from the same server as your WordPress installation. " 339 | "WordPress can live on a subdomain (e.g. wordpress.example.com) while your " 340 | "static files are served from your primary domain (e.g. www.example.com)." 341 | msgstr "" 342 | "Sauver vos fichiers dans un répertoire local est pratique si vous souhaitez " 343 | "servir vos fichiers statiques depuis le même serveur que votre installation. " 344 | "WordPress peut être installé sur un sous-domaine (ex : wordpress.exemple.fr) " 345 | "tandis que vos fichiers statiques sont servis depuis votre domaine principal " 346 | "(ex : www.exemple.fr)." 347 | 348 | #: views/settings.php:79 349 | msgid "" 350 | "This is the directory where your static files will be saved. The directory " 351 | "must exist and be writeable by the webserver." 352 | msgstr "" 353 | "Ceci est le répertoire où vos fichiers seront sauvegardés. Le répertoire " 354 | "doit exister et il doit être possible d'y écrire depuis le serveur web." 355 | 356 | #: views/settings.php:86 views/settings.php:151 357 | msgid "Save Changes" 358 | msgstr "Sauvegarder les changements" 359 | 360 | #: views/settings.php:100 361 | msgid "Temporary Files Directory" 362 | msgstr "Répertoire temporaire des fichiers" 363 | 364 | #: views/settings.php:105 365 | msgid "" 366 | "Your static files (and ZIP archives, if generated) are temporarily saved to " 367 | "this directory. This directory must exist and be writeable." 368 | msgstr "" 369 | "Vos fichiers statiques (et archives ZIP, si générées) sont sauvegardés " 370 | "temporairement dans ce répertoire. Ce dossier doit exister et il doit être " 371 | "possible d'y écrire depuis le serveur web." 372 | 373 | #: views/settings.php:113 374 | msgid "Delete temporary files" 375 | msgstr "Effacer les fichiers temporaires" 376 | 377 | #: views/settings.php:116 378 | msgid "" 379 | "Static files are temporarily saved to the directory above before being " 380 | "copied to their destination. These files can be deleted after the copy " 381 | "process, or you can keep them as a backup." 382 | msgstr "" 383 | "Les fichiers temporaires seront sauvegardés dans le répertoire ci-dessus " 384 | "avant d'être copiés vers leur destination. Ces fichiers peuvent être effacés " 385 | "après la procédure de copie, ou vous pouvez les conserver comme une copie de " 386 | "secours." 387 | 388 | #: views/settings.php:122 389 | msgid "Additional URLs" 390 | msgstr "URLs additionnelles" 391 | 392 | #: views/settings.php:127 393 | msgid "" 394 | "Simply Static will create a static copy of any page it can find a link to, " 395 | "starting at %s. If you want to create static copies of pages or files that " 396 | "aren't linked to, add the URLs here (one per line). Examples: " 397 | "%s or %s" 398 | msgstr "" 399 | "Simply Static créera une copie statique de toute page liée qu'il trouvera, " 400 | "commençant par %s. Si vous souhaitez créer une copie statique de pages ou " 401 | "fichiers qui ne sont pas liés, ajoutez leurs URLs ici (une par " 402 | "ligne). Exemples : %s ou %s" 403 | 404 | #: views/settings.php:129 405 | msgid "/hidden-page" 406 | msgstr "/page-non-liee" 407 | 408 | #: views/settings.php:130 409 | msgid "/images/secret.jpg" 410 | msgstr "" 411 | 412 | #: views/settings.php:136 413 | msgid "Additional Files and Directories" 414 | msgstr "Fichiers et Répertoires additionnels" 415 | 416 | #: views/settings.php:141 417 | msgid "" 418 | "Sometimes you may want to include additional files (such as files referenced " 419 | "via AJAX) or directories. Add the paths to those files or directories here " 420 | "(one per line). Examples: %s or %s" 421 | msgstr "" 422 | "Parfois vous pouvez vouloir inclure des fichiers additionnels (comme des " 423 | "fichiers référencés par AJAX) ou des répertoires. Ajoutez les chemins vers " 424 | "ces fichiers ou répertoires (un par ligne). Exemples : %s ou " 425 | "%s" 426 | 427 | #: views/settings.php:142 428 | msgid "additional-directory" 429 | msgstr "repertoire-additionnel" 430 | 431 | #: views/settings.php:143 432 | msgid "fancy.pdf" 433 | msgstr "chic.pdf" 434 | 435 | #. Plugin URI of the plugin/theme 436 | msgid "http://codeofconduct.co/simply-static" 437 | msgstr "" 438 | 439 | #. Description of the plugin/theme 440 | msgid "" 441 | "Produces a static HTML version of your WordPress install and adjusts URLs " 442 | "accordingly." 443 | msgstr "" 444 | "Produit une version HTML statique de votre installation WordPress et ajuste " 445 | "les URLs en conséquence." 446 | 447 | #. Author of the plugin/theme 448 | msgid "Code of Conduct" 449 | msgstr "" 450 | 451 | #. Author URI of the plugin/theme 452 | msgid "http://codeofconduct.co/" 453 | msgstr "" 454 | -------------------------------------------------------------------------------- /includes/class-ss-util.php: -------------------------------------------------------------------------------- 1 | $length + 3 ) ? ( substr( $string, 0, $length ) . $omission ) : $string; 69 | } 70 | 71 | /** 72 | * Use trailingslashit unless the string is empty 73 | * @return string 74 | */ 75 | public static function trailingslashit_unless_blank( $string ) { 76 | return $string === '' ? $string : trailingslashit( $string ); 77 | } 78 | 79 | /** 80 | * Dump an object to error_log 81 | * @param mixed $object Object to dump to the error log 82 | * @return void 83 | */ 84 | public static function error_log( $object = null ) { 85 | $contents = self::get_contents_from_object( $object ); 86 | error_log( $contents ); 87 | } 88 | 89 | /** 90 | * Delete the debug log 91 | * @return void 92 | */ 93 | public static function delete_debug_log() { 94 | $debug_file = self::get_debug_log_filename(); 95 | if ( file_exists( $debug_file ) ) { 96 | unlink( $debug_file ); 97 | } 98 | } 99 | 100 | /** 101 | * Save an object/string to the debug log 102 | * @param mixed $object Object to save to the debug log 103 | * @return void 104 | */ 105 | public static function debug_log( $object = null ) { 106 | $options = Options::instance(); 107 | if ( $options->get( 'debugging_mode' ) !== '1' ) { 108 | return; 109 | } 110 | 111 | $debug_file = self::get_debug_log_filename(); 112 | 113 | // add timestamp and newline 114 | $message = '[' . date( 'Y-m-d H:i:s' ) . '] '; 115 | 116 | $trace = debug_backtrace(); 117 | if ( isset( $trace[0]['file'] ) ) { 118 | $file = basename( $trace[0]['file'] ); 119 | if ( isset( $trace[0]['line'] ) ) { 120 | $file .= ':' . $trace[0]['line']; 121 | } 122 | $message .= '[' . $file . '] '; 123 | } 124 | 125 | $contents = self::get_contents_from_object( $object ); 126 | 127 | // get message onto a single line 128 | $contents = preg_replace( "/\r|\n/", "", $contents ); 129 | 130 | $message .= $contents . "\n"; 131 | 132 | // log the message to the debug file instead of the usual error_log location 133 | error_log( $message, 3, $debug_file ); 134 | } 135 | 136 | /** 137 | * Return the filename for the debug log 138 | * @return string Filename for the debug log 139 | */ 140 | public static function get_debug_log_filename() { 141 | return plugin_dir_path( dirname( __FILE__ ) ) . 'debug.txt'; 142 | } 143 | 144 | /** 145 | * Get contents of an object as a string 146 | * @param mixed $object Object to get string for 147 | * @return string String containing the contents of the object 148 | */ 149 | protected static function get_contents_from_object( $object ) { 150 | if ( is_string( $object ) ) { 151 | return $object; 152 | } 153 | 154 | ob_start(); 155 | var_dump( $object ); 156 | $contents = ob_get_contents(); 157 | ob_end_clean(); 158 | return $contents; 159 | } 160 | 161 | /** 162 | * Given a URL extracted from a page, return an absolute URL 163 | * 164 | * Takes a URL (e.g. /test) extracted from a page (e.g. http://foo.com/bar/) and 165 | * returns an absolute URL (e.g. http://foo.com/bar/test). Absolute URLs are 166 | * returned as-is. Exception: links beginning with a # (hash) are left as-is. 167 | * 168 | * A null value is returned in the event that the extracted_url is blank or it's 169 | * unable to be parsed. 170 | * 171 | * @param string $extracted_url Relative or absolute URL extracted from page 172 | * @param string $page_url URL of page 173 | * @return string|null Absolute URL, or null 174 | */ 175 | public static function relative_to_absolute_url( $extracted_url, $page_url ) { 176 | 177 | $extracted_url = trim( $extracted_url ); 178 | 179 | // we can't do anything with blank urls 180 | if ( $extracted_url === '' ) { 181 | return null; 182 | } 183 | 184 | // if we get a hash, e.g. href='#section-three', just return it as-is 185 | if ( strpos( $extracted_url, '#' ) === 0 ) { 186 | return $extracted_url; 187 | } 188 | 189 | // check for a protocol-less URL 190 | // (Note: there's a bug in PHP <= 5.4.7 where parsed URLs starting with // 191 | // are treated as a path. So we're doing this check upfront.) 192 | // http://php.net/manual/en/function.parse-url.php#example-4617 193 | if ( strpos( $extracted_url, '//' ) === 0 ) { 194 | 195 | // if this is a local URL, add the protocol to the URL 196 | if ( stripos( $extracted_url, '//' . self::origin_host() ) === 0 ) { 197 | $extracted_url = self::origin_scheme() . ':' . $extracted_url; 198 | } 199 | 200 | return $extracted_url; 201 | 202 | } 203 | 204 | $parsed_extracted_url = parse_url( $extracted_url ); 205 | 206 | // parse_url can sometimes return false; bail if it does 207 | if ( $parsed_extracted_url === false ) { 208 | return null; 209 | } 210 | 211 | // if no path, check for an ending slash; if there isn't one, add one 212 | if ( ! isset( $parsed_extracted_url['path'] ) ) { 213 | $clean_url = self::remove_params_and_fragment( $extracted_url ); 214 | $fragment = substr( $extracted_url, strlen( $clean_url ) ); 215 | $extracted_url = trailingslashit( $clean_url ) . $fragment; 216 | } 217 | 218 | if ( isset( $parsed_extracted_url['host'] ) ) { 219 | 220 | return $extracted_url; 221 | 222 | } elseif ( isset( $parsed_extracted_url['scheme'] ) ) { 223 | 224 | // examples of schemes without hosts: java:, data: 225 | return $extracted_url; 226 | 227 | } else { // no host on extracted page (might be relative url) 228 | 229 | $path = isset( $parsed_extracted_url['path'] ) ? $parsed_extracted_url['path'] : ''; 230 | 231 | $query = isset( $parsed_extracted_url['query'] ) ? '?' . $parsed_extracted_url['query'] : ''; 232 | $fragment = isset( $parsed_extracted_url['fragment'] ) ? '#' . $parsed_extracted_url['fragment'] : ''; 233 | 234 | // turn our relative url into an absolute url 235 | $extracted_url = \phpUri::parse( $page_url )->join( $path . $query . $fragment ); 236 | 237 | return $extracted_url; 238 | 239 | } 240 | } 241 | 242 | /** 243 | * Recursively create a path from one page to another 244 | * 245 | * Takes a path (e.g. /blog/foobar/) extracted from a page (e.g. /blog/page/3/) 246 | * and returns a path to get to the extracted page from the current page 247 | * (e.g. ./../../foobar/index.html). Since this is for offline use, the path 248 | * return will include a /index.html if the extracted path doesn't contain 249 | * an extension. 250 | * 251 | * The function recursively calls itself, cutting off sections of the page path 252 | * until the base matches the extracted path or it runs out of parts to remove, 253 | * then it builds out the path to the extracted page. 254 | * 255 | * @param string $extracted_path Relative or absolute URL extracted from page 256 | * @param string $page_path URL of page 257 | * @param int $iterations Number of times the page path has been chopped 258 | * @return string|null Absolute URL, or null 259 | */ 260 | public static function create_offline_path( $extracted_path, $page_path, $iterations = 0 ) { 261 | // We're done if we get a match between the path of the page and the extracted URL 262 | // OR if there are no more slashes to remove 263 | if ( strpos( $page_path, '/' ) === false || strpos( $extracted_path, $page_path ) === 0 ) { 264 | $extracted_path = substr( $extracted_path, strlen( $page_path ) ); 265 | $iterations = ( $iterations == 0 ) ? 0 : $iterations - 1; 266 | $new_path = '.' . str_repeat( '/..', $iterations ) . self::add_leading_slash( $extracted_path ); 267 | return $new_path; 268 | } else { 269 | // match everything before the last slash 270 | $pattern = '/(.*)\/[^\/]*$/'; 271 | // remove the last slash and anything after it 272 | $new_page_path = preg_replace( $pattern, '$1', $page_path ); 273 | return self::create_offline_path( $extracted_path, $new_page_path, ++$iterations ); 274 | } 275 | } 276 | 277 | /** 278 | * Check if URL starts with same URL as WordPress installation 279 | * 280 | * Both http and https are assumed to be the same domain. 281 | * 282 | * @param string $url URL to check 283 | * @return boolean true if URL is local, false otherwise 284 | */ 285 | public static function is_local_url( $url ) { 286 | return ( stripos( self::strip_protocol_from_url( $url ), self::origin_host() ) === 0 ); 287 | } 288 | 289 | /** 290 | * Get the path from a local URL, removing the protocol and host 291 | * @param string $url URL to strip protocol/host from 292 | * @return string URL sans protocol/host 293 | */ 294 | public static function get_path_from_local_url( $url ) { 295 | $url = self::strip_protocol_from_url( $url ); 296 | $url = str_replace( self::origin_host(), '', $url ); 297 | return $url; 298 | } 299 | 300 | /** 301 | * Returns a URL w/o the query string or fragment (i.e. nothing after the path) 302 | * @param string $url URL to remove query string/fragment from 303 | * @return string URL without query string/fragment 304 | */ 305 | public static function remove_params_and_fragment( $url ) { 306 | return preg_replace('/(\?|#).*/', '', $url); 307 | } 308 | 309 | /** 310 | * Converts a textarea into an array w/ each line being an entry in the array 311 | * @param string $textarea Textarea to convert 312 | * @return array Converted array 313 | */ 314 | public static function string_to_array( $textarea ) { 315 | // using preg_split to intelligently break at newlines 316 | // see: http://stackoverflow.com/questions/1483497/how-to-put-string-in-array-split-by-new-line 317 | $lines = preg_split( "/\r\n|\n|\r/", $textarea ); 318 | array_walk( $lines, 'trim' ); 319 | $lines = array_filter( $lines ); 320 | return $lines; 321 | } 322 | 323 | /** 324 | * Remove the //, http://, https:// protocols from a URL 325 | * @param string $url URL to remove protocol from 326 | * @return string URL sans http/https protocol 327 | */ 328 | public static function strip_protocol_from_url( $url ) { 329 | $pattern = '/^(https?:)?\/\//'; 330 | return preg_replace( $pattern, '', $url ); 331 | } 332 | 333 | /** 334 | * Remove index.html/index.php from a URL 335 | * @param string $url URL to remove index file from 336 | * @return string URL sans index file 337 | */ 338 | public static function strip_index_filenames_from_url( $url ) { 339 | $pattern = '/index.(html?|php)$/'; 340 | return preg_replace( $pattern, '', $url ); 341 | } 342 | 343 | /** 344 | * Get the current datetime formatted as a string for entry into MySQL 345 | * @return string MySQL formatted datetime 346 | */ 347 | public static function formatted_datetime() { 348 | return date( 'Y-m-d H:i:s' ); 349 | } 350 | 351 | /** 352 | * Similar to PHP's pathinfo(), but designed with URL paths in mind (instead of directories) 353 | * 354 | * Example: 355 | * $info = self::url_path_info( '/manual/en/function.pathinfo.php?test=true' ); 356 | * $info['dirname'] === '/manual/en/' 357 | * $info['basename'] === 'function.pathinfo.php' 358 | * $info['extension'] === 'php' 359 | * $info['filename'] === 'function.pathinfo' 360 | * @param string $path The URL path 361 | * @return array Array containing info on the parts of the path 362 | */ 363 | public static function url_path_info( $path ) { 364 | $info = array( 365 | 'dirname' => '', 366 | 'basename' => '', 367 | 'filename' => '', 368 | 'extension' => '' 369 | ); 370 | 371 | $path = self::remove_params_and_fragment( $path ); 372 | 373 | // everything after the last slash is the filename 374 | $last_slash_location = strrpos( $path, '/' ); 375 | if ( $last_slash_location === false ) { 376 | $info['basename'] = $path; 377 | } else { 378 | $info['dirname'] = substr( $path, 0, $last_slash_location+1 ); 379 | $info['basename'] = substr( $path, $last_slash_location+1 ); 380 | } 381 | 382 | // finding the dot for the extension 383 | $last_dot_location = strrpos( $info['basename'], '.' ); 384 | if ( $last_dot_location === false ) { 385 | $info['filename'] = $info['basename']; 386 | } else { 387 | $info['filename'] = substr( $info['basename'], 0, $last_dot_location ); 388 | $info['extension'] = substr( $info['basename'], $last_dot_location+1 ); 389 | } 390 | 391 | // substr sets false if it fails, we're going to reset those values to '' 392 | foreach ( $info as $name => $value ) { 393 | if ( $value === false ) { 394 | $info[ $name ] = ''; 395 | } 396 | } 397 | 398 | return $info; 399 | } 400 | 401 | /** 402 | * Ensure there is a single trailing directory separator on the path 403 | * @param string $path File path to add trailing directory separator to 404 | */ 405 | public static function add_trailing_directory_separator( $path ) { 406 | return self::remove_trailing_directory_separator( $path ) . DIRECTORY_SEPARATOR; 407 | } 408 | 409 | /** 410 | * Remove all trailing directory separators 411 | * @param string $path File path to remove trailing directory separators from 412 | */ 413 | public static function remove_trailing_directory_separator( $path ) { 414 | return rtrim( $path, DIRECTORY_SEPARATOR ); 415 | } 416 | 417 | /** 418 | * Ensure there is a single leading directory separator on the path 419 | * @param string $path File path to add leading directory separator to 420 | */ 421 | public static function add_leading_directory_separator( $path ) { 422 | return DIRECTORY_SEPARATOR . self::remove_leading_directory_separator( $path ); 423 | } 424 | 425 | /** 426 | * Remove all leading directory separators 427 | * @param string $path File path to remove leading directory separators from 428 | */ 429 | public static function remove_leading_directory_separator( $path ) { 430 | return ltrim( $path, DIRECTORY_SEPARATOR ); 431 | } 432 | 433 | /** 434 | * Add a slash to the beginning of a path 435 | * @param string $path URL path to add leading slash to 436 | */ 437 | public static function add_leading_slash( $path ) { 438 | return '/' . self::remove_leading_slash( $path ); 439 | } 440 | 441 | /** 442 | * Remove a slash from the beginning of a path 443 | * @param string $path URL path to remove leading slash from 444 | */ 445 | public static function remove_leading_slash( $path ) { 446 | return ltrim( $path, '/' ); 447 | } 448 | 449 | /** 450 | * Add a message to the array of status messages for the job 451 | * @param array $messages Array of messages to add the message to 452 | * @param string $task_name Name of the task 453 | * @param string $message Message to display about the status of the job 454 | * @return void 455 | */ 456 | public static function add_archive_status_message( $messages, $task_name, $message ) { 457 | // if the state exists, set the datetime and message 458 | if ( ! array_key_exists( $task_name, $messages ) ) { 459 | $messages[ $task_name ] = array( 460 | 'message' => $message, 461 | 'datetime' => self::formatted_datetime() 462 | ); 463 | } else { // otherwise just update the message 464 | $messages[ $task_name ]['message'] = $message; 465 | } 466 | 467 | return $messages; 468 | } 469 | 470 | } 471 | --------------------------------------------------------------------------------