mirror of
https://github.com/WenPai-org/wp-archiver.git
synced 2025-08-17 19:31:09 +08:00
1.3 版本提交
稳定版发布
This commit is contained in:
parent
5be6536d93
commit
4aad56bea3
8 changed files with 1688 additions and 0 deletions
74
archiver.php
Normal file
74
archiver.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: WP Archiver
|
||||
* Plugin URI: http://wenpai.org/plugins/wp-archiver
|
||||
* Description: Archive your content using the Wayback Machine.
|
||||
* Version: 1.3.0
|
||||
* Author: WenPai.org
|
||||
* Author URI: https://wenpai.org/
|
||||
* License: GPL-2.0+
|
||||
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
||||
* Text Domain: archiver
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
|
||||
if (!defined('WPINC')) {
|
||||
die;
|
||||
}
|
||||
|
||||
define('ARCHIVER_VERSION', '1.3.0');
|
||||
define('ARCHIVER_PLUGIN_DIR_URL', plugin_dir_url(__FILE__));
|
||||
define('ARCHIVER_PLUGIN_DIR_PATH', plugin_dir_path(__FILE__));
|
||||
define('ARCHIVER_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
require_once ARCHIVER_PLUGIN_DIR_PATH . 'includes/class-archiver.php';
|
||||
require_once ARCHIVER_PLUGIN_DIR_PATH . 'includes/class-archiver-admin.php';
|
||||
|
||||
function archiver_activate() {
|
||||
if (!get_option('archiver_post_types')) {
|
||||
update_option('archiver_post_types', array('post', 'page'));
|
||||
}
|
||||
if (!get_option('archiver_update_frequency')) {
|
||||
update_option('archiver_update_frequency', 'daily');
|
||||
}
|
||||
if (!get_option('archiver_urls_to_update')) {
|
||||
update_option('archiver_urls_to_update', array());
|
||||
}
|
||||
if (!wp_next_scheduled('archiver_process_urls')) {
|
||||
$frequency = get_option('archiver_update_frequency', 'daily');
|
||||
wp_schedule_event(time(), $frequency, 'archiver_process_urls');
|
||||
}
|
||||
}
|
||||
register_activation_hook(__FILE__, 'archiver_activate');
|
||||
|
||||
function archiver_uninstall() {
|
||||
$timestamp = wp_next_scheduled('archiver_process_urls');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'archiver_process_urls');
|
||||
}
|
||||
delete_option('archiver_post_types');
|
||||
delete_option('archiver_update_frequency');
|
||||
delete_option('archiver_urls_to_update');
|
||||
delete_option('archiver_last_run');
|
||||
}
|
||||
register_uninstall_hook(__FILE__, 'archiver_uninstall');
|
||||
|
||||
function archiver_load_textdomain() {
|
||||
load_plugin_textdomain(
|
||||
'archiver',
|
||||
false,
|
||||
dirname(plugin_basename(__FILE__)) . '/languages/'
|
||||
);
|
||||
}
|
||||
add_action('init', 'archiver_load_textdomain');
|
||||
|
||||
function archiver_run() {
|
||||
$archiver = Archiver::get_instance();
|
||||
if (is_admin()) {
|
||||
new Archiver_Admin($archiver);
|
||||
}
|
||||
$archiver->run();
|
||||
return $archiver;
|
||||
}
|
||||
add_action('plugins_loaded', 'archiver_run');
|
161
css/archiver.css
Normal file
161
css/archiver.css
Normal file
|
@ -0,0 +1,161 @@
|
|||
#wp-admin-bar-archiver .ab-item {
|
||||
transition: all .3s ease;
|
||||
color: #72aee6
|
||||
}
|
||||
|
||||
#wp-admin-bar-archiver.archiver-success .ab-item {
|
||||
color: #46b450!important
|
||||
}
|
||||
|
||||
#wp-admin-bar-archiver.archiver-failure .ab-item {
|
||||
color: #dc3232!important
|
||||
}
|
||||
|
||||
#wp-admin-bar-archiver-trigger .ab-icon {
|
||||
display: inline-block;
|
||||
float: right!important;
|
||||
margin: 0 0 0 6px;
|
||||
opacity: 0;
|
||||
transition: opacity .3s ease;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
#wp-admin-bar-archiver-trigger .ab-icon::before {
|
||||
content: "\f463";
|
||||
font-family: dashicons;
|
||||
font-size: 18px;
|
||||
speak: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
}
|
||||
|
||||
#wp-admin-bar-archiver-trigger.archiver-active .ab-icon {
|
||||
opacity: 1;
|
||||
animation: archiver-spin 1s infinite linear
|
||||
}
|
||||
|
||||
@keyframes archiver-spin {
|
||||
0% {
|
||||
transform: rotate(0)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
.wrap.archiver-settings {
|
||||
max-width: 1200px
|
||||
}
|
||||
|
||||
.postbox {
|
||||
border: 0 solid #c3c4c7;
|
||||
background: #ffffff00
|
||||
}
|
||||
|
||||
.archiver-stats-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin: 0 0 20px
|
||||
}
|
||||
|
||||
.archiver-stat-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04)
|
||||
}
|
||||
|
||||
.archiver-stat-card h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #646970
|
||||
}
|
||||
|
||||
.archiver-stat-card .stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d2327
|
||||
}
|
||||
|
||||
.archiver-pending-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
margin: 0 0 20px
|
||||
}
|
||||
|
||||
.archiver-pending-list ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none
|
||||
}
|
||||
|
||||
.archiver-pending-list li {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
word-break: break-all
|
||||
}
|
||||
|
||||
.archiver-pending-list li:last-child {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
@media (max-width:782px) {
|
||||
.archiver-stats-container {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.archiver-stat-card {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
#archiver_post,#archiver_terms {
|
||||
background: #fff;
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
#archiver-snapshots ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none
|
||||
}
|
||||
|
||||
#archiver-snapshots li {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee
|
||||
}
|
||||
|
||||
#archiver-snapshots li:last-child {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
#archiver-snapshots a {
|
||||
text-decoration: none;
|
||||
color: #2271b1
|
||||
}
|
||||
|
||||
#archiver-snapshots a:hover {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
#archiver-status {
|
||||
vertical-align: middle;
|
||||
color: #646970
|
||||
}
|
||||
|
||||
#archiver-status .dashicons {
|
||||
vertical-align: middle;
|
||||
margin-right: 3px
|
||||
}
|
||||
|
||||
#archiver-immediate-snapshot {
|
||||
margin-top: 10px
|
||||
}
|
1
css/archiver.min.css
vendored
Normal file
1
css/archiver.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
#wp-admin-bar-archiver .ab-item{transition:all .3s ease;color:#72aee6}#wp-admin-bar-archiver.archiver-success .ab-item{color:#46b450!important}#wp-admin-bar-archiver.archiver-failure .ab-item{color:#dc3232!important}#wp-admin-bar-archiver-trigger .ab-icon{display:inline-block;float:right!important;margin:0 0 0 6px;opacity:0;transition:opacity .3s ease;vertical-align:middle}#wp-admin-bar-archiver-trigger .ab-icon::before{content:"\f463";font-family:dashicons;font-size:18px;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#wp-admin-bar-archiver-trigger.archiver-active .ab-icon{opacity:1;animation:archiver-spin 1s infinite linear}@keyframes archiver-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.wrap.archiver-settings{max-width:1200px}.postbox{border:0 solid #c3c4c7;background:#ffffff00}.archiver-stats-container{display:flex;gap:20px;margin:0 0 20px}.archiver-stat-card{flex:1;background:#fff;border:1px solid #ccd0d4;border-radius:4px;padding:15px;text-align:center;box-shadow:0 1px 1px rgba(0,0,0,.04)}.archiver-stat-card h3{margin:0 0 10px;font-size:14px;font-weight:500;color:#646970}.archiver-stat-card .stat-value{font-size:18px;font-weight:600;color:#1d2327}.archiver-pending-list{max-height:300px;overflow-y:auto;border:1px solid #ddd;padding:10px;background:#f9f9f9;margin:0 0 20px}.archiver-pending-list ul{margin:0;padding:0;list-style:none}.archiver-pending-list li{padding:5px 0;border-bottom:1px solid #eee;word-break:break-all}.archiver-pending-list li:last-child{border-bottom:none}@media (max-width:782px){.archiver-stats-container{flex-direction:column}.archiver-stat-card{width:100%}}#archiver_post,#archiver_terms{background:#fff;border-radius:4px}#archiver-snapshots ul{margin:0;padding:0;list-style:none}#archiver-snapshots li{padding:5px 0;border-bottom:1px solid #eee}#archiver-snapshots li:last-child{border-bottom:none}#archiver-snapshots a{text-decoration:none;color:#2271b1}#archiver-snapshots a:hover{text-decoration:underline}#archiver-status{vertical-align:middle;color:#646970}#archiver-status .dashicons{vertical-align:middle;margin-right:3px}#archiver-immediate-snapshot{margin-top:10px}
|
312
includes/class-archiver-admin.php
Normal file
312
includes/class-archiver-admin.php
Normal file
|
@ -0,0 +1,312 @@
|
|||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Archiver_Admin {
|
||||
protected $archiver;
|
||||
protected $slug;
|
||||
protected $min_suffix = '';
|
||||
|
||||
public function __construct($archiver) {
|
||||
$this->archiver = $archiver;
|
||||
$this->slug = $archiver->get_slug();
|
||||
$this->min_suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min';
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
add_action('admin_init', array($this, 'handle_settings_actions'));
|
||||
}
|
||||
|
||||
public function get_slug() {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function add_admin_menu() {
|
||||
add_management_page(
|
||||
__('Archiver Settings', 'archiver'),
|
||||
__('Archiver', 'archiver'),
|
||||
'manage_options',
|
||||
'archiver-settings',
|
||||
array($this, 'render_admin_page')
|
||||
);
|
||||
}
|
||||
|
||||
public function render_admin_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('You do not have sufficient permissions to access this page.'));
|
||||
}
|
||||
$post_types = get_post_types(array('public' => true), 'objects');
|
||||
$selected_post_types = get_option('archiver_post_types', array('post', 'page'));
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<h2 class="nav-tab-wrapper">
|
||||
<a href="#general" class="nav-tab nav-tab-active" data-tab="general"><?php _e('General Settings', 'archiver'); ?></a>
|
||||
<a href="#tools" class="nav-tab" data-tab="tools"><?php _e('Tools', 'archiver'); ?></a>
|
||||
</h2>
|
||||
<?php settings_errors('archiver_messages'); ?>
|
||||
|
||||
<div id="archiver-tabs-content">
|
||||
<div id="general" class="archiver-tab-content" style="display: block;">
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('archiver_settings_nonce'); ?>
|
||||
<div class="postbox">
|
||||
<div class="inside">
|
||||
<h3><?php _e('Content Types to Archive', 'archiver'); ?></h3>
|
||||
<p class="description"><?php _e('Select which content types should be automatically archived when published or updated.', 'archiver'); ?></p>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Post Types', 'archiver'); ?></th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<?php foreach ($post_types as $post_type) : ?>
|
||||
<label>
|
||||
<input type="checkbox" name="archiver_post_types[]"
|
||||
value="<?php echo esc_attr($post_type->name); ?>"
|
||||
<?php checked(in_array($post_type->name, $selected_post_types)); ?>>
|
||||
<?php echo esc_html($post_type->label); ?>
|
||||
</label><br>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3><?php _e('Update Frequency', 'archiver'); ?></h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php _e('Frequency', 'archiver'); ?></th>
|
||||
<td>
|
||||
<select name="archiver_update_frequency" class="regular-text">
|
||||
<option value="hourly" <?php selected(get_option('archiver_update_frequency', 'daily'), 'hourly'); ?>>
|
||||
<?php _e('Hourly', 'archiver'); ?>
|
||||
</option>
|
||||
<option value="twicedaily" <?php selected(get_option('archiver_update_frequency', 'daily'), 'twicedaily'); ?>>
|
||||
<?php _e('Twice Daily', 'archiver'); ?>
|
||||
</option>
|
||||
<option value="daily" <?php selected(get_option('archiver_update_frequency', 'daily'), 'daily'); ?>>
|
||||
<?php _e('Daily', 'archiver'); ?>
|
||||
</option>
|
||||
<option value="weekly" <?php selected(get_option('archiver_update_frequency', 'daily'), 'weekly'); ?>>
|
||||
<?php _e('Weekly', 'archiver'); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description"><?php _e('How often should the plugin check for new archives?', 'archiver'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button(__('Save Settings', 'archiver'), 'primary', 'submit'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="tools" class="archiver-tab-content" style="display: none;">
|
||||
<div class="postbox">
|
||||
<div class="inside">
|
||||
<h3><?php _e('Archive Statistics', 'archiver'); ?></h3>
|
||||
<div class="archiver-stats-container">
|
||||
<div class="archiver-stat-card">
|
||||
<h3><?php _e('Pending URLs', 'archiver'); ?></h3>
|
||||
<div class="stat-value"><?php echo number_format(count(get_option('archiver_urls_to_update', array()))); ?></div>
|
||||
</div>
|
||||
<div class="archiver-stat-card">
|
||||
<h3><?php _e('Total Archived', 'archiver'); ?></h3>
|
||||
<div class="stat-value"><?php echo number_format((int) get_option('archiver_total_archived', 0)); ?></div>
|
||||
</div>
|
||||
<div class="archiver-stat-card">
|
||||
<h3><?php _e('Failed Snapshots', 'archiver'); ?></h3>
|
||||
<div class="stat-value"><?php echo number_format((int) get_option('archiver_failed_snapshots', 0)); ?></div>
|
||||
</div>
|
||||
<div class="archiver-stat-card">
|
||||
<h3><?php _e('Last Run', 'archiver'); ?></h3>
|
||||
<div class="stat-value"><?php echo esc_html(get_option('archiver_last_run', __('Never', 'archiver'))); ?></div>
|
||||
</div>
|
||||
<div class="archiver-stat-card">
|
||||
<h3><?php _e('Next Run', 'archiver'); ?></h3>
|
||||
<div class="stat-value"><?php
|
||||
$next_run = wp_next_scheduled('archiver_process_urls');
|
||||
echo $next_run ? esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)) : __('Not scheduled', 'archiver');
|
||||
?></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3><?php _e('Pending Updates', 'archiver'); ?></h3>
|
||||
<?php $this->display_pending_updates(); ?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('archiver_manual_update_nonce'); ?>
|
||||
<p>
|
||||
<input type="submit" name="archiver_trigger_update"
|
||||
class="button button-primary"
|
||||
value="<?php esc_attr_e('Run Manual Update Now', 'archiver'); ?>">
|
||||
<span class="description"><?php _e('Process all pending URLs immediately.', 'archiver'); ?></span>
|
||||
</p>
|
||||
</form>
|
||||
<h3><?php _e('Bulk Actions', 'archiver'); ?></h3>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('archiver_bulk_actions_nonce'); ?>
|
||||
<p>
|
||||
<select name="archiver_bulk_action" class="regular-text">
|
||||
<option value=""><?php _e('Select bulk action...', 'archiver'); ?></option>
|
||||
<option value="archive_all_posts"><?php _e('Archive all published posts', 'archiver'); ?></option>
|
||||
<option value="clear_queue"><?php _e('Clear pending queue', 'archiver'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="archiver_submit_bulk_action"
|
||||
class="button button-secondary"
|
||||
value="<?php esc_attr_e('Apply', 'archiver'); ?>">
|
||||
</p>
|
||||
</form>
|
||||
<h3><?php _e('Debug Tools', 'archiver'); ?></h3>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('archiver_debug_tools_nonce'); ?>
|
||||
<p>
|
||||
<input type="submit" name="archiver_clear_cache"
|
||||
class="button button-secondary"
|
||||
value="<?php esc_attr_e('Clear Cache', 'archiver'); ?>">
|
||||
<span class="description"><?php _e('Clear all cached snapshot data.', 'archiver'); ?></span>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_settings_actions() {
|
||||
if (isset($_POST['submit']) && check_admin_referer('archiver_settings_nonce')) {
|
||||
$this->save_settings();
|
||||
}
|
||||
if (isset($_POST['archiver_trigger_update']) && check_admin_referer('archiver_manual_update_nonce')) {
|
||||
$this->archiver->process_urls_for_update();
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Manual update triggered. The update will run in the background.', 'archiver'),
|
||||
'updated'
|
||||
);
|
||||
}
|
||||
if (isset($_POST['archiver_submit_bulk_action']) && check_admin_referer('archiver_bulk_actions_nonce')) {
|
||||
$this->handle_bulk_actions();
|
||||
}
|
||||
if (isset($_POST['archiver_clear_cache']) && check_admin_referer('archiver_debug_tools_nonce')) {
|
||||
$this->clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
private function save_settings() {
|
||||
if (!isset($_POST['archiver_post_types']) || !is_array($_POST['archiver_post_types'])) {
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Please select at least one post type to archive.', 'archiver'),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
$valid_post_types = array_keys(get_post_types(['public' => true]));
|
||||
$selected_types = array_intersect($_POST['archiver_post_types'], $valid_post_types);
|
||||
if (empty($selected_types)) {
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Invalid post types selected.', 'archiver'),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
update_option('archiver_post_types', $selected_types);
|
||||
$valid_frequencies = ['hourly', 'twicedaily', 'daily', 'weekly'];
|
||||
$frequency = isset($_POST['archiver_update_frequency']) && in_array($_POST['archiver_update_frequency'], $valid_frequencies)
|
||||
? $_POST['archiver_update_frequency']
|
||||
: 'daily';
|
||||
update_option('archiver_update_frequency', $frequency);
|
||||
$this->archiver->reschedule_cron_task($frequency);
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Settings saved successfully.', 'archiver'),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
|
||||
private function handle_bulk_actions() {
|
||||
$action = isset($_POST['archiver_bulk_action']) ? sanitize_text_field($_POST['archiver_bulk_action']) : '';
|
||||
switch ($action) {
|
||||
case 'archive_all_posts':
|
||||
$this->archive_all_published_posts();
|
||||
break;
|
||||
case 'clear_queue':
|
||||
update_option('archiver_urls_to_update', array());
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Pending queue has been cleared.', 'archiver'),
|
||||
'updated'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('Please select a valid bulk action.', 'archiver'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function archive_all_published_posts() {
|
||||
$post_types = get_option('archiver_post_types', array('post', 'page'));
|
||||
$posts = get_posts(array(
|
||||
'post_type' => $post_types,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids'
|
||||
));
|
||||
$urls = array();
|
||||
foreach ($posts as $post_id) {
|
||||
$urls[] = get_permalink($post_id);
|
||||
}
|
||||
update_option('archiver_urls_to_update', $urls);
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
sprintf(__('%d posts have been added to the archive queue.', 'archiver'), count($posts)),
|
||||
'updated'
|
||||
);
|
||||
}
|
||||
|
||||
private function clear_cache() {
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_archiver_last_known_snapshots_%'");
|
||||
$wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_timeout_archiver_last_known_snapshots_%'");
|
||||
update_option('archiver_total_archived', 0);
|
||||
update_option('archiver_failed_snapshots', 0);
|
||||
wp_cache_flush();
|
||||
add_settings_error(
|
||||
'archiver_messages',
|
||||
'archiver_message',
|
||||
__('All cached data has been cleared.', 'archiver'),
|
||||
'updated'
|
||||
);
|
||||
}
|
||||
|
||||
private function display_pending_updates() {
|
||||
$urls_to_update = get_option('archiver_urls_to_update', array());
|
||||
if (empty($urls_to_update)) {
|
||||
echo '<p>' . __('No pending updates.', 'archiver') . '</p>';
|
||||
return;
|
||||
}
|
||||
echo '<p>' . sprintf(__('Total pending URLs: %d', 'archiver'), count($urls_to_update)) . '</p>';
|
||||
echo '<div style="max-height: 300px; overflow-y: scroll; border: 1px solid #ddd; padding: 10px; background: #f9f9f9;">';
|
||||
echo '<ul>';
|
||||
foreach (array_slice($urls_to_update, 0, 50) as $url) {
|
||||
echo '<li style="word-break: break-all;">' . esc_html($url) . '</li>';
|
||||
}
|
||||
if (count($urls_to_update) > 50) {
|
||||
echo '<li>... ' . (count($urls_to_update) - 50) . ' ' . __('more', 'archiver') . '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
680
includes/class-archiver.php
Normal file
680
includes/class-archiver.php
Normal file
|
@ -0,0 +1,680 @@
|
|||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Archiver {
|
||||
protected $slug = 'archiver';
|
||||
protected $name;
|
||||
protected $min_suffix = '';
|
||||
protected $snapshot_max_count;
|
||||
protected $wayback_machine_url_save = 'https://web.wenpai.net/save/';
|
||||
protected $wayback_machine_url_fetch_archives = 'https://web.wenpai.net/cdx/';
|
||||
protected $wayback_machine_url_view = 'https://web.archive.org/web/';
|
||||
protected $current_permalink = '';
|
||||
protected static $instance = null;
|
||||
|
||||
public static function get_instance($args = array()) {
|
||||
if (null == self::$instance) {
|
||||
self::$instance = new self($args);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct($args) {
|
||||
$this->name = __('Archiver', 'archiver');
|
||||
$this->snapshot_max_count = apply_filters('archiver_snapshot_max_count', 10);
|
||||
$this->min_suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min';
|
||||
add_action('init', array($this, 'setup_cron'));
|
||||
add_action('rest_api_init', array($this, 'register_rest_routes'));
|
||||
}
|
||||
|
||||
public function get_slug() {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function run() {
|
||||
add_action('wp_loaded', array($this, 'init'));
|
||||
add_action('admin_init', array($this, 'admin_init'));
|
||||
}
|
||||
|
||||
public function init() {
|
||||
$this->set_locale();
|
||||
add_action('wp_enqueue_scripts', array($this, 'register_scripts_and_styles'), 5);
|
||||
add_action('admin_enqueue_scripts', array($this, 'register_scripts_and_styles'), 5);
|
||||
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
|
||||
add_action('wp_ajax_archiver_immediate_snapshot', array($this, 'ajax_immediate_snapshot'));
|
||||
if ($this->can_run()) {
|
||||
add_action('save_post', array($this, 'trigger_post_snapshot'));
|
||||
add_action('created_term', array($this, 'trigger_term_snapshot'), 10, 3);
|
||||
add_action('edited_term', array($this, 'trigger_term_snapshot'), 10, 3);
|
||||
add_action('profile_update', array($this, 'trigger_user_snapshot'), 10, 3);
|
||||
add_action('admin_bar_menu', array($this, 'add_admin_bar_links'), 999);
|
||||
} else {
|
||||
add_action('admin_notices', array($this, 'do_admin_notice_disabled'));
|
||||
}
|
||||
}
|
||||
|
||||
public function admin_init() {
|
||||
$this->add_post_meta_box();
|
||||
$this->add_term_meta_box();
|
||||
$this->add_user_meta_box();
|
||||
}
|
||||
|
||||
public function setup_cron() {
|
||||
$frequency = get_option('archiver_update_frequency', 'daily');
|
||||
if (!wp_next_scheduled('archiver_process_urls')) {
|
||||
wp_schedule_event(time(), $frequency, 'archiver_process_urls');
|
||||
}
|
||||
add_action('archiver_process_urls', array($this, 'process_urls_for_update'));
|
||||
}
|
||||
|
||||
public function can_run() {
|
||||
return apply_filters('archiver_can_run', __return_true());
|
||||
}
|
||||
|
||||
protected function set_locale() {
|
||||
load_plugin_textdomain(
|
||||
$this->slug,
|
||||
false,
|
||||
dirname(dirname(plugin_basename(__FILE__))) . '/languages/'
|
||||
);
|
||||
}
|
||||
|
||||
public function add_post_meta_box() {
|
||||
$post_types = apply_filters('archiver_post_types', get_post_types());
|
||||
add_meta_box(
|
||||
'archiver_post',
|
||||
__('Archives', 'archiver'),
|
||||
array($this, 'output_archiver_metabox'),
|
||||
$post_types,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public function add_term_meta_box() {
|
||||
$taxonomies = apply_filters('archiver_taxonomies', get_taxonomies());
|
||||
$archiver_taxonomy_slugs = array_map(
|
||||
function($taxonomy) { return "archiver-" . $taxonomy; },
|
||||
$taxonomies
|
||||
);
|
||||
add_meta_box(
|
||||
'archiver_terms',
|
||||
__('Archives', 'archiver'),
|
||||
array($this, 'output_archiver_metabox'),
|
||||
$archiver_taxonomy_slugs,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
foreach ($taxonomies as $taxonomy) {
|
||||
add_action("{$taxonomy}_edit_form", array($this, 'output_term_meta_box'));
|
||||
}
|
||||
}
|
||||
|
||||
public function output_term_meta_box() {
|
||||
$object_type = get_current_screen()->taxonomy;
|
||||
$this->output_manual_meta_box($object_type);
|
||||
}
|
||||
|
||||
public function add_user_meta_box() {
|
||||
add_meta_box(
|
||||
'archiver_terms',
|
||||
__('Archives', 'archiver'),
|
||||
array($this, 'output_archiver_metabox'),
|
||||
array('archiver-user'),
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public function output_archiver_metabox() {
|
||||
$url = $this->get_current_permalink();
|
||||
$snapshots = $this->get_post_snapshots($url);
|
||||
wp_nonce_field('archiver_immediate_snapshot', '_ajax_nonce');
|
||||
echo '<input type="hidden" id="archiver-url" value="' . esc_attr($url) . '">';
|
||||
echo '<input type="hidden" id="archiver_nonce" value="' . wp_create_nonce('archiver_immediate_snapshot') . '">';
|
||||
echo '<div id="archiver-snapshots">';
|
||||
if (empty($snapshots)) {
|
||||
$urls_to_update = get_option('archiver_urls_to_update', array());
|
||||
if (in_array($url, $urls_to_update)) {
|
||||
esc_html_e('No archives yet. A snapshot request has been scheduled and will be processed soon.', 'archiver');
|
||||
} else {
|
||||
esc_html_e('There are no archives of this URL.', 'archiver');
|
||||
}
|
||||
} else {
|
||||
$snapshots = array_slice($snapshots, 0, $this->snapshot_max_count);
|
||||
$date_format = get_option('date_format');
|
||||
$time_format = get_option('time_format');
|
||||
echo '<ul>';
|
||||
foreach ($snapshots as $snapshot) {
|
||||
$date_time = date('Y-m-d H:i:s', strtotime($snapshot['timestamp']));
|
||||
$adjusted_date = get_date_from_gmt($date_time);
|
||||
$snapshot_url = $this->wayback_machine_url_view . $snapshot['timestamp'] . '/' . $snapshot['original'];
|
||||
$date_time = date_i18n($date_format . ' @ ' . $time_format, strtotime($adjusted_date));
|
||||
echo '<li><a href="' . esc_url($snapshot_url) . '" target="_blank">' . esc_html($date_time) . '</a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '<hr />';
|
||||
printf('<a href="%s" target="_external">%s</a>',
|
||||
esc_url($this->wayback_machine_url_view . '*/' . $url),
|
||||
esc_html__('See all snapshots ↗', 'archiver')
|
||||
);
|
||||
echo '<div style="margin-top: 10px;">';
|
||||
echo '<button id="archiver-immediate-snapshot" class="button button-secondary">' . esc_html__('Archive Now', 'archiver') . '</button>';
|
||||
echo '<span id="archiver-status" style="margin-left: 10px; display: none;"></span>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function ajax_immediate_snapshot() {
|
||||
if (!check_ajax_referer('archiver_immediate_snapshot', '_ajax_nonce', false)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Security check failed. Please try again.', 'archiver')
|
||||
], 403);
|
||||
}
|
||||
if (!current_user_can('edit_posts')) {
|
||||
wp_send_json_error([
|
||||
'message' => __('You do not have permission to perform this action.', 'archiver')
|
||||
], 403);
|
||||
}
|
||||
$url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
|
||||
if (empty($url)) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Invalid URL provided.', 'archiver')
|
||||
], 400);
|
||||
}
|
||||
$result = $this->trigger_wayback_machine_snapshot($url);
|
||||
$snapshots = $this->fetch_snapshots_from_wayback($url);
|
||||
if (!empty($snapshots)) {
|
||||
$cache_key = 'archiver_snapshots_' . md5($url);
|
||||
set_transient($cache_key, $snapshots, WEEK_IN_SECONDS);
|
||||
$date_format = get_option('date_format');
|
||||
$time_format = get_option('time_format');
|
||||
$snapshot_list = [];
|
||||
foreach ($snapshots as $snapshot) {
|
||||
$date_time = date_i18n("$date_format @ $time_format", strtotime($snapshot['timestamp']));
|
||||
$snapshot_url = $this->wayback_machine_url_view . $snapshot['timestamp'] . '/' . $snapshot['original'];
|
||||
$snapshot_list[] = '<li><a href="'.esc_url($snapshot_url).'" target="_blank">'.esc_html($date_time).'</a></li>';
|
||||
}
|
||||
wp_send_json_success([
|
||||
'message' => __('Snapshot created successfully!', 'archiver'),
|
||||
'snapshots' => $snapshot_list
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => __('Failed to retrieve snapshots.', 'archiver')
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function add_admin_bar_links($wp_admin_bar) {
|
||||
if (!current_user_can('edit_posts')) {
|
||||
return;
|
||||
}
|
||||
$url = $this->get_current_permalink();
|
||||
if (!$url) {
|
||||
return;
|
||||
}
|
||||
$wp_admin_bar->add_node([
|
||||
'id' => 'archiver',
|
||||
'title' => '<span class="ab-icon dashicons dashicons-archive"></span><span class="ab-label">' . __('Archiver', 'archiver') . '</span>',
|
||||
'href' => $this->wayback_machine_url_view . '*/' . $url,
|
||||
'meta' => ['target' => '_blank']
|
||||
]);
|
||||
$snapshots = $this->get_post_snapshots();
|
||||
$snapshot_count = is_wp_error($snapshots) ? 0 : count($snapshots);
|
||||
if ($snapshot_count >= $this->snapshot_max_count) {
|
||||
$snapshot_count = $this->snapshot_max_count . '+';
|
||||
}
|
||||
$wp_admin_bar->add_node([
|
||||
'parent' => 'archiver',
|
||||
'id' => 'archiver-snapshots',
|
||||
'title' => __('Snapshots', 'archiver') . " ({$snapshot_count})",
|
||||
'href' => $this->wayback_machine_url_view . '*/' . $url,
|
||||
'meta' => ['target' => '_blank']
|
||||
]);
|
||||
$wp_admin_bar->add_node([
|
||||
'parent' => 'archiver',
|
||||
'id' => 'archiver-trigger',
|
||||
'title' => __('Trigger Snapshot', 'archiver') . ' <span class="ab-icon"></span>',
|
||||
'href' => '#',
|
||||
'meta' => [
|
||||
'class' => 'archiver-trigger'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_post_snapshots($url = '') {
|
||||
$url = $url ? $url : $this->get_current_permalink();
|
||||
if (empty($url)) {
|
||||
return array();
|
||||
}
|
||||
$cache_key = 'archiver_snapshots_' . md5($url);
|
||||
$snapshots = wp_cache_get($cache_key);
|
||||
if (false === $snapshots) {
|
||||
$snapshots = get_transient('archiver_last_known_snapshots_' . md5($url));
|
||||
if (false === $snapshots) {
|
||||
$this->record_url_for_update($url);
|
||||
$snapshots = array();
|
||||
} else {
|
||||
wp_cache_set($cache_key, $snapshots, '', HOUR_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
private function fetch_snapshots_from_wayback($url) {
|
||||
$fetch_url = add_query_arg([
|
||||
'url' => $url,
|
||||
'output' => 'json',
|
||||
], $this->wayback_machine_url_fetch_archives);
|
||||
$response = wp_remote_get($fetch_url, array(
|
||||
'timeout' => 30,
|
||||
'sslverify' => false
|
||||
));
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Archiver: Failed to fetch snapshots for ' . $url . '. Error: ' . $response->get_error_message());
|
||||
return array();
|
||||
}
|
||||
$response_code = wp_remote_retrieve_response_code($response);
|
||||
if (200 != $response_code) {
|
||||
error_log('Archiver: Failed to fetch snapshots for ' . $url . '. Status code: ' . $response_code);
|
||||
return array();
|
||||
}
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
if (empty($data)) {
|
||||
error_log('Archiver: Empty response data for ' . $url);
|
||||
return array();
|
||||
}
|
||||
return $this->process_snapshot_data($data);
|
||||
}
|
||||
|
||||
private function record_url_for_update($url, $priority = false) {
|
||||
$urls_to_update = get_option('archiver_urls_to_update', array());
|
||||
if (!in_array($url, $urls_to_update)) {
|
||||
if ($priority) {
|
||||
array_unshift($urls_to_update, $url);
|
||||
} else {
|
||||
$urls_to_update[] = $url;
|
||||
}
|
||||
update_option('archiver_urls_to_update', $urls_to_update);
|
||||
}
|
||||
}
|
||||
|
||||
private function fetch_and_cache_snapshots($url) {
|
||||
$fetch_url = add_query_arg([
|
||||
'url' => $url,
|
||||
'output' => 'json',
|
||||
], $this->wayback_machine_url_fetch_archives);
|
||||
$response = wp_remote_get($fetch_url, array(
|
||||
'timeout' => 30,
|
||||
'sslverify' => false
|
||||
));
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Archiver: Failed to fetch snapshots for ' . $url . '. Error: ' . $response->get_error_message());
|
||||
return false;
|
||||
}
|
||||
$response_code = wp_remote_retrieve_response_code($response);
|
||||
if (200 != $response_code) {
|
||||
error_log('Archiver: Failed to fetch snapshots for ' . $url . '. Status code: ' . $response_code);
|
||||
return false;
|
||||
}
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
if (empty($data)) {
|
||||
error_log('Archiver: Empty response data for ' . $url);
|
||||
return false;
|
||||
}
|
||||
$snapshots = $this->process_snapshot_data($data);
|
||||
set_transient('archiver_last_known_snapshots_' . md5($url), $snapshots, WEEK_IN_SECONDS);
|
||||
wp_cache_set('archiver_snapshots_' . md5($url), $snapshots, '', HOUR_IN_SECONDS);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function trigger_wayback_machine_snapshot($url) {
|
||||
$save_url = $this->wayback_machine_url_save . $url;
|
||||
$response = wp_remote_get($save_url, array(
|
||||
'timeout' => 10,
|
||||
'sslverify' => false
|
||||
));
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Archiver: Failed to trigger Wayback Machine snapshot for ' . $url . '. Error: ' . $response->get_error_message());
|
||||
return false;
|
||||
}
|
||||
$response_code = wp_remote_retrieve_response_code($response);
|
||||
if (200 == $response_code) {
|
||||
error_log('Archiver: Successfully triggered Wayback Machine snapshot for ' . $url);
|
||||
return true;
|
||||
}
|
||||
error_log('Archiver: Failed to trigger Wayback Machine snapshot for ' . $url . '. Status: ' . $response_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function process_snapshot_data($data) {
|
||||
if (empty($data)) {
|
||||
return array();
|
||||
}
|
||||
$field_columns = $data[0];
|
||||
unset($data[0]);
|
||||
$data = array_reverse($data);
|
||||
$data = array_slice($data, 0, $this->snapshot_max_count);
|
||||
$snapshots = array();
|
||||
foreach ($data as $snapshot) {
|
||||
$keyed_snapshot = array();
|
||||
foreach ($snapshot as $i => $field) {
|
||||
$keyed_snapshot[$field_columns[$i]] = $field;
|
||||
}
|
||||
$snapshots[] = $keyed_snapshot;
|
||||
}
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
public function get_current_permalink() {
|
||||
if (empty($this->current_permalink)) {
|
||||
if (is_admin()) {
|
||||
$this->current_permalink = $this->get_current_permalink_admin();
|
||||
} else {
|
||||
$this->current_permalink = $this->get_current_permalink_public();
|
||||
}
|
||||
}
|
||||
return apply_filters('archiver_permalink', $this->current_permalink);
|
||||
}
|
||||
|
||||
public function get_current_permalink_admin() {
|
||||
$permalink = '';
|
||||
$current_screen = get_current_screen();
|
||||
$object_type = $current_screen->base;
|
||||
switch ($object_type) {
|
||||
case 'post':
|
||||
global $post;
|
||||
if ($post && $post->ID) {
|
||||
$permalink = get_permalink($post->ID);
|
||||
} else {
|
||||
$post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
|
||||
if ($post_id) {
|
||||
$permalink = get_permalink($post_id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'term':
|
||||
global $taxnow, $tag;
|
||||
$taxonomy = $taxnow;
|
||||
$term_id = intval($tag->term_id);
|
||||
$permalink = get_term_link($term_id, $taxonomy);
|
||||
break;
|
||||
case 'profile':
|
||||
case 'user-edit':
|
||||
$user_id = !empty($_GET['user_id']) ? intval($_GET['user_id']) : get_current_user_id();
|
||||
$permalink = get_author_posts_url($user_id);
|
||||
break;
|
||||
}
|
||||
return apply_filters('archiver_permalink_admin', $permalink);
|
||||
}
|
||||
|
||||
public function get_current_permalink_public() {
|
||||
global $wp;
|
||||
$permalink = home_url($wp->request);
|
||||
if (!empty($_SERVER['QUERY_STRING'])) {
|
||||
$permalink .= '?' . $_SERVER['QUERY_STRING'];
|
||||
}
|
||||
return apply_filters('archiver_permalink_public', $permalink);
|
||||
}
|
||||
|
||||
public function register_scripts_and_styles() {
|
||||
wp_register_script(
|
||||
'archiver',
|
||||
ARCHIVER_PLUGIN_DIR_URL . 'js/archiver' . $this->min_suffix . '.js',
|
||||
array('jquery', 'wp-api-request', 'wp-i18n'),
|
||||
filemtime(ARCHIVER_PLUGIN_DIR_PATH . 'js/archiver' . $this->min_suffix . '.js'),
|
||||
true
|
||||
);
|
||||
wp_register_style(
|
||||
'archiver',
|
||||
ARCHIVER_PLUGIN_DIR_URL . 'css/archiver' . $this->min_suffix . '.css',
|
||||
array('dashicons'),
|
||||
filemtime(ARCHIVER_PLUGIN_DIR_PATH . 'css/archiver' . $this->min_suffix . '.css')
|
||||
);
|
||||
wp_localize_script('archiver', 'archiver', array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'rest_url' => rest_url('archiver/v1/trigger-snapshot'),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'url' => $this->get_current_permalink(),
|
||||
'i18n' => array(
|
||||
'triggering' => __('Triggering snapshot...', 'archiver'),
|
||||
'success' => __('Snapshot triggered successfully!', 'archiver'),
|
||||
'error' => __('Failed to trigger snapshot.', 'archiver')
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
public function enqueue_scripts() {
|
||||
$url = $this->get_current_permalink();
|
||||
if (!$url) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_script('archiver');
|
||||
wp_enqueue_style('archiver');
|
||||
}
|
||||
|
||||
public function admin_enqueue_scripts($hook) {
|
||||
if ('tools_page_archiver-settings' === $hook) {
|
||||
wp_enqueue_style('archiver');
|
||||
}
|
||||
wp_enqueue_script('archiver');
|
||||
}
|
||||
|
||||
public function do_admin_notice_disabled() {
|
||||
$id = 'archiver-notice-disabled';
|
||||
$dismiss_notice_key = 'archiver_dismiss_notice_' . $id;
|
||||
if (get_user_meta(get_current_user_id(), $dismiss_notice_key)) {
|
||||
return;
|
||||
}
|
||||
$class = 'archiver-notice notice notice-error is-dismissible';
|
||||
$message = __("Archiver is currently disabled via the <code>archiver_can_run</code> filter.", 'archiver');
|
||||
printf('<div id="%s" class="%s"><p>%s</p></div>', $id, $class, $message);
|
||||
}
|
||||
|
||||
public function register_rest_routes() {
|
||||
register_rest_route('archiver/v1', '/trigger-snapshot', [
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'rest_trigger_snapshot'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('edit_posts');
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_trigger_snapshot($request) {
|
||||
if (!current_user_can('edit_posts')) {
|
||||
return new WP_Error('rest_forbidden', __('You do not have permissions to trigger snapshots.', 'archiver'), array('status' => 401));
|
||||
}
|
||||
$url = $request->get_param('url');
|
||||
if (empty($url)) {
|
||||
return new WP_Error('rest_invalid_param', __('Invalid URL parameter.', 'archiver'), array('status' => 400));
|
||||
}
|
||||
$this->record_url_for_update($url, true);
|
||||
return new WP_REST_Response(array(
|
||||
'success' => true,
|
||||
'message' => __('Snapshot request recorded and will be processed soon.', 'archiver')
|
||||
), 200);
|
||||
}
|
||||
|
||||
public function process_urls_for_update() {
|
||||
$urls_to_update = get_option('archiver_urls_to_update', array());
|
||||
$processed_urls = array();
|
||||
$selected_post_types = get_option('archiver_post_types', array('post', 'page'));
|
||||
$normalized_urls = array();
|
||||
$valid_urls = array();
|
||||
$total_archived = (int) get_option('archiver_total_archived', 0);
|
||||
$failed_snapshots = (int) get_option('archiver_failed_snapshots', 0);
|
||||
|
||||
foreach ($urls_to_update as $url) {
|
||||
$normalized_url = $this->normalize_url($url);
|
||||
if (empty($normalized_url)) {
|
||||
$processed_urls[] = $url;
|
||||
continue;
|
||||
}
|
||||
if (!in_array($normalized_url, $normalized_urls)) {
|
||||
$normalized_urls[] = $normalized_url;
|
||||
$valid_urls[$normalized_url] = $url;
|
||||
} else {
|
||||
$processed_urls[] = $url;
|
||||
error_log("Archiver: Duplicate URL removed: $url");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($normalized_urls as $normalized_url) {
|
||||
$url = $valid_urls[$normalized_url];
|
||||
$post_id = $this->get_post_id_from_url($url);
|
||||
if ($post_id) {
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_status === 'draft' || $post->post_status === 'private' || !empty($post->post_password) || $this->is_preview_url($url)) {
|
||||
$processed_urls[] = $url;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!$post_id || ($post_id && in_array(get_post_type($post_id), $selected_post_types))) {
|
||||
$fetch_result = $this->fetch_and_cache_snapshots($normalized_url);
|
||||
$processed_urls[] = $url;
|
||||
if ($fetch_result) {
|
||||
$snapshot_result = $this->trigger_wayback_machine_snapshot($normalized_url);
|
||||
if ($snapshot_result) {
|
||||
$total_archived++;
|
||||
} else {
|
||||
$failed_snapshots++;
|
||||
}
|
||||
} else {
|
||||
$failed_snapshots++;
|
||||
}
|
||||
if (count($processed_urls) >= 5) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$processed_urls[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
$remaining_urls = array_diff($urls_to_update, $processed_urls);
|
||||
update_option('archiver_urls_to_update', array_values($remaining_urls));
|
||||
update_option('archiver_last_run', current_time('mysql'));
|
||||
update_option('archiver_total_archived', $total_archived);
|
||||
update_option('archiver_failed_snapshots', $failed_snapshots);
|
||||
error_log("Archiver: Updated pending URLs: " . print_r($remaining_urls, true));
|
||||
}
|
||||
|
||||
private function normalize_url($url) {
|
||||
$parsed_url = parse_url($url);
|
||||
$normalized = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : 'https://';
|
||||
$normalized .= isset($parsed_url['host']) ? $parsed_url['host'] : '';
|
||||
$normalized .= isset($parsed_url['path']) ? $parsed_url['path'] : '/';
|
||||
$normalized = preg_replace('/^www\./i', '', $normalized);
|
||||
$normalized = rtrim($normalized, '/');
|
||||
if (isset($parsed_url['query'])) {
|
||||
parse_str($parsed_url['query'], $query_params);
|
||||
if (isset($query_params['preview'])) {
|
||||
error_log("Archiver: Excluding preview URL: $url");
|
||||
return '';
|
||||
}
|
||||
if (isset($query_params['p'])) {
|
||||
$post_id = intval($query_params['p']);
|
||||
$post = get_post($post_id);
|
||||
if ($post && $post->post_status === 'publish' && empty($post->post_password)) {
|
||||
$normalized = get_permalink($post_id);
|
||||
error_log("Archiver: Normalized $url to $normalized");
|
||||
} else {
|
||||
error_log("Archiver: Excluding draft/protected URL: $url");
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
$normalized = rtrim($normalized, '/');
|
||||
error_log("Archiver: Normalized URL: $url -> $normalized");
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function get_post_id_from_url($url) {
|
||||
$post_id = url_to_postid($url);
|
||||
if (!$post_id) {
|
||||
$parsed_url = parse_url($url);
|
||||
if (isset($parsed_url['query'])) {
|
||||
parse_str($parsed_url['query'], $query_params);
|
||||
if (isset($query_params['p'])) {
|
||||
$post_id = intval($query_params['p']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
private function is_preview_url($url) {
|
||||
$parsed_url = parse_url($url);
|
||||
if (isset($parsed_url['query'])) {
|
||||
parse_str($parsed_url['query'], $query_params);
|
||||
return isset($query_params['preview']) && $query_params['preview'] === 'true';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function trigger_post_snapshot($post_id) {
|
||||
if ('publish' != get_post_status($post_id) || wp_is_post_revision($post_id)) {
|
||||
return;
|
||||
}
|
||||
$url = get_permalink($post_id);
|
||||
$this->trigger_url_snapshot($url);
|
||||
}
|
||||
|
||||
public function trigger_term_snapshot($term_id, $taxonomy_id, $taxonomy) {
|
||||
$url = get_term_link($term_id, $taxonomy);
|
||||
if (is_wp_error($url)) {
|
||||
return;
|
||||
}
|
||||
$this->trigger_url_snapshot($url);
|
||||
}
|
||||
|
||||
public function trigger_user_snapshot($user_id) {
|
||||
$url = get_author_posts_url($user_id);
|
||||
$this->trigger_url_snapshot($url);
|
||||
}
|
||||
|
||||
public function trigger_url_snapshot($url) {
|
||||
if (empty($url)) {
|
||||
return new WP_Error('empty_url', __('URL cannot be empty.', 'archiver'));
|
||||
}
|
||||
$last_snapshot = get_transient('archiver_last_snapshot_' . md5($url));
|
||||
if ($last_snapshot) {
|
||||
return new WP_Error('snapshot_throttled', __('Snapshot already taken recently.', 'archiver'));
|
||||
}
|
||||
$response = wp_safe_remote_get($this->wayback_machine_url_save . $url, array(
|
||||
'timeout' => 30,
|
||||
'sslverify' => false
|
||||
));
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
$response_code = wp_remote_retrieve_response_code($response);
|
||||
if (200 === $response_code) {
|
||||
set_transient('archiver_last_snapshot_' . md5($url), time(), HOUR_IN_SECONDS);
|
||||
return true;
|
||||
} else {
|
||||
return new WP_Error('snapshot_failed', __('Failed to trigger snapshot.', 'archiver'));
|
||||
}
|
||||
}
|
||||
|
||||
public function reschedule_cron_task($frequency) {
|
||||
$timestamp = wp_next_scheduled('archiver_process_urls');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'archiver_process_urls');
|
||||
}
|
||||
wp_schedule_event(time(), $frequency, 'archiver_process_urls');
|
||||
}
|
||||
|
||||
public static function deactivate() {
|
||||
$timestamp = wp_next_scheduled('archiver_process_urls');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'archiver_process_urls');
|
||||
}
|
||||
delete_option('archiver_urls_to_update');
|
||||
}
|
||||
}
|
120
js/archiver.js
Normal file
120
js/archiver.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
(($) => {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(() => {
|
||||
initAdminBarTrigger();
|
||||
initMetaboxButton();
|
||||
initTabSwitching();
|
||||
});
|
||||
|
||||
const initAdminBarTrigger = () => {
|
||||
const $triggerButton = $('#wp-admin-bar-archiver-trigger a');
|
||||
|
||||
if (!$triggerButton.length) return;
|
||||
|
||||
$triggerButton.on('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const $menuItem = $(e.target).closest('li');
|
||||
$menuItem.addClass('archiver-active');
|
||||
|
||||
try {
|
||||
const response = await wp.apiRequest({
|
||||
url: archiver.rest_url,
|
||||
method: 'POST',
|
||||
data: { url: archiver.url },
|
||||
beforeSend: (xhr) => {
|
||||
xhr.setRequestHeader('X-WP-Nonce', archiver.nonce);
|
||||
}
|
||||
});
|
||||
|
||||
$menuItem.removeClass('archiver-active');
|
||||
|
||||
if (response.success) {
|
||||
$menuItem.addClass('archiver-success');
|
||||
console.log('Archiver:', response.message);
|
||||
} else {
|
||||
throw new Error(response.message || wp.i18n.__('Unknown error occurred', 'archiver'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Archiver Error:', error);
|
||||
$menuItem.addClass('archiver-failure');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
$menuItem.removeClass('archiver-success archiver-failure');
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const initMetaboxButton = () => {
|
||||
const $immediateButton = $('#archiver-immediate-snapshot');
|
||||
|
||||
$immediateButton.on('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const $statusElement = $('#archiver-status');
|
||||
const url = $('#archiver-url').val();
|
||||
const nonce = $('#archiver_nonce').val();
|
||||
|
||||
$immediateButton.prop('disabled', true);
|
||||
$statusElement.show().text(archiver.i18n.triggering).removeClass('error success').addClass('processing');
|
||||
|
||||
try {
|
||||
const response = await $.ajax({
|
||||
url: archiver.ajax_url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
action: 'archiver_immediate_snapshot',
|
||||
_ajax_nonce: nonce,
|
||||
url: url
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
$statusElement.text(response.data.message).removeClass('processing').addClass('success');
|
||||
if (response.data.snapshots) {
|
||||
$('#archiver-snapshots ul').html(response.data.snapshots.join(''));
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data.message || 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Archiver Error:', error);
|
||||
$statusElement.text(`${archiver.i18n.error}: ${error.message || 'Request failed'}`)
|
||||
.removeClass('processing success')
|
||||
.addClass('error');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
$immediateButton.prop('disabled', false);
|
||||
$statusElement.fadeOut(500, () => {
|
||||
$statusElement.removeClass('processing error success').empty();
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initTabSwitching = () => {
|
||||
$('.nav-tab').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('.nav-tab').removeClass('nav-tab-active');
|
||||
$(this).addClass('nav-tab-active');
|
||||
|
||||
$('.archiver-tab-content').hide();
|
||||
const tabId = $(this).data('tab');
|
||||
$('#' + tabId).show();
|
||||
});
|
||||
};
|
||||
|
||||
if (!$('#archiver-spin-animation').length) {
|
||||
$('head').append(`
|
||||
<style id="archiver-spin-animation">
|
||||
@keyframes archiver-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
|
||||
})(jQuery);
|
1
js/archiver.min.js
vendored
Normal file
1
js/archiver.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(($)=>{'use strict';$(document).ready(()=>{initAdminBarTrigger();initMetaboxButton();initTabSwitching()});const initAdminBarTrigger=()=>{const $triggerButton=$('#wp-admin-bar-archiver-trigger a');if(!$triggerButton.length)return;$triggerButton.on('click',async(e)=>{e.preventDefault();const $menuItem=$(e.target).closest('li');$menuItem.addClass('archiver-active');try{const response=await wp.apiRequest({url:archiver.rest_url,method:'POST',data:{url:archiver.url},beforeSend:(xhr)=>{xhr.setRequestHeader('X-WP-Nonce',archiver.nonce)}});$menuItem.removeClass('archiver-active');if(response.success){$menuItem.addClass('archiver-success');console.log('Archiver:',response.message)}else{throw new Error(response.message||wp.i18n.__('Unknown error occurred','archiver'));}}catch(error){console.error('Archiver Error:',error);$menuItem.addClass('archiver-failure')}setTimeout(()=>{$menuItem.removeClass('archiver-success archiver-failure')},2000)})};const initMetaboxButton=()=>{const $immediateButton=$('#archiver-immediate-snapshot');$immediateButton.on('click',async(e)=>{e.preventDefault();const $statusElement=$('#archiver-status');const url=$('#archiver-url').val();const nonce=$('#archiver_nonce').val();$immediateButton.prop('disabled',true);$statusElement.show().text(archiver.i18n.triggering).removeClass('error success').addClass('processing');try{const response=await $.ajax({url:archiver.ajax_url,type:'POST',dataType:'json',data:{action:'archiver_immediate_snapshot',_ajax_nonce:nonce,url:url}});if(response.success){$statusElement.text(response.data.message).removeClass('processing').addClass('success');if(response.data.snapshots){$('#archiver-snapshots ul').html(response.data.snapshots.join(''))}}else{throw new Error(response.data.message||'Unknown error');}}catch(error){console.error('Archiver Error:',error);$statusElement.text(`${archiver.i18n.error}:${error.message||'Request failed'}`).removeClass('processing success').addClass('error')}finally{setTimeout(()=>{$immediateButton.prop('disabled',false);$statusElement.fadeOut(500,()=>{$statusElement.removeClass('processing error success').empty()})},3000)}})};const initTabSwitching=()=>{$('.nav-tab').on('click',function(e){e.preventDefault();$('.nav-tab').removeClass('nav-tab-active');$(this).addClass('nav-tab-active');$('.archiver-tab-content').hide();const tabId=$(this).data('tab');$('#'+tabId).show()})};if(!$('#archiver-spin-animation').length){$('head').append(`<style id="archiver-spin-animation">@keyframes archiver-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}</style>`)}})(jQuery);
|
339
license.txt
Normal file
339
license.txt
Normal file
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
Loading…
Add table
Add a link
Reference in a new issue