slug ); ?>
28 | 29 | 30 |├── .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 |
redirect_url . '">' . $this->redirect_url . '' ); ?>
13 | 14 | 15 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 |%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 |8 | 9 |
10 || 16 | | 17 | | 18 | 0 ) : ?> 19 | | 20 | 21 | |
|---|---|---|---|
| '> 29 | http_status_code; ?> 30 | | 31 |url; ?> | 32 | 47 | 0 ) : ?> 48 | 51 | 52 |
| 14 | | ||
|---|---|---|
| 20 | 21 | | 22 | 23 | | 24 | 25 | |
| 35 | | 36 | | 37 | | 38 | | |
|---|---|---|---|---|
| get( 'Name'); ?> | 44 |'>get( 'ThemeURI'); ?> | 45 |get( 'Version'); ?> | 46 | get( 'Name') === $this->current_theme_name ) : ?> 47 |48 | 49 | | 50 | 51 | |
| 60 | | 61 | | 62 | | 63 | | |
|---|---|---|---|---|
| 69 | | '> | 70 |71 | 72 | | 73 | 74 | | 75 | 76 | |
%s%s%s"
15 | msgstr "Eine zusätzliche Datei oder Verzeichnis ist nicht in dem erwarteten Verzeichnis: %s%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 %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%s%s%s"
165 | msgstr ""
166 | "Un fichier ou un répertoire additionnel ne peut être localisé dans le "
167 | "répertoire attendu : %s%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 |
--------------------------------------------------------------------------------