从gitee代码库中初始化项目。
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Options Framework
|
||||
*
|
||||
* @package Options Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*
|
||||
* @wordpress-plugin
|
||||
* Plugin Name: Options Framework
|
||||
* Plugin URI: http://wptheming.com
|
||||
* Description: A framework for building theme options.
|
||||
* Version: 1.9.1
|
||||
* Author: Devin Price
|
||||
* Author URI: http://wptheming.com
|
||||
* License: GPL-2.0+
|
||||
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
||||
* Text Domain: optionsframework
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
// If this file is called directly, abort.
|
||||
if (!defined('WPINC')) {
|
||||
die;
|
||||
}
|
||||
|
||||
// Don't load if optionsframework_init is already defined
|
||||
if (is_admin() && !function_exists('optionsframework_init')):
|
||||
|
||||
function optionsframework_init()
|
||||
{
|
||||
|
||||
// If user can't edit theme options, exit
|
||||
if (!current_user_can('edit_theme_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
require get_template_directory() . '/inc/theme-options.php';
|
||||
|
||||
// Loads the required Options Framework classes.
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-options-framework.php';
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-options-framework-admin.php';
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-options-interface.php';
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-options-media-uploader.php';
|
||||
require plugin_dir_path(__FILE__) . 'includes/class-options-sanitization.php';
|
||||
|
||||
// Instantiate the options page.
|
||||
$options_framework_admin = new Options_Framework_Admin;
|
||||
$options_framework_admin->init();
|
||||
|
||||
// Instantiate the media uploader class
|
||||
$options_framework_media_uploader = new Options_Framework_Media_Uploader;
|
||||
$options_framework_media_uploader->init();
|
||||
|
||||
}
|
||||
|
||||
add_action('init', 'optionsframework_init', 20);
|
||||
|
||||
endif;
|
||||
|
||||
/**
|
||||
* Helper function to return the theme option value.
|
||||
* If no value has been saved, it returns $default.
|
||||
* Needed because options are saved as serialized strings.
|
||||
*
|
||||
* Not in a class to support backwards compatibility in themes.
|
||||
*/
|
||||
if (!function_exists('kratos_option')):
|
||||
function kratos_option($name, $default = false)
|
||||
{
|
||||
|
||||
$option_name = 'kratos';
|
||||
|
||||
// Get option settings from database
|
||||
$options = get_option($option_name);
|
||||
|
||||
// Return specific option
|
||||
if (isset($options[$name])) {
|
||||
return $options[$name];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
endif;
|
||||
|
||||
add_action('admin_init', 'optionscheck_change_santiziation', 100);
|
||||
function optionscheck_change_santiziation()
|
||||
{
|
||||
remove_filter('of_sanitize_textarea', 'of_sanitize_textarea');
|
||||
add_filter('of_sanitize_textarea', 'custom_sanitize_textarea');
|
||||
}
|
||||
function custom_sanitize_textarea($input)
|
||||
{
|
||||
global $allowedposttags;
|
||||
$custom_allowedtags["embed"] = array(
|
||||
"src" => array(),
|
||||
"type" => array(),
|
||||
"allowfullscreen" => array(),
|
||||
"allowscriptaccess" => array(),
|
||||
"height" => array(),
|
||||
"width" => array(),
|
||||
);
|
||||
$custom_allowedtags["script"] = array("type" => array(), "src" => array());
|
||||
$custom_allowedtags = array_merge($custom_allowedtags, $allowedposttags);
|
||||
$output = wp_kses($input, $custom_allowedtags);
|
||||
return $output;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#optionsframework {
|
||||
max-width: 855px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#optionsframework h3 {
|
||||
margin: 0;
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: #f1f1f1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#optionsframework p {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#optionsframework .section {
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
|
||||
#optionsframework .group {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
#optionsframework .section .controls {
|
||||
float: left;
|
||||
padding-right: 2%;
|
||||
width: 54%;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
#optionsframework .section .explain {
|
||||
float: left;
|
||||
max-width: 38%;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
#optionsframework .section-checkbox .controls {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
#optionsframework .section-checkbox .explain {
|
||||
max-width: 94%;
|
||||
}
|
||||
|
||||
#optionsframework .controls input[type="text"],
|
||||
#optionsframework .controls input[type="password"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#optionsframework .controls input[type="text"].wp-color-picker {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
#optionsframework .controls select,
|
||||
#optionsframework .controls textarea {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#optionsframework .section-radio label,
|
||||
#optionsframework .section-multicheck label {
|
||||
float: left;
|
||||
margin-bottom: 5px;
|
||||
max-width: 90%;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
#optionsframework input.checkbox,
|
||||
#optionsframework input.of-radio {
|
||||
float: left;
|
||||
clear: both;
|
||||
margin: 0 10px 5px 0;
|
||||
}
|
||||
|
||||
#optionsframework .section-typography .controls {
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#optionsframework .section-typography .explain {
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-typography-size {
|
||||
float: left;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-typography-unit {
|
||||
float: left;
|
||||
margin-left: 5px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-typography-face {
|
||||
float: left;
|
||||
margin-left: 5px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-typography-style {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#optionsframework .section-typography .wp-picker-container {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
#optionsframework .of-background-properties {
|
||||
clear: both;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-background-repeat {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-background-position {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-background-attachment {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
#optionsframework .section-background .wp-picker-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-radio-img-img {
|
||||
float: left;
|
||||
display: none;
|
||||
margin: 0 5px 10px 0;
|
||||
border: 3px solid #f9f9f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-radio-img-selected {
|
||||
border: 3px solid #ccc;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-radio-img-img:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-border-width {
|
||||
float: left;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#optionsframework .controls .of-border-style {
|
||||
float: left;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
#optionsframework .hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#optionsframework .of-option-image {
|
||||
margin: 3px 0 18px 0;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
#optionsframework .mini .controls select,
|
||||
#optionsframework .section .mini .controls {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
#optionsframework .mini .controls input,
|
||||
#optionsframework .mini .controls {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
#optionsframework .mini .explain {
|
||||
max-width: 74%;
|
||||
}
|
||||
|
||||
#optionsframework .section-info {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
#optionsframework .controls input.upload {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#optionsframework .screenshot {
|
||||
position: relative;
|
||||
float: left;
|
||||
margin-top: 3px;
|
||||
margin-left: 1px;
|
||||
width: 344px;
|
||||
}
|
||||
|
||||
#optionsframework .screenshot img {
|
||||
float: left;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px;
|
||||
max-width: 334px;
|
||||
border-color: #ccc #eee #eee #ccc;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
#optionsframework .screenshot .remove-image {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: -4px;
|
||||
float: left;
|
||||
display: block;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: medium none;
|
||||
background: url("../images/ico-delete.png") no-repeat;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
#optionsframework .screenshot .no_image .file_link {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#optionsframework .screenshot .no_image .remove-button {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
#optionsframework .reset-button {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#optionsframework-submit {
|
||||
padding: 7px 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
#optionsframework .button-primary {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#optionsframework .section:after {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#optionsframework .section:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#optionsframework .about-content {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
#optionsframework .about-content img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#optionsframework .about-content h4 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#optionsframework .about-content ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#optionsframework .about-content ul li {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
#optionsframework .about-content .notices {
|
||||
color: #727777;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#optionsframework .about-content .tips {
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#optionsframework .about-content .tips b {
|
||||
margin-left: 3px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 715 B |
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Options_Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*/
|
||||
|
||||
class Options_Framework_Admin {
|
||||
|
||||
/**
|
||||
* Page hook for the options screen
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @type string
|
||||
*/
|
||||
protected $options_screen = null;
|
||||
|
||||
/**
|
||||
* Hook in the scripts and styles
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public function init() {
|
||||
|
||||
// Gets options to load
|
||||
$options = & Options_Framework::_optionsframework_options();
|
||||
|
||||
// Checks if options are available
|
||||
if ( $options ) {
|
||||
|
||||
// Add the options page and menu item.
|
||||
add_action('admin_menu', array($this, 'add_top_options_page'));
|
||||
add_action('admin_menu', array($this, 'add_sub_options_page'));
|
||||
|
||||
// Add the required scripts and styles
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
||||
|
||||
// Settings need to be registered after admin_init
|
||||
add_action( 'admin_init', array( $this, 'settings_init' ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the settings
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
function settings_init() {
|
||||
|
||||
// Get the option name
|
||||
$options_framework = new Options_Framework;
|
||||
$name = $options_framework->get_option_name();
|
||||
|
||||
// Registers the settings fields and callback
|
||||
register_setting( 'optionsframework', $name, array ( $this, 'validate_options' ) );
|
||||
|
||||
// Displays notice after options save
|
||||
add_action( 'optionsframework_after_validate', array( $this, 'save_options_notice' ) );
|
||||
|
||||
add_action( 'optionsframework_after_sendmail', array( $this, 'send_mail_notice' ) );
|
||||
}
|
||||
|
||||
public function add_top_options_page()
|
||||
{
|
||||
add_menu_page(
|
||||
__('主题设置', 'kratos'),
|
||||
__('主题设置', 'kratos'),
|
||||
'manage_options',
|
||||
'kratos_options',
|
||||
'',
|
||||
'dashicons-admin-generic',
|
||||
99
|
||||
);
|
||||
}
|
||||
|
||||
public static function menu_settings()
|
||||
{
|
||||
$menu = array(
|
||||
'parent_slug' => 'kratos_options',
|
||||
'page_title' => __('主题设置', 'kratos'),
|
||||
'menu_title' => __('主题设置', 'kratos'),
|
||||
'capability' => 'manage_options',
|
||||
'menu_slug' => 'kratos_options',
|
||||
);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
public function add_sub_options_page()
|
||||
{
|
||||
$menu = $this->menu_settings();
|
||||
|
||||
$this->options_screen = add_submenu_page(
|
||||
$menu['parent_slug'],
|
||||
$menu['page_title'],
|
||||
$menu['menu_title'],
|
||||
$menu['capability'],
|
||||
$menu['menu_slug'],
|
||||
array($this, 'options_page')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the required stylesheets
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
|
||||
function enqueue_admin_styles( $hook ) {
|
||||
|
||||
if ( $this->options_screen != $hook )
|
||||
return;
|
||||
|
||||
wp_enqueue_style( 'optionsframework', get_stylesheet_directory_uri() . '/inc/options-framework/css/optionsframework.css', array(), Options_Framework::VERSION );
|
||||
wp_enqueue_style( 'wp-color-picker' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the required javascript
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
function enqueue_admin_scripts( $hook ) {
|
||||
|
||||
if ( $this->options_screen != $hook )
|
||||
return;
|
||||
|
||||
// Enqueue custom option panel JS
|
||||
wp_enqueue_script(
|
||||
'options-custom',
|
||||
get_stylesheet_directory_uri() . '/inc/options-framework/js/options-custom.js',
|
||||
array( 'jquery','wp-color-picker' ),
|
||||
Options_Framework::VERSION
|
||||
);
|
||||
|
||||
// Inline scripts from options-interface.php
|
||||
add_action( 'admin_head', array( $this, 'of_admin_head' ) );
|
||||
}
|
||||
|
||||
function of_admin_head() {
|
||||
// Hook to add custom scripts
|
||||
do_action( 'optionsframework_custom_scripts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out the options panel.
|
||||
*
|
||||
* If we were using the Settings API as it was intended we would use
|
||||
* do_settings_sections here. But as we don't want the settings wrapped in a table,
|
||||
* we'll call our own custom optionsframework_fields. See options-interface.php
|
||||
* for specifics on how each individual field is generated.
|
||||
*
|
||||
* Nonces are provided using the settings_fields()
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
function options_page() { ?>
|
||||
|
||||
<div id="optionsframework-wrap" class="wrap">
|
||||
|
||||
<?php $menu = $this->menu_settings(); ?>
|
||||
<h2><?php echo esc_html( $menu['page_title'] ); ?></h2>
|
||||
|
||||
<h2 class="nav-tab-wrapper">
|
||||
<?php echo Options_Framework_Interface::optionsframework_tabs(); ?>
|
||||
</h2>
|
||||
|
||||
<?php settings_errors( 'options-framework' ); ?>
|
||||
|
||||
<div id="optionsframework-metabox" class="metabox-holder">
|
||||
<div id="optionsframework" class="postbox">
|
||||
<form action="options.php" method="post">
|
||||
<?php settings_fields( 'optionsframework' ); ?>
|
||||
<?php Options_Framework_Interface::optionsframework_fields(); /* Settings */ ?>
|
||||
<div id="optionsframework-submit">
|
||||
<input type="submit" class="button-primary" name="update" value="<?php esc_attr_e( '保存配置', 'kratos' ); ?>" />
|
||||
<input type="submit" class="reset-button button-secondary" name="reset" value="<?php esc_attr_e( '恢复默认', 'kratos' ); ?>" onclick="return confirm( '<?php print esc_js( __( '单击「确定」进行恢复,但所有的配置都将丢失!', 'kratos' ) ); ?>' );" />
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- / #container -->
|
||||
</div>
|
||||
<?php do_action( 'optionsframework_after' ); ?>
|
||||
</div> <!-- / .wrap -->
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Options.
|
||||
*
|
||||
* This runs after the submit/reset button has been clicked and
|
||||
* validates the inputs.
|
||||
*
|
||||
* @uses $_POST['reset'] to restore default options
|
||||
*/
|
||||
function validate_options( $input ) {
|
||||
|
||||
/*
|
||||
* Restore Defaults.
|
||||
*
|
||||
* In the event that the user clicked the "Restore Defaults"
|
||||
* button, the options defined in the theme's options.php
|
||||
* file will be added to the option for the active theme.
|
||||
*/
|
||||
|
||||
if ( isset( $_POST['reset'] ) ) {
|
||||
add_settings_error( 'options-framework', 'restore_defaults', __( '恢复完成', 'kratos' ), 'updated fade' );
|
||||
return $this->get_default_values();
|
||||
}
|
||||
|
||||
/*
|
||||
* Update Settings
|
||||
*
|
||||
* This used to check for $_POST['update'], but has been updated
|
||||
* to be compatible with the theme customizer introduced in WordPress 3.4
|
||||
*/
|
||||
|
||||
$clean = array();
|
||||
$options = & Options_Framework::_optionsframework_options();
|
||||
foreach ( $options as $option ) {
|
||||
|
||||
if ( ! isset( $option['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $option['type'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = preg_replace( '/[^a-zA-Z0-9._\-]/', '', strtolower( $option['id'] ) );
|
||||
|
||||
// Set checkbox to false if it wasn't sent in the $_POST
|
||||
if ( 'checkbox' == $option['type'] && ! isset( $input[$id] ) ) {
|
||||
$input[$id] = false;
|
||||
}
|
||||
|
||||
// Set each item in the multicheck to false if it wasn't sent in the $_POST
|
||||
if ( 'multicheck' == $option['type'] && ! isset( $input[$id] ) ) {
|
||||
foreach ( $option['options'] as $key => $value ) {
|
||||
$input[$id][$key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// For a value to be submitted to database it must pass through a sanitization filter
|
||||
if ( has_filter( 'of_sanitize_' . $option['type'] ) ) {
|
||||
$clean[$id] = apply_filters( 'of_sanitize_' . $option['type'], $input[$id], $option );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $_POST['sendmail'] ) ) {
|
||||
wp_mail( get_bloginfo( 'admin_email' ) ,__('[测试]邮件服务配置成功', 'kratos'),__('恭喜您 SMTP 邮件服务配置成功!', 'kratos'));
|
||||
do_action( 'optionsframework_after_sendmail', $clean );
|
||||
return $clean;
|
||||
} else {
|
||||
do_action( 'optionsframework_after_validate', $clean );
|
||||
return $clean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display message when options have been saved
|
||||
*/
|
||||
function save_options_notice() {
|
||||
add_settings_error( 'options-framework', 'save_options', __( '保存成功', 'kratos' ), 'updated fade' );
|
||||
}
|
||||
|
||||
function send_mail_notice() {
|
||||
add_settings_error( 'options-framework', 'send_mail', __( '发送完成,请留意邮箱:' . get_bloginfo( 'admin_email' ), 'kratos' ), 'updated fade' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default values for all the theme options
|
||||
*
|
||||
* Get an array of all default values as set in
|
||||
* options.php. The 'id','std' and 'type' keys need
|
||||
* to be defined in the configuration array. In the
|
||||
* event that these keys are not present the option
|
||||
* will not be included in this function's output.
|
||||
*
|
||||
* @return array Re-keyed options configuration array.
|
||||
*
|
||||
*/
|
||||
function get_default_values() {
|
||||
$output = array();
|
||||
$config = & Options_Framework::_optionsframework_options();
|
||||
foreach ( (array) $config as $option ) {
|
||||
if ( ! isset( $option['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $option['std'] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $option['type'] ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( has_filter( 'of_sanitize_' . $option['type'] ) ) {
|
||||
$output[$option['id']] = apply_filters( 'of_sanitize_' . $option['type'], $option['std'], $option );
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Options_Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*/
|
||||
|
||||
class Options_Framework {
|
||||
|
||||
/**
|
||||
* Plugin version, used for cache-busting of style and script file references.
|
||||
*
|
||||
* @since 1.7.0
|
||||
* @type string
|
||||
*/
|
||||
const VERSION = '1.9.1';
|
||||
|
||||
/**
|
||||
* Gets option name
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
function get_option_name() {
|
||||
|
||||
$name = 'kratos';
|
||||
|
||||
return apply_filters( 'options_framework_option_name', $name );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for optionsframework_options()
|
||||
*
|
||||
* Allows for manipulating or setting options via 'of_options' filter
|
||||
* For example:
|
||||
*
|
||||
* <code>
|
||||
* add_filter( 'of_options', function( $options ) {
|
||||
* $options[] = array(
|
||||
* 'name' => 'Input Text Mini',
|
||||
* 'desc' => 'A mini text input field.',
|
||||
* 'id' => 'example_text_mini',
|
||||
* 'std' => 'Default',
|
||||
* 'class' => 'mini',
|
||||
* 'type' => 'text'
|
||||
* );
|
||||
*
|
||||
* return $options;
|
||||
* });
|
||||
* </code>
|
||||
*
|
||||
* Also allows for setting options via a return statement in the
|
||||
* options.php file. For example (in options.php):
|
||||
*
|
||||
* <code>
|
||||
* return array(...);
|
||||
* </code>
|
||||
*
|
||||
* @return array (by reference)
|
||||
*/
|
||||
static function &_optionsframework_options() {
|
||||
static $options = null;
|
||||
|
||||
if ( !$options ) {
|
||||
// Load options from options.php file (if it exists)
|
||||
$location = apply_filters( 'options_framework_location', array( 'theme-options.php' ) );
|
||||
$options = kratos_options();
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Options_Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*/
|
||||
|
||||
class Options_Framework_Interface
|
||||
{
|
||||
|
||||
/**
|
||||
* Generates the tabs that are used in the options menu
|
||||
*/
|
||||
public static function optionsframework_tabs()
|
||||
{
|
||||
$counter = 0;
|
||||
$options = &Options_Framework::_optionsframework_options();
|
||||
$menu = '';
|
||||
|
||||
foreach ($options as $value) {
|
||||
// Heading for Navigation
|
||||
if ($value['type'] == "heading") {
|
||||
$counter++;
|
||||
$class = '';
|
||||
$class = !empty($value['id']) ? $value['id'] : $value['name'];
|
||||
$class = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($class)) . '-tab';
|
||||
$menu .= '<a id="options-group-' . $counter . '-tab" class="nav-tab ' . $class . '" title="' . esc_attr($value['name']) . '" href="' . esc_attr('#options-group-' . $counter) . '">' . esc_html($value['name']) . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the options fields that are used in the form.
|
||||
*/
|
||||
public static function optionsframework_fields()
|
||||
{
|
||||
|
||||
global $allowedtags;
|
||||
|
||||
$options_framework = new Options_Framework;
|
||||
$option_name = $options_framework->get_option_name();
|
||||
$settings = get_option($option_name);
|
||||
$options = &Options_Framework::_optionsframework_options();
|
||||
|
||||
$counter = 0;
|
||||
$menu = '';
|
||||
|
||||
foreach ($options as $value) {
|
||||
|
||||
$val = '';
|
||||
$select_value = '';
|
||||
$output = '';
|
||||
|
||||
// Wrap all options
|
||||
if (($value['type'] != "heading") && ($value['type'] != "info") && ($value['type'] != "about")) {
|
||||
|
||||
// Keep all ids lowercase with no spaces
|
||||
$value['id'] = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($value['id']));
|
||||
|
||||
$id = 'section-' . $value['id'];
|
||||
|
||||
$class = 'section';
|
||||
if (isset($value['type'])) {
|
||||
$class .= ' section-' . $value['type'];
|
||||
}
|
||||
if (isset($value['class'])) {
|
||||
$class .= ' ' . $value['class'];
|
||||
}
|
||||
|
||||
$output .= '<div id="' . esc_attr($id) . '" class="' . esc_attr($class) . '">' . "\n";
|
||||
if (isset($value['name'])) {
|
||||
$output .= '<h4 class="heading">' . esc_html($value['name']) . '</h4>' . "\n";
|
||||
}
|
||||
if ($value['type'] != 'editor') {
|
||||
$output .= '<div class="option">' . "\n" . '<div class="controls">' . "\n";
|
||||
} else {
|
||||
$output .= '<div class="option">' . "\n" . '<div>' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Set default value to $val
|
||||
if (isset($value['std'])) {
|
||||
$val = $value['std'];
|
||||
}
|
||||
|
||||
// If the option is already saved, override $val
|
||||
if (($value['type'] != 'heading') && ($value['type'] != 'info') && ($value['type'] != "about")) {
|
||||
if (isset($settings[($value['id'])])) {
|
||||
$val = $settings[($value['id'])];
|
||||
// Striping slashes of non-array options
|
||||
if (!is_array($val)) {
|
||||
$val = stripslashes($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a description save it for labels
|
||||
$explain_value = '';
|
||||
if (isset($value['desc'])) {
|
||||
$explain_value = $value['desc'];
|
||||
}
|
||||
|
||||
// Set the placeholder if one exists
|
||||
$placeholder = '';
|
||||
if (isset($value['placeholder'])) {
|
||||
$placeholder = ' placeholder="' . esc_attr($value['placeholder']) . '"';
|
||||
}
|
||||
|
||||
if (has_filter('optionsframework_' . $value['type'])) {
|
||||
$output .= apply_filters('optionsframework_' . $value['type'], $option_name, $value, $val);
|
||||
}
|
||||
|
||||
switch ($value['type']) {
|
||||
|
||||
// Basic text input
|
||||
case 'text':
|
||||
$output .= '<input id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" type="text" value="' . esc_attr($val) . '"' . $placeholder . ' />';
|
||||
break;
|
||||
|
||||
// Password input
|
||||
case 'password':
|
||||
$output .= '<input id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" type="password" value="' . esc_attr($val) . '" />';
|
||||
break;
|
||||
|
||||
// Textarea
|
||||
case 'textarea':
|
||||
$rows = '8';
|
||||
|
||||
if (isset($value['settings']['rows'])) {
|
||||
$custom_rows = $value['settings']['rows'];
|
||||
if (is_numeric($custom_rows)) {
|
||||
$rows = $custom_rows;
|
||||
}
|
||||
}
|
||||
|
||||
$val = stripslashes($val);
|
||||
$output .= '<textarea id="' . esc_attr($value['id']) . '" class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" rows="' . $rows . '"' . $placeholder . '>' . esc_textarea($val) . '</textarea>';
|
||||
break;
|
||||
|
||||
// Select Box
|
||||
case 'select':
|
||||
$output .= '<select class="of-input" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" id="' . esc_attr($value['id']) . '">';
|
||||
|
||||
foreach ($value['options'] as $key => $option) {
|
||||
$output .= '<option' . selected($val, $key, false) . ' value="' . esc_attr($key) . '">' . esc_html($option) . '</option>';
|
||||
}
|
||||
$output .= '</select>';
|
||||
break;
|
||||
|
||||
// Radio Box
|
||||
case "radio":
|
||||
$name = $option_name . '[' . $value['id'] . ']';
|
||||
foreach ($value['options'] as $key => $option) {
|
||||
$id = $option_name . '-' . $value['id'] . '-' . $key;
|
||||
$output .= '<input class="of-input of-radio" type="radio" name="' . esc_attr($name) . '" id="' . esc_attr($id) . '" value="' . esc_attr($key) . '" ' . checked($val, $key, false) . ' /><label for="' . esc_attr($id) . '">' . esc_html($option) . '</label>';
|
||||
}
|
||||
break;
|
||||
|
||||
// Image Selectors
|
||||
case "images":
|
||||
$name = $option_name . '[' . $value['id'] . ']';
|
||||
foreach ($value['options'] as $key => $option) {
|
||||
$selected = '';
|
||||
if ($val != '' && ($val == $key)) {
|
||||
$selected = ' of-radio-img-selected';
|
||||
}
|
||||
$output .= '<input type="radio" id="' . esc_attr($value['id'] . '_' . $key) . '" class="of-radio-img-radio" value="' . esc_attr($key) . '" name="' . esc_attr($name) . '" ' . checked($val, $key, false) . ' />';
|
||||
$output .= '<div class="of-radio-img-label">' . esc_html($key) . '</div>';
|
||||
$output .= '<img src="' . esc_url($option) . '" alt="' . $option . '" class="of-radio-img-img' . $selected . '" onclick="document.getElementById(\'' . esc_attr($value['id'] . '_' . $key) . '\').checked=true;" />';
|
||||
}
|
||||
break;
|
||||
|
||||
// Checkbox
|
||||
case "checkbox":
|
||||
$output .= '<input id="' . esc_attr($value['id']) . '" class="checkbox of-input" type="checkbox" name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" ' . checked($val, 1, false) . ' />';
|
||||
$output .= '<label class="explain" for="' . esc_attr($value['id']) . '">' . wp_kses($explain_value, $allowedtags) . '</label>';
|
||||
break;
|
||||
|
||||
// Color picker
|
||||
case "color":
|
||||
$default_color = '';
|
||||
if (isset($value['std'])) {
|
||||
if ($val != $value['std']) {
|
||||
$default_color = ' data-default-color="' . $value['std'] . '" ';
|
||||
}
|
||||
|
||||
}
|
||||
$output .= '<input name="' . esc_attr($option_name . '[' . $value['id'] . ']') . '" id="' . esc_attr($value['id']) . '" class="of-color" type="text" value="' . esc_attr($val) . '"' . $default_color . ' />';
|
||||
|
||||
break;
|
||||
|
||||
// Uploader
|
||||
case "upload":
|
||||
$output .= Options_Framework_Media_Uploader::optionsframework_uploader($value['id'], $val, null);
|
||||
|
||||
break;
|
||||
|
||||
case "info":
|
||||
$id = '';
|
||||
$class = 'section';
|
||||
if (isset($value['id'])) {
|
||||
$id = 'id="' . esc_attr($value['id']) . '" ';
|
||||
}
|
||||
if (isset($value['type'])) {
|
||||
$class .= ' section-' . $value['type'];
|
||||
}
|
||||
if (isset($value['class'])) {
|
||||
$class .= ' ' . $value['class'];
|
||||
}
|
||||
|
||||
$output .= '<div ' . $id . 'class="' . esc_attr($class) . '">' . "\n";
|
||||
if (isset($value['name'])) {
|
||||
$output .= '<h4 class="heading">' . esc_html($value['name']) . '</h4>' . "\n";
|
||||
}
|
||||
if (isset($value['desc'])) {
|
||||
$output .= $value['desc'] . "\n";
|
||||
}
|
||||
$output .= '</div>' . "\n";
|
||||
break;
|
||||
case "sendmail":
|
||||
$output .= '<input type="submit" name="sendmail" class="button-secondary" value="' . __('测试邮件', 'kratos') . '">';
|
||||
break;
|
||||
|
||||
// Heading for Navigation
|
||||
case "heading":
|
||||
$counter++;
|
||||
if ($counter >= 2) {
|
||||
$output .= '</div>' . "\n";
|
||||
}
|
||||
$class = '';
|
||||
$class = !empty($value['id']) ? $value['id'] : $value['name'];
|
||||
$class = preg_replace('/[^a-zA-Z0-9._\-]/', '', strtolower($class));
|
||||
$output .= '<div id="options-group-' . $counter . '" class="group ' . $class . '">';
|
||||
$output .= '<h3>' . esc_html($value['name']) . '</h3>' . "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
if (($value['type'] != "heading") && ($value['type'] != "info") && ($value['type'] != "about")) {
|
||||
$output .= '</div>';
|
||||
if (($value['type'] != "checkbox") && ($value['type'] != "editor")) {
|
||||
$output .= '<div class="explain">' . wp_kses($explain_value, $allowedtags) . '</div>' . "\n";
|
||||
}
|
||||
$output .= '</div></div>' . "\n";
|
||||
}
|
||||
|
||||
echo $output;
|
||||
}
|
||||
|
||||
// Outputs closing div if there tabs
|
||||
if (Options_Framework_Interface::optionsframework_tabs() != '') {
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Options_Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*/
|
||||
|
||||
class Options_Framework_Media_Uploader {
|
||||
|
||||
/**
|
||||
* Initialize the media uploader class
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'optionsframework_media_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Uploader Using the WordPress Media Library.
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* string $_id - A token to identify this field (the name).
|
||||
* string $_value - The value of the field, if present.
|
||||
* string $_desc - An optional description of the field.
|
||||
*
|
||||
*/
|
||||
|
||||
static function optionsframework_uploader( $_id, $_value, $_desc = '', $_name = '' ) {
|
||||
|
||||
// Gets the unique option id
|
||||
$options_framework = new Options_Framework;
|
||||
$option_name = $options_framework->get_option_name();
|
||||
|
||||
$output = '';
|
||||
$id = '';
|
||||
$class = '';
|
||||
$int = '';
|
||||
$value = '';
|
||||
$name = '';
|
||||
|
||||
$id = strip_tags( strtolower( $_id ) );
|
||||
|
||||
// If a value is passed and we don't have a stored value, use the value that's passed through.
|
||||
if ( $_value != '' && $value == '' ) {
|
||||
$value = $_value;
|
||||
}
|
||||
|
||||
if ($_name != '') {
|
||||
$name = $_name;
|
||||
}
|
||||
else {
|
||||
$name = $option_name.'['.$id.']';
|
||||
}
|
||||
|
||||
if ( $value ) {
|
||||
$class = ' has-file';
|
||||
}
|
||||
$output .= '<input id="' . $id . '" class="upload' . $class . '" type="text" name="'.$name.'" value="' . $value . '" placeholder="' . __('没有选择任何文件', 'kratos') .'" />' . "\n";
|
||||
if ( function_exists( 'wp_enqueue_media' ) ) {
|
||||
if ( ( $value == '' ) ) {
|
||||
$output .= '<input id="upload-' . $id . '" class="upload-button button" type="button" value="' . __( '上传', 'kratos' ) . '" />' . "\n";
|
||||
} else {
|
||||
$output .= '<input id="remove-' . $id . '" class="remove-file button" type="button" value="' . __( '删除', 'kratos' ) . '" />' . "\n";
|
||||
}
|
||||
} else {
|
||||
$output .= '<p><i>' . __( '升级 WordPress 版本获得完整的媒体支持', 'kratos' ) . '</i></p>';
|
||||
}
|
||||
|
||||
if ( $_desc != '' ) {
|
||||
$output .= '<span class="of-metabox-desc">' . $_desc . '</span>' . "\n";
|
||||
}
|
||||
|
||||
$output .= '<div class="screenshot" id="' . $id . '-image">' . "\n";
|
||||
|
||||
if ( $value != '' ) {
|
||||
$remove = '<a class="remove-image">删除</a>';
|
||||
$image = preg_match( '/(^.*\.jpg|jpeg|png|gif|svg|ico*)/i', $value );
|
||||
if ( $image ) {
|
||||
$output .= '<img src="' . $value . '" alt="" />' . $remove;
|
||||
} else {
|
||||
$parts = explode( "/", $value );
|
||||
for( $i = 0; $i < sizeof( $parts ); ++$i ) {
|
||||
$title = $parts[$i];
|
||||
}
|
||||
|
||||
// No output preview if it's not an image.
|
||||
$output .= '';
|
||||
|
||||
// Standard generic output if it's not an image.
|
||||
$title = __( '浏览文件', 'kratos' );
|
||||
$output .= '<div class="no-image"><span class="file_link"><a href="' . $value . '" target="_blank" rel="external">'.$title.'</a></span></div>';
|
||||
}
|
||||
}
|
||||
$output .= '</div>' . "\n";
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts for file uploader
|
||||
*/
|
||||
function optionsframework_media_scripts( $hook ) {
|
||||
|
||||
$menu = Options_Framework_Admin::menu_settings();
|
||||
|
||||
if ( substr( $hook, -strlen( $menu['menu_slug'] ) ) !== $menu['menu_slug'] )
|
||||
return;
|
||||
|
||||
if ( function_exists( 'wp_enqueue_media' ) )
|
||||
wp_enqueue_media();
|
||||
|
||||
wp_register_script( 'of-media-uploader', get_stylesheet_directory_uri() . '/inc/options-framework/js/media-uploader.js', array( 'jquery' ), Options_Framework::VERSION );
|
||||
wp_enqueue_script( 'of-media-uploader' );
|
||||
wp_localize_script( 'of-media-uploader', 'optionsframework_l10n', array(
|
||||
'upload' => __( '上传', 'kratos' ),
|
||||
'remove' => __( '删除', 'kratos' )
|
||||
) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Options_Framework
|
||||
* @author Devin Price <devin@wptheming.com>
|
||||
* @license GPL-2.0+
|
||||
* @link http://wptheming.com
|
||||
* @copyright 2010-2014 WP Theming
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sanitization for text input
|
||||
*
|
||||
* @link http://developer.wordpress.org/reference/functions/sanitize_text_field/
|
||||
*/
|
||||
add_filter( 'of_sanitize_text', 'sanitize_text_field' );
|
||||
|
||||
/**
|
||||
* Sanitization for password input
|
||||
*
|
||||
* @link http://developer.wordpress.org/reference/functions/sanitize_text_field/
|
||||
*/
|
||||
add_filter( 'of_sanitize_password', 'sanitize_text_field' );
|
||||
|
||||
/**
|
||||
* Sanitization for select input
|
||||
*
|
||||
* Validates that the selected option is a valid option.
|
||||
*/
|
||||
add_filter( 'of_sanitize_select', 'of_sanitize_enum', 10, 2 );
|
||||
|
||||
/**
|
||||
* Sanitization for radio input
|
||||
*
|
||||
* Validates that the selected option is a valid option.
|
||||
*/
|
||||
add_filter( 'of_sanitize_radio', 'of_sanitize_enum', 10, 2 );
|
||||
|
||||
/**
|
||||
* Sanitization for image selector
|
||||
*
|
||||
* Validates that the selected option is a valid option.
|
||||
*/
|
||||
add_filter( 'of_sanitize_images', 'of_sanitize_enum', 10, 2 );
|
||||
|
||||
/**
|
||||
* Sanitization for textarea field
|
||||
*
|
||||
* @param $input string
|
||||
* @return $output sanitized string
|
||||
*/
|
||||
function of_sanitize_textarea( $input ) {
|
||||
global $allowedposttags;
|
||||
$output = wp_kses( $input, $allowedposttags );
|
||||
return $output;
|
||||
}
|
||||
add_filter( 'of_sanitize_textarea', 'of_sanitize_textarea' );
|
||||
|
||||
/**
|
||||
* Sanitization for checkbox input
|
||||
*
|
||||
* @param $input string (1 or empty) checkbox state
|
||||
* @return $output '1' or false
|
||||
*/
|
||||
function of_sanitize_checkbox( $input ) {
|
||||
if ( $input ) {
|
||||
$output = '1';
|
||||
} else {
|
||||
$output = false;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
add_filter( 'of_sanitize_checkbox', 'of_sanitize_checkbox' );
|
||||
|
||||
/**
|
||||
* File upload sanitization.
|
||||
*
|
||||
* Returns a sanitized filepath if it has a valid extension.
|
||||
*
|
||||
* @param string $input filepath
|
||||
* @returns string $output filepath
|
||||
*/
|
||||
function of_sanitize_upload( $input ) {
|
||||
$output = '';
|
||||
$filetype = wp_check_filetype( $input );
|
||||
if ( $filetype["ext"] ) {
|
||||
$output = esc_url( $input );
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
add_filter( 'of_sanitize_upload', 'of_sanitize_upload' );
|
||||
|
||||
/**
|
||||
* Sanitization of input with allowed tags and wpautotop.
|
||||
*
|
||||
* Allows allowed tags in html input and ensures tags close properly.
|
||||
*
|
||||
* @param string $input
|
||||
* @returns string $output
|
||||
*/
|
||||
function of_sanitize_allowedtags( $input ) {
|
||||
global $allowedtags;
|
||||
$output = wpautop( wp_kses( $input, $allowedtags ) );
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitization of input with allowed post tags and wpautotop.
|
||||
*
|
||||
* Allows allowed post tags in html input and ensures tags close properly.
|
||||
*
|
||||
* @param string $input
|
||||
* @returns string $output
|
||||
*/
|
||||
function of_sanitize_allowedposttags( $input ) {
|
||||
global $allowedposttags;
|
||||
$output = wpautop( wp_kses( $input, $allowedposttags) );
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the $input is one of the avilable choices
|
||||
* for that specific option.
|
||||
*
|
||||
* @param string $input
|
||||
* @returns string $output
|
||||
*/
|
||||
function of_sanitize_enum( $input, $option ) {
|
||||
$output = '';
|
||||
if ( array_key_exists( $input, $option['options'] ) ) {
|
||||
$output = $input;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitization for background option.
|
||||
*
|
||||
* @returns array $output
|
||||
*/
|
||||
function of_sanitize_background( $input ) {
|
||||
|
||||
$output = wp_parse_args( $input, array(
|
||||
'color' => '',
|
||||
'image' => '',
|
||||
'repeat' => 'repeat',
|
||||
'position' => 'top center',
|
||||
'attachment' => 'scroll'
|
||||
) );
|
||||
|
||||
$output['color'] = apply_filters( 'of_sanitize_hex', $input['color'] );
|
||||
$output['image'] = apply_filters( 'of_sanitize_upload', $input['image'] );
|
||||
|
||||
return $output;
|
||||
}
|
||||
add_filter( 'of_sanitize_background', 'of_sanitize_background' );
|
||||
|
||||
/**
|
||||
* Sanitize a color represented in hexidecimal notation.
|
||||
*
|
||||
* @param string Color in hexidecimal notation. "#" may or may not be prepended to the string.
|
||||
* @param string The value that this function should return if it cannot be recognized as a color.
|
||||
* @return string
|
||||
*/
|
||||
|
||||
function of_sanitize_hex( $hex, $default = '' ) {
|
||||
if ( of_validate_hex( $hex ) ) {
|
||||
return $hex;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
add_filter( 'of_sanitize_color', 'of_sanitize_hex' );
|
||||
|
||||
/**
|
||||
* Is a given string a color formatted in hexidecimal notation?
|
||||
*
|
||||
* @param string Color in hexidecimal notation. "#" may or may not be prepended to the string.
|
||||
* @return bool
|
||||
*/
|
||||
function of_validate_hex( $hex ) {
|
||||
$hex = trim( $hex );
|
||||
/* Strip recognized prefixes. */
|
||||
if ( 0 === strpos( $hex, '#' ) ) {
|
||||
$hex = substr( $hex, 1 );
|
||||
}
|
||||
elseif ( 0 === strpos( $hex, '%23' ) ) {
|
||||
$hex = substr( $hex, 3 );
|
||||
}
|
||||
/* Regex match. */
|
||||
if ( 0 === preg_match( '/^[0-9a-fA-F]{6}$/', $hex ) ) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
jQuery(document).ready(function($){
|
||||
|
||||
var optionsframework_upload;
|
||||
var optionsframework_selector;
|
||||
|
||||
function optionsframework_add_file(event, selector) {
|
||||
|
||||
var upload = $(".uploaded-file"), frame;
|
||||
var $el = $(this);
|
||||
optionsframework_selector = selector;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// If the media frame already exists, reopen it.
|
||||
if ( optionsframework_upload ) {
|
||||
optionsframework_upload.open();
|
||||
} else {
|
||||
// Create the media frame.
|
||||
optionsframework_upload = wp.media.frames.optionsframework_upload = wp.media({
|
||||
// Set the title of the modal.
|
||||
title: $el.data('choose'),
|
||||
|
||||
// Customize the submit button.
|
||||
button: {
|
||||
// Set the text of the button.
|
||||
text: $el.data('update'),
|
||||
// Tell the button not to close the modal, since we're
|
||||
// going to refresh the page when the image is selected.
|
||||
close: false
|
||||
}
|
||||
});
|
||||
|
||||
// When an image is selected, run a callback.
|
||||
optionsframework_upload.on( 'select', function() {
|
||||
// Grab the selected attachment.
|
||||
var attachment = optionsframework_upload.state().get('selection').first();
|
||||
optionsframework_upload.close();
|
||||
optionsframework_selector.find('.upload').val(attachment.attributes.url);
|
||||
if ( attachment.attributes.type == 'image' ) {
|
||||
optionsframework_selector.find('.screenshot').empty().hide().append('<img src="' + attachment.attributes.url + '"><a class="remove-image">Remove</a>').slideDown('fast');
|
||||
}
|
||||
optionsframework_selector.find('.upload-button').unbind().addClass('remove-file').removeClass('upload-button').val(optionsframework_l10n.remove);
|
||||
optionsframework_selector.find('.of-background-properties').slideDown();
|
||||
optionsframework_selector.find('.remove-image, .remove-file').on('click', function() {
|
||||
optionsframework_remove_file( $(this).parents('.section') );
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Finally, open the modal.
|
||||
optionsframework_upload.open();
|
||||
}
|
||||
|
||||
function optionsframework_remove_file(selector) {
|
||||
selector.find('.remove-image').hide();
|
||||
selector.find('.upload').val('');
|
||||
selector.find('.of-background-properties').hide();
|
||||
selector.find('.screenshot').slideUp();
|
||||
selector.find('.remove-file').unbind().addClass('upload-button').removeClass('remove-file').val(optionsframework_l10n.upload);
|
||||
// We don't display the upload button if .upload-notice is present
|
||||
// This means the user doesn't have the WordPress 3.5 Media Library Support
|
||||
if ( $('.section-upload .upload-notice').length > 0 ) {
|
||||
$('.upload-button').remove();
|
||||
}
|
||||
selector.find('.upload-button').on('click', function(event) {
|
||||
optionsframework_add_file(event, $(this).parents('.section'));
|
||||
});
|
||||
}
|
||||
|
||||
$('.remove-image, .remove-file').on('click', function() {
|
||||
optionsframework_remove_file( $(this).parents('.section') );
|
||||
});
|
||||
|
||||
$('.upload-button').click( function( event ) {
|
||||
optionsframework_add_file(event, $(this).parents('.section'));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Custom scripts needed for the colorpicker, image button selectors,
|
||||
* and navigation tabs.
|
||||
*/
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
|
||||
$('input[id^="s_"]').click(function () {
|
||||
jQuery('#section-' + this.id + '_url').fadeToggle(400)
|
||||
jQuery('#section-' + this.id + '_links').fadeToggle(400)
|
||||
})
|
||||
|
||||
for (i = 0; i < $('input[id^="s_"]' + ':checked').length; i++) {
|
||||
let id = $('input[id^="s_"]' + ':checked')[i].id
|
||||
$('#section-' + id + '_url').show()
|
||||
$('#section-' + id + '_links').show()
|
||||
}
|
||||
|
||||
jQuery('#g_cos').click(function () {
|
||||
jQuery('#section-g_cos_bucketname').fadeToggle(400)
|
||||
jQuery('#section-g_cos_url').fadeToggle(400)
|
||||
jQuery('#section-g_cos_accesskey').fadeToggle(400)
|
||||
jQuery('#section-g_cos_secretkey').fadeToggle(400)
|
||||
})
|
||||
|
||||
if (jQuery('#g_cos:checked').val() !== undefined) {
|
||||
jQuery('#section-g_cos_bucketname').show()
|
||||
jQuery('#section-g_cos_url').show()
|
||||
jQuery('#section-g_cos_accesskey').show()
|
||||
jQuery('#section-g_cos_secretkey').show()
|
||||
}
|
||||
|
||||
jQuery('#g_cc_switch').click(function () {
|
||||
jQuery('#section-g_cc').fadeToggle(400)
|
||||
})
|
||||
|
||||
if (jQuery('#g_cc_switch:checked').val() !== undefined) {
|
||||
jQuery('#section-g_cc').show()
|
||||
}
|
||||
|
||||
jQuery('#g_donate').click(function () {
|
||||
jQuery('#section-g_donate_alipay').fadeToggle(400)
|
||||
jQuery('#section-g_donate_wechat').fadeToggle(400)
|
||||
})
|
||||
|
||||
if (jQuery('#g_donate:checked').val() !== undefined) {
|
||||
jQuery('#section-g_donate_alipay').show()
|
||||
jQuery('#section-g_donate_wechat').show()
|
||||
}
|
||||
|
||||
jQuery('#m_smtp').click(function () {
|
||||
jQuery('#section-m_host').fadeToggle(400)
|
||||
jQuery('#section-m_sec').fadeToggle(400)
|
||||
jQuery('#section-m_port').fadeToggle(400)
|
||||
jQuery('#section-m_username').fadeToggle(400)
|
||||
jQuery('#section-m_passwd').fadeToggle(400)
|
||||
jQuery('#section-m_sendmail').fadeToggle(400)
|
||||
})
|
||||
|
||||
if (jQuery('#m_smtp:checked').val() !== undefined) {
|
||||
jQuery('#section-m_host').show()
|
||||
jQuery('#section-m_sec').show()
|
||||
jQuery('#section-m_port').show()
|
||||
jQuery('#section-m_username').show()
|
||||
jQuery('#section-m_passwd').show()
|
||||
jQuery('#section-m_sendmail').show()
|
||||
}
|
||||
|
||||
jQuery('#g_thumbnail').click(function () {
|
||||
jQuery('#section-g_postthumbnail').fadeToggle(400)
|
||||
})
|
||||
|
||||
if (jQuery('#g_thumbnail:checked').val() !== undefined) {
|
||||
jQuery('#section-g_postthumbnail').show()
|
||||
}
|
||||
|
||||
jQuery('#top_select').change(function () {
|
||||
if (jQuery("#top_select").val() == 'color') {
|
||||
jQuery('#section-top_color').fadeIn(400)
|
||||
jQuery('#section-top_img').fadeOut(400)
|
||||
jQuery('#section-top_title').fadeOut(400)
|
||||
jQuery('#section-top_describe').fadeOut(400)
|
||||
} else {
|
||||
jQuery('#section-top_color').fadeOut(400)
|
||||
jQuery('#section-top_img').fadeIn(400)
|
||||
jQuery('#section-top_title').fadeIn(400)
|
||||
jQuery('#section-top_describe').fadeIn(400)
|
||||
}
|
||||
})
|
||||
|
||||
if (jQuery('#top_select').val() == 'color') {
|
||||
jQuery('#section-top_color').show()
|
||||
jQuery('#section-top_img').hide()
|
||||
jQuery('#section-top_title').hide()
|
||||
jQuery('#section-top_describe').hide()
|
||||
} else {
|
||||
jQuery('#section-top_color').hide()
|
||||
jQuery('#section-top_img').show()
|
||||
jQuery('#section-top_title').show()
|
||||
jQuery('#section-top_describe').show()
|
||||
}
|
||||
|
||||
// Loads the color pickers
|
||||
$('.of-color').wpColorPicker()
|
||||
|
||||
// Image Options
|
||||
$('.of-radio-img-img').click(function () {
|
||||
$(this).parent().parent().find('.of-radio-img-img').removeClass('of-radio-img-selected')
|
||||
$(this).addClass('of-radio-img-selected')
|
||||
})
|
||||
|
||||
$('.of-radio-img-label').hide()
|
||||
$('.of-radio-img-img').show()
|
||||
$('.of-radio-img-radio').hide()
|
||||
|
||||
// Loads tabbed sections if they exist
|
||||
if ($('.nav-tab-wrapper').length > 0) {
|
||||
options_framework_tabs()
|
||||
}
|
||||
|
||||
function options_framework_tabs () {
|
||||
|
||||
var $group = $('.group'),
|
||||
$navtabs = $('.nav-tab-wrapper a'),
|
||||
active_tab = ''
|
||||
|
||||
// Hides all the .group sections to start
|
||||
$group.hide()
|
||||
|
||||
// Find if a selected tab is saved in localStorage
|
||||
if (typeof (localStorage) != 'undefined') {
|
||||
active_tab = localStorage.getItem('active_tab')
|
||||
}
|
||||
|
||||
// If active tab is saved and exists, load it's .group
|
||||
if (active_tab != '' && $(active_tab).length) {
|
||||
$(active_tab).fadeIn()
|
||||
$(active_tab + '-tab').addClass('nav-tab-active')
|
||||
} else {
|
||||
$('.group:first').fadeIn()
|
||||
$('.nav-tab-wrapper a:first').addClass('nav-tab-active')
|
||||
}
|
||||
|
||||
// Bind tabs clicks
|
||||
$navtabs.click(function (e) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
// Remove active class from all tabs
|
||||
$navtabs.removeClass('nav-tab-active')
|
||||
|
||||
$(this).addClass('nav-tab-active').blur()
|
||||
|
||||
if (typeof (localStorage) != 'undefined') {
|
||||
localStorage.setItem('active_tab', $(this).attr('href'))
|
||||
}
|
||||
|
||||
var selected = $(this).attr('href')
|
||||
|
||||
$group.hide()
|
||||
$(selected).fadeIn()
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
/**
|
||||
* 文章相关函数
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.06.13
|
||||
*/
|
||||
|
||||
// 文章链接添加 target 和 rel
|
||||
function imgnofollow($content)
|
||||
{
|
||||
$regexp = "<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>";
|
||||
if (preg_match_all("/$regexp/siU", $content, $matches, PREG_SET_ORDER)) {
|
||||
if (!empty($matches)) {
|
||||
$srcUrl = get_option('siteurl');
|
||||
for ($i = 0; $i < count($matches); $i++) {
|
||||
$tag = $matches[$i][0];
|
||||
$tag2 = $matches[$i][0];
|
||||
$url = $matches[$i][0];
|
||||
$noFollow = '';
|
||||
$pattern = '/target\s*=\s*"\s*_blank\s*"/';
|
||||
preg_match($pattern, $tag2, $match, PREG_OFFSET_CAPTURE);
|
||||
if (count($match) < 1) {
|
||||
$noFollow .= ' target="_blank" ';
|
||||
}
|
||||
|
||||
$pattern = '/rel\s*=\s*"\s*[n|d]ofollow\s*"/';
|
||||
preg_match($pattern, $tag2, $match, PREG_OFFSET_CAPTURE);
|
||||
if (count($match) < 1) {
|
||||
$noFollow .= ' rel="nofollow" ';
|
||||
}
|
||||
|
||||
$pos = strpos($url, $srcUrl);
|
||||
if ($pos === false) {
|
||||
$tag = rtrim($tag, '>');
|
||||
$tag .= $noFollow . '>';
|
||||
$content = str_replace($tag2, $tag, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$content = str_replace(']]>', ']]>', $content);
|
||||
return $content;
|
||||
}
|
||||
add_filter('the_content', 'imgnofollow');
|
||||
|
||||
// 文章点赞
|
||||
function love()
|
||||
{
|
||||
global $wpdb, $post;
|
||||
$id = $_POST["um_id"];
|
||||
$action = $_POST["um_action"];
|
||||
if ($action == 'love') {
|
||||
$raters = get_post_meta($id, 'love', true);
|
||||
$expire = time() + 99999999;
|
||||
$domain = ($_SERVER['HTTP_HOST'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false;
|
||||
setcookie('love_' . $id, $id, $expire, '/', $domain, false);
|
||||
if (!$raters || !is_numeric($raters)) {
|
||||
update_post_meta($id, 'love', 1);
|
||||
} else {
|
||||
update_post_meta($id, 'love', ($raters + 1));
|
||||
}
|
||||
echo get_post_meta($id, 'love', true);
|
||||
}
|
||||
die;
|
||||
}
|
||||
add_action('wp_ajax_nopriv_love', 'love');
|
||||
add_action('wp_ajax_love', 'love');
|
||||
|
||||
// 文章阅读次数统计
|
||||
function set_post_views()
|
||||
{
|
||||
if (is_singular()) {
|
||||
global $post;
|
||||
$post_ID = $post->ID;
|
||||
if ($post_ID) {
|
||||
$post_views = (int) get_post_meta($post_ID, 'views', true);
|
||||
if (!update_post_meta($post_ID, 'views', ($post_views + 1))) {
|
||||
add_post_meta($post_ID, 'views', 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('wp_head', 'set_post_views');
|
||||
|
||||
function get_post_views($echo = 1)
|
||||
{
|
||||
global $post;
|
||||
$post_ID = $post->ID;
|
||||
$views = (int) get_post_meta($post_ID, 'views', true);
|
||||
return $views;
|
||||
}
|
||||
|
||||
// 文章列表简介内容
|
||||
function excerpt_length($length)
|
||||
{
|
||||
return 260;
|
||||
}
|
||||
add_filter('excerpt_length', 'excerpt_length');
|
||||
|
||||
// 开启特色图
|
||||
add_theme_support("post-thumbnails");
|
||||
|
||||
// 文章特色图片
|
||||
function post_thumbnail()
|
||||
{
|
||||
global $post;
|
||||
$img_id = get_post_thumbnail_id();
|
||||
$img_url = wp_get_attachment_image_src($img_id, array(720, 435));
|
||||
if (is_array($img_url)) {
|
||||
$img_url = $img_url[0];
|
||||
}
|
||||
if (has_post_thumbnail()) {
|
||||
echo '<img src="' . $img_url . '" />';
|
||||
} else {
|
||||
$content = $post->post_content;
|
||||
$img_preg = "/<img (.*?)src=\"(.+?)\".*?>/";
|
||||
preg_match($img_preg, $content, $img_src);
|
||||
$img_count = count($img_src) - 1;
|
||||
if (isset($img_src[$img_count])) {
|
||||
$img_val = $img_src[$img_count];
|
||||
}
|
||||
if (!empty($img_val)) {
|
||||
echo '<img src="' . $img_val . '" />';
|
||||
} else {
|
||||
if (!kratos_option('g_postthumbnail')) {
|
||||
$img = ASSET_PATH . '/assets/img/default.jpg';
|
||||
} else {
|
||||
$img = kratos_option('g_postthumbnail', ASSET_PATH . '/assets/img/default.jpg');
|
||||
}
|
||||
echo '<img src="' . $img . '" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文章列表分页
|
||||
function pagelist($range = 5)
|
||||
{
|
||||
global $paged, $wp_query, $max_page;
|
||||
if (!$max_page) {$max_page = $wp_query->max_num_pages;}
|
||||
if ($max_page > 1) {if (!$paged) {$paged = 1;}
|
||||
echo "<div class='paginations'>";
|
||||
if ($paged > 1) {
|
||||
echo '<a href="' . get_pagenum_link($paged - 1) . '" class="prev" title="上一页"><i class="kicon i-larrows"></i></a>';
|
||||
}
|
||||
if ($max_page > $range) {
|
||||
if ($paged < $range) {
|
||||
for ($i = 1; $i <= $range; $i++) {
|
||||
if ($i == $paged) {
|
||||
echo '<span class="page-numbers current">' . $i . '</span>';
|
||||
} else {
|
||||
echo "<a href='" . get_pagenum_link($i) . "'>$i</a>";
|
||||
}
|
||||
}
|
||||
echo '<span class="page-numbers dots">…</span>';
|
||||
echo "<a href='" . get_pagenum_link($max_page) . "'>$max_page</a>";
|
||||
} elseif ($paged >= ($max_page - ceil(($range / 2)))) {
|
||||
if ($paged != 1) {
|
||||
echo "<a href='" . get_pagenum_link(1) . "' class='extend' title='首页'>1</a>";
|
||||
echo '<span class="page-numbers dots">…</span>';
|
||||
}
|
||||
for ($i = $max_page - $range + 1; $i <= $max_page; $i++) {
|
||||
if ($i == $paged) {
|
||||
echo '<span class="page-numbers current">' . $i . '</span>';
|
||||
} else {
|
||||
echo "<a href='" . get_pagenum_link($i) . "'>$i</a>";
|
||||
}
|
||||
}
|
||||
} elseif ($paged >= $range && $paged < ($max_page - ceil(($range / 2)))) {
|
||||
if ($paged != 1) {
|
||||
echo "<a href='" . get_pagenum_link(1) . "' class='extend' title='首页'>1</a>";
|
||||
echo '<span class="page-numbers dots">…</span>';
|
||||
}
|
||||
for ($i = ($paged - ceil($range / 3)); $i <= ($paged + ceil(($range / 3))); $i++) {
|
||||
if ($i == $paged) {
|
||||
echo '<span class="page-numbers current">' . $i . '</span>';
|
||||
} else {
|
||||
echo "<a href='" . get_pagenum_link($i) . "'>$i</a>";
|
||||
}
|
||||
}
|
||||
echo '<span class="page-numbers dots">…</span>';
|
||||
echo "<a href='" . get_pagenum_link($max_page) . "'>$max_page</a>";
|
||||
}
|
||||
} else {
|
||||
for ($i = 1; $i <= $max_page; $i++) {
|
||||
if ($i == $paged) {
|
||||
echo '<span class="page-numbers current">' . $i . '</span>';
|
||||
} else {
|
||||
echo "<a href='" . get_pagenum_link($i) . "'>$i</a>";
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($paged < $max_page) {
|
||||
echo '<a href="' . get_pagenum_link($paged + 1) . '" class="next" title="下一页"><i class="kicon i-rarrows"></i></a>';
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
// 文章评论
|
||||
function comment_scripts()
|
||||
{
|
||||
wp_enqueue_script('comment', ASSET_PATH . '/assets/js/comments.min.js', array(), THEME_VERSION);
|
||||
wp_localize_script('comment', 'ajaxcomment', array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'order' => get_option('comment_order')
|
||||
));
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'comment_scripts');
|
||||
|
||||
function comment_err($a)
|
||||
{
|
||||
header('HTTP/1.0 500 Internal Server Error');
|
||||
header('Content-Type: text/plain;charset=UTF-8');
|
||||
echo $a;
|
||||
exit;
|
||||
}
|
||||
|
||||
function comment_callback()
|
||||
{
|
||||
$comment = wp_handle_comment_submission(wp_unslash($_POST));
|
||||
if (is_wp_error($comment)) {
|
||||
$data = $comment->get_error_data();
|
||||
if (!empty($data)) {
|
||||
comment_err($comment->get_error_message());
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$user = wp_get_current_user();
|
||||
do_action('set_comment_cookies', $comment, $user);
|
||||
$GLOBALS['comment'] = $comment;
|
||||
?>
|
||||
<li class="comment cleanfix" id="comment-<?php echo esc_attr(comment_ID()); ?>">
|
||||
<div class="avatar float-left d-inline-block mr-2">
|
||||
<?php if (function_exists('get_avatar') && get_option('show_avatars')) {echo get_avatar($comment, 50);}?>
|
||||
</div>
|
||||
<div class="info clearfix">
|
||||
<cite class="author_name"><?php echo get_comment_author_link();?></cite>
|
||||
<div class="content pb-2">
|
||||
<?php comment_text();?>
|
||||
</div>
|
||||
<div class="meta clearfix">
|
||||
<div class="date d-inline-block float-left"><?php echo get_comment_date('Y年m月d日'); ?><?php if (current_user_can('edit_posts')) {echo '<span class="ml-2">';edit_comment_link(__('编辑', 'kratos'));echo '</span>';};?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php die();
|
||||
}
|
||||
|
||||
add_action('wp_ajax_nopriv_ajax_comment', 'comment_callback');
|
||||
add_action('wp_ajax_ajax_comment', 'comment_callback');
|
||||
|
||||
function comment_post($incoming_comment)
|
||||
{
|
||||
$incoming_comment['comment_content'] = htmlspecialchars($incoming_comment['comment_content']);
|
||||
$incoming_comment['comment_content'] = str_replace("'", ''', $incoming_comment['comment_content']);
|
||||
return ($incoming_comment);
|
||||
}
|
||||
add_filter('preprocess_comment', 'comment_post', '', 1);
|
||||
|
||||
function comment_display($comment_to_display)
|
||||
{
|
||||
$comment_to_display = str_replace(''', "'", $comment_to_display);
|
||||
return $comment_to_display;
|
||||
}
|
||||
add_filter('comment_text', 'comment_display', '', 1);
|
||||
|
||||
function comment_callbacks($comment, $args, $depth = 2)
|
||||
{
|
||||
$GLOBALS['comment'] = $comment;?>
|
||||
<li class="comment cleanfix" id="comment-<?php echo esc_attr(comment_ID()); ?>">
|
||||
<div class="avatar float-left d-inline-block mr-2">
|
||||
<?php if (function_exists('get_avatar') && get_option('show_avatars')) {echo get_avatar($comment, 50);}?>
|
||||
</div>
|
||||
<div class="info clearfix">
|
||||
<cite class="author_name"><?php echo get_comment_author_link();?></cite>
|
||||
<div class="content pb-2">
|
||||
<?php comment_text();?>
|
||||
</div>
|
||||
<div class="meta clearfix">
|
||||
<div class="date d-inline-block float-left"><?php echo get_comment_date('Y年m月d日'); ?><?php if (current_user_can('edit_posts')) {echo '<span class="ml-2">';edit_comment_link(__('编辑', 'kratos'));echo '</span>';};?>
|
||||
</div>
|
||||
<div class="tool reply ml-2 d-inline-block float-right">
|
||||
<?php
|
||||
$defaults = array('add_below' => 'comment', 'respond_id' => 'respond', 'reply_text' => '<i class="kicon i-reply"></i><span class="ml-1">' . __('回复', 'kratos') . '</span>');
|
||||
comment_reply_link(array_merge($defaults, array('depth' => $depth, 'max_depth' => $args['max_depth'])));
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// 文章评论表情
|
||||
function custom_smilies_src($img_src, $img, $siteurl)
|
||||
{
|
||||
return ASSET_PATH . '/assets/img/smilies/' . $img;
|
||||
}
|
||||
add_filter('smilies_src', 'custom_smilies_src', 1, 10);
|
||||
|
||||
function disable_emojis_tinymce($plugins)
|
||||
{
|
||||
return array_diff($plugins, array('wpemoji'));
|
||||
}
|
||||
function smilies_reset()
|
||||
{
|
||||
global $wpsmiliestrans, $wp_smiliessearch, $wp_version;
|
||||
if (!get_option('use_smilies') || $wp_version < 4.2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wpsmiliestrans = array(
|
||||
':mrgreen:' => 'mrgreen.png',
|
||||
':exclaim:' => 'exclaim.png',
|
||||
':neutral:' => 'neutral.png',
|
||||
':twisted:' => 'twisted.png',
|
||||
':arrow:' => 'arrow.png',
|
||||
':eek:' => 'eek.png',
|
||||
':smile:' => 'smile.png',
|
||||
':confused:' => 'confused.png',
|
||||
':cool:' => 'cool.png',
|
||||
':evil:' => 'evil.png',
|
||||
':biggrin:' => 'biggrin.png',
|
||||
':idea:' => 'idea.png',
|
||||
':redface:' => 'redface.png',
|
||||
':razz:' => 'razz.png',
|
||||
':rolleyes:' => 'rolleyes.png',
|
||||
':wink:' => 'wink.png',
|
||||
':cry:' => 'cry.png',
|
||||
':lol:' => 'lol.png',
|
||||
':mad:' => 'mad.png',
|
||||
':drooling:' => 'drooling.png',
|
||||
':persevering:' => 'persevering.png',
|
||||
);
|
||||
}
|
||||
smilies_reset();
|
||||
|
||||
function smilies_custom_button()
|
||||
{
|
||||
printf('<style>.smilies-wrap{background:#fff;border: 1px solid #ccc;box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.24);padding: 10px;position: absolute;top: 60px;width: 400px;display:none}.smilies-wrap img{height:24px;width:24px;cursor:pointer;margin-bottom:5px} .is-active.smilies-wrap{display:block}@media screen and (max-width: 782px){ #wp-content-media-buttons a { font-size: 14px; padding: 0 14px; }}</style><a id="insert-media-button" style="position:relative" class="button insert-smilies add_smilies" data-editor="content" href="javascript:;"><span class="dashicons dashicons-smiley" style="line-height: 26px;"></span>' . __('添加表情', 'kratos') . '</a><div class="smilies-wrap">' . get_wpsmiliestrans() . '</div><script>jQuery(document).ready(function(){jQuery(document).on("click", ".insert-smilies",function() { if(jQuery(".smilies-wrap").hasClass("is-active")){jQuery(".smilies-wrap").removeClass("is-active");}else{jQuery(".smilies-wrap").addClass("is-active");}});jQuery(document).on("click", ".add-smily",function() { send_to_editor(" " + jQuery(this).data("smilies") + " ");jQuery(".smilies-wrap").removeClass("is-active");return false;});});</script>');
|
||||
}
|
||||
add_action('media_buttons', 'smilies_custom_button');
|
||||
|
||||
function get_wpsmiliestrans()
|
||||
{
|
||||
global $wpsmiliestrans;
|
||||
global $output;
|
||||
$wpsmilies = array_unique($wpsmiliestrans);
|
||||
foreach ($wpsmilies as $alt => $src_path) {
|
||||
$output .= '<a class="add-smily" data-smilies="' . $alt . '"><img class="wp-smiley" src="' . ASSET_PATH . '/assets/img/smilies/' . rtrim($src_path, "png") . 'png" /></a>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
if (!kratos_option('g_gutenberg',false)) {
|
||||
// 禁用 Gutenberg 编辑器
|
||||
add_filter('use_block_editor_for_post', '__return_false');
|
||||
remove_action('wp_enqueue_scripts', 'wp_common_block_scripts_and_styles');
|
||||
|
||||
// 删除前端的block library的css资源,
|
||||
add_action('wp_enqueue_scripts', 'remove_block_library_css', 100);
|
||||
function remove_block_library_css()
|
||||
{
|
||||
wp_dequeue_style('wp-block-library');
|
||||
}
|
||||
}
|
||||
|
||||
// 文章评论增强
|
||||
function comment_add_at($comment_text, $comment = '') {
|
||||
if( $comment->comment_parent > 0) {
|
||||
$comment_text = '<span>@' . get_comment_author( $comment->comment_parent ) . '</span> ' . $comment_text;
|
||||
}
|
||||
|
||||
return $comment_text;
|
||||
}
|
||||
add_filter('comment_text' , 'comment_add_at', 20, 2);
|
||||
|
||||
function recover_comment_fields($comment_fields){
|
||||
$comment = array_shift($comment_fields);
|
||||
$comment_fields = array_merge($comment_fields ,array('comment' => $comment));
|
||||
return $comment_fields;
|
||||
}
|
||||
add_filter('comment_form_fields','recover_comment_fields');
|
||||
|
||||
$new_meta_boxes =
|
||||
array(
|
||||
"description" => array(
|
||||
"name" => "seo_description",
|
||||
"std" => "",
|
||||
"title" => __( '描述', 'kratos' )
|
||||
),
|
||||
"keywords" => array(
|
||||
"name" => "seo_keywords",
|
||||
"std" => "",
|
||||
"title" => __( '关键词', 'kratos' )
|
||||
)
|
||||
);
|
||||
|
||||
function seo_meta_boxes() {
|
||||
$post_types = get_post_types();
|
||||
add_meta_box( 'meta-box-id', __( 'SEO 设置', 'kratos' ), 'post_seo_callback', $post_types );
|
||||
}
|
||||
add_action( 'add_meta_boxes', 'seo_meta_boxes' );
|
||||
|
||||
function post_seo_callback( $post ) {
|
||||
global $new_meta_boxes;
|
||||
|
||||
foreach($new_meta_boxes as $meta_box) {
|
||||
$meta_box_value = get_post_meta($post->ID, $meta_box['name'].'_value', true);
|
||||
|
||||
if($meta_box_value == "")
|
||||
$meta_box_value = $meta_box['std'];
|
||||
|
||||
echo '<h3 style="font-size: 14px; padding: 9px 0; margin: 0; line-height: 1.4;">'.$meta_box['title'].'</h3>';
|
||||
echo '<textarea cols="60" rows="3" style="width:100%" name="'.$meta_box['name'].'_value">'.$meta_box_value.'</textarea><br/>';
|
||||
}
|
||||
|
||||
echo '<input type="hidden" name="metaboxes_nonce" id="metaboxes_nonce" value="'.wp_create_nonce( plugin_basename(__FILE__) ).'" />';
|
||||
}
|
||||
|
||||
function wpdocs_save_meta_box( $post_id ) {
|
||||
global $new_meta_boxes;
|
||||
|
||||
if ( !wp_verify_nonce( $_POST['metaboxes_nonce'], plugin_basename(__FILE__) ))
|
||||
return;
|
||||
|
||||
if ( !current_user_can( 'edit_posts', $post_id ))
|
||||
return;
|
||||
|
||||
foreach($new_meta_boxes as $meta_box) {
|
||||
$data = $_POST[$meta_box['name'].'_value'];
|
||||
|
||||
if($data == "")
|
||||
delete_post_meta($post_id, $meta_box['name'].'_value', get_post_meta($post_id, $meta_box['name'].'_value', true));
|
||||
else
|
||||
update_post_meta($post_id, $meta_box['name'].'_value', $data);
|
||||
}
|
||||
}
|
||||
add_action( 'save_post', 'wpdocs_save_meta_box' );
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* 核心函数
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.08.04
|
||||
*/
|
||||
|
||||
if (kratos_option('g_cdn', false)) {
|
||||
$asset_path = 'https://cdn.jsdelivr.net/gh/vtrois/kratos@' . THEME_VERSION;
|
||||
} else {
|
||||
$asset_path = get_template_directory_uri();
|
||||
}
|
||||
define('ASSET_PATH', $asset_path);
|
||||
|
||||
// 自动跳转主题设置
|
||||
function init_theme()
|
||||
{
|
||||
global $pagenow;
|
||||
if ('themes.php' == $pagenow && isset($_GET['activated'])) {
|
||||
wp_redirect(admin_url('admin.php?page=kratos_options'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
add_action('load-themes.php', 'init_theme');
|
||||
|
||||
// 语言国际化
|
||||
function theme_languages()
|
||||
{
|
||||
load_theme_textdomain('kratos', get_template_directory() . '/languages');
|
||||
}
|
||||
add_action('after_setup_theme', 'theme_languages');
|
||||
|
||||
// 资源加载
|
||||
function theme_autoload()
|
||||
{
|
||||
if (!is_admin()) {
|
||||
// css
|
||||
wp_enqueue_style('bootstrap', ASSET_PATH . '/assets/css/bootstrap.min.css', array(), '4.5.0');
|
||||
wp_enqueue_style('kicon', ASSET_PATH . '/assets/css/iconfont.min.css', array(), THEME_VERSION);
|
||||
wp_enqueue_style('layer', ASSET_PATH . '/assets/css/layer.min.css', array(), '3.1.1');
|
||||
if (kratos_option('g_animate', false)) {
|
||||
wp_enqueue_style('animate', ASSET_PATH . '/assets/css/animate.min.css', array(), '3.7.2');
|
||||
}
|
||||
if (kratos_option('g_fontawesome', false)) {
|
||||
wp_enqueue_style('fontawesome', ASSET_PATH . '/assets/css/fontawesome.min.css', array(), '5.13.0');
|
||||
}
|
||||
wp_enqueue_style('kratos', ASSET_PATH . '/assets/css/kratos.min.css', array(), THEME_VERSION);
|
||||
if (kratos_option('g_adminbar', true)) {
|
||||
$admin_bar_css = "
|
||||
@media screen and (max-width: 782px) {
|
||||
.k-nav{
|
||||
padding-top: 46px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 782px) {
|
||||
.k-nav{
|
||||
padding-top: 32px;
|
||||
}
|
||||
}
|
||||
";
|
||||
if (current_user_can('level_10')) {
|
||||
wp_add_inline_style('kratos', $admin_bar_css);
|
||||
}
|
||||
}
|
||||
wp_enqueue_style('custom', get_template_directory_uri() . '/custom/custom.css', array(), THEME_VERSION);
|
||||
// js
|
||||
wp_deregister_script('jquery');
|
||||
wp_enqueue_script('jquery', ASSET_PATH . '/assets/js/jquery.min.js', array(), '3.4.1', false);
|
||||
wp_enqueue_script('bootstrap-bundle', ASSET_PATH . '/assets/js/bootstrap.bundle.min.js', array(), '4.5.0', true);
|
||||
wp_enqueue_script('layer', ASSET_PATH . '/assets/js/layer.min.js', array(), '3.1.1', true);
|
||||
wp_enqueue_script('kratos', ASSET_PATH . '/assets/js/kratos.min.js', array(), THEME_VERSION, true);
|
||||
wp_enqueue_script('custom', get_template_directory_uri() . '/custom/custom.js', array(), THEME_VERSION, true);
|
||||
|
||||
$data = array(
|
||||
'site' => home_url(),
|
||||
'directory' => get_stylesheet_directory_uri(),
|
||||
'alipay' => kratos_option('g_donate_alipay', ASSET_PATH . '/assets/img/donate.png'),
|
||||
'wechat' => kratos_option('g_donate_wechat', ASSET_PATH . '/assets/img/donate.png'),
|
||||
'repeat' => __('您已经赞过了', 'kratos'),
|
||||
'thanks' => __('感谢您的支持', 'kratos'),
|
||||
'donate' => __('打赏作者', 'kratos'),
|
||||
'scan' => __('扫码支付', 'kratos'),
|
||||
);
|
||||
wp_localize_script('kratos', 'kratos', $data);
|
||||
}
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'theme_autoload');
|
||||
|
||||
// Admin Bar
|
||||
if (! kratos_option('g_adminbar', true)) {
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
}
|
||||
|
||||
// 移除自动保存、修订版本
|
||||
remove_action('post_updated', 'wp_save_post_revision');
|
||||
|
||||
// 添加友情链接
|
||||
add_filter('pre_option_link_manager_enabled', '__return_true');
|
||||
|
||||
// 禁用转义
|
||||
$qmr_work_tags = array('the_title', 'the_excerpt', 'single_post_title', 'comment_author', 'comment_text', 'link_description', 'bloginfo', 'wp_title', 'term_description', 'category_description', 'widget_title', 'widget_text');
|
||||
|
||||
foreach ($qmr_work_tags as $qmr_work_tag) {
|
||||
remove_filter($qmr_work_tag, 'wptexturize');
|
||||
}
|
||||
|
||||
remove_filter('the_content', 'wptexturize');
|
||||
add_filter('run_wptexturize', '__return_false');
|
||||
|
||||
// 禁用 Emoji
|
||||
add_filter('emoji_svg_url', '__return_false');
|
||||
remove_action('admin_print_scripts', 'print_emoji_detection_script');
|
||||
remove_action('admin_print_styles', 'print_emoji_styles');
|
||||
remove_filter('the_content', 'wptexturize');
|
||||
remove_filter('comment_text', 'wptexturize');
|
||||
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
||||
remove_action('wp_print_styles', 'print_emoji_styles');
|
||||
remove_action('embed_head', 'print_emoji_detection_script');
|
||||
remove_filter('the_content_feed', 'wp_staticize_emoji');
|
||||
remove_filter('comment_text_rss', 'wp_staticize_emoji');
|
||||
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
|
||||
|
||||
// 禁用 Trackbacks
|
||||
add_filter('xmlrpc_methods', function ($methods) {
|
||||
$methods['pingback.ping'] = '__return_false';
|
||||
$methods['pingback.extensions.getPingbacks'] = '__return_false';
|
||||
return $methods;
|
||||
});
|
||||
remove_action('do_pings', 'do_all_pings', 10);
|
||||
remove_action('publish_post', '_publish_post_hook', 5);
|
||||
|
||||
// 优化 wp_head() 内容
|
||||
foreach (array('rss2_head', 'commentsrss2_head', 'rss_head', 'rdf_header', 'atom_head', 'comments_atom_head', 'opml_head', 'app_head') as $action) {
|
||||
remove_action($action, 'the_generator');
|
||||
}
|
||||
remove_action('wp_head', 'wp_print_head_scripts', 9);
|
||||
remove_action('wp_head', 'rel_canonical');
|
||||
remove_action('wp_head', 'wp_generator');
|
||||
remove_action('wp_head', 'rsd_link');
|
||||
remove_action('wp_head', 'wlwmanifest_link');
|
||||
remove_action('wp_head', 'feed_links_extra', 3);
|
||||
remove_action('wp_head', 'feed_links', 2);
|
||||
remove_action('wp_head', 'index_rel_link');
|
||||
remove_action('wp_head', 'parent_post_rel_link', 10);
|
||||
remove_action('wp_head', 'start_post_rel_link', 10);
|
||||
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10);
|
||||
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0);
|
||||
remove_action('wp_head', 'rest_output_link_wp_head', 10);
|
||||
remove_action('template_redirect', 'wp_shortlink_header', 11);
|
||||
remove_action('template_redirect', 'rest_output_link_header', 11);
|
||||
|
||||
// 禁用 WordPress 拼写修正
|
||||
remove_filter('the_title', 'capital_P_dangit', 11);
|
||||
remove_filter('the_content', 'capital_P_dangit', 11);
|
||||
remove_filter('comment_text', 'capital_P_dangit', 31);
|
||||
|
||||
// 禁用后台 Google Fonts
|
||||
add_filter('style_loader_src', function ($href) {
|
||||
if (strpos($href, "fonts.googleapis.com") === false) {
|
||||
return $href;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 禁用 Auto Embeds
|
||||
remove_filter('the_content', array($GLOBALS['wp_embed'], 'autoembed'), 8);
|
||||
|
||||
// 替换国内 Gravatar 源
|
||||
function get_https_avatar($avatar)
|
||||
{
|
||||
if (kratos_option('g_gravatar', false)) {
|
||||
$cdn = "gravatar.loli.net";
|
||||
} else {
|
||||
$cdn = "cn.gravatar.com";
|
||||
}
|
||||
|
||||
$avatar = str_replace(array("www.gravatar.com", "0.gravatar.com", "1.gravatar.com", "2.gravatar.com", "3.gravatar.com", "secure.gravatar.com"), $cdn, $avatar);
|
||||
$avatar = str_replace("http://", "https://", $avatar);
|
||||
return $avatar;
|
||||
}
|
||||
add_filter('get_avatar', 'get_https_avatar');
|
||||
|
||||
// 主题更新检测
|
||||
$myUpdateChecker = Puc_v4_Factory::buildUpdateChecker(
|
||||
'https://theme.yangyq.net/update.json',
|
||||
get_template_directory() . '/functions.php',
|
||||
'Kratos_Yang'
|
||||
);
|
||||
|
||||
// 禁止生成多种尺寸图片
|
||||
if (kratos_option('g_removeimgsize', false)) {
|
||||
function remove_default_images($sizes)
|
||||
{
|
||||
unset($sizes['thumbnail']);
|
||||
unset($sizes['medium']);
|
||||
unset($sizes['large']);
|
||||
unset($sizes['medium_large']);
|
||||
return $sizes;
|
||||
}
|
||||
add_filter('intermediate_image_sizes_advanced', 'remove_default_images');
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* dogecloud 对象存储
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.03.17
|
||||
*/
|
||||
|
||||
if (kratos_option('g_cos', false)) {
|
||||
function dogcloud_upload($object, $file, $mime){
|
||||
if (!@file_exists($file)) {
|
||||
return false;
|
||||
}
|
||||
if (@file_exists($file)) {
|
||||
$accessKey = kratos_option('g_cos_accesskey');
|
||||
$secretKey = kratos_option('g_cos_secretkey');
|
||||
$bucket = kratos_option('g_cos_bucketname');
|
||||
|
||||
$filesize = fileSize($file);
|
||||
$file = fopen($file, 'rb');
|
||||
|
||||
$signStr = "/oss/upload/put.json?bucket=$bucket&key=$object" . "\n" . "";
|
||||
$sign = hash_hmac('sha1', $signStr, $secretKey);
|
||||
$authorization = "TOKEN " . $accessKey . ":" . $sign;
|
||||
|
||||
$url = "https://api.dogecloud.com/oss/upload/put.json?bucket=$bucket&key=$object";
|
||||
$headers = array("Host: api.dogecloud.com", "Content-Type: $mime", "Authorization: $authorization");
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_PUT => true,
|
||||
CURLOPT_TIMEOUT => 0,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => "PUT",
|
||||
CURLOPT_INFILE => $file,
|
||||
CURLOPT_INFILESIZE => $filesize,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
));
|
||||
$response = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传附件
|
||||
function dogecloud_upload_attachments($metadata)
|
||||
{
|
||||
if (get_option('upload_path') == '.') {
|
||||
$metadata['file'] = str_replace("./", '', $metadata['file']);
|
||||
}
|
||||
|
||||
$object = str_replace("\\", '/', $metadata['file']);
|
||||
$object = str_replace(get_home_path(), '', $object);
|
||||
$file = get_home_path() . $object;
|
||||
$object = str_replace("wp-content/uploads/", '', $object);
|
||||
$mime = $metadata['type'];
|
||||
|
||||
dogcloud_upload('/' . $object, $file, $mime);
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (substr_count($_SERVER['REQUEST_URI'], '/update.php') <= 0) {
|
||||
add_filter('wp_handle_upload', 'dogecloud_upload_attachments', 50);
|
||||
}
|
||||
|
||||
// 上传缩略图
|
||||
function dogecloud_upload_thumbs($metadata)
|
||||
{
|
||||
if (isset($metadata['sizes']) && count($metadata['sizes']) > 0) {
|
||||
$wp_uploads = wp_upload_dir();
|
||||
$basedir = $wp_uploads['basedir'];
|
||||
$file_dir = $metadata['file'];
|
||||
$file_path = $basedir . '/' . dirname($file_dir) . '/';
|
||||
if (get_option('upload_path') == '.') {
|
||||
$file_path = str_replace("\\", '/', $file_path);
|
||||
$file_path = str_replace(get_home_path() . "./", '', $file_path);
|
||||
} else {
|
||||
$file_path = str_replace("\\", '/', $file_path);
|
||||
}
|
||||
$object_path = str_replace(get_home_path(), '', $file_path);
|
||||
foreach ($metadata['sizes'] as $val) {
|
||||
$object = '/' . $object_path . $val['file'];
|
||||
$object = str_replace("wp-content/uploads/", '', $object);
|
||||
$file = $file_path . $val['file'];
|
||||
$mime = $metadata['type'];
|
||||
|
||||
dogcloud_upload('/' . $object, $file, $mime);
|
||||
}
|
||||
}
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (substr_count($_SERVER['REQUEST_URI'], '/update.php') <= 0) {
|
||||
add_filter('wp_generate_attachment_metadata', 'dogecloud_upload_thumbs', 100);
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
function dogecloud_delete_remote_file($file)
|
||||
{
|
||||
$accessKey = kratos_option('g_cos_accesskey');
|
||||
$secretKey = kratos_option('g_cos_secretkey');
|
||||
$bucket = kratos_option('g_cos_bucketname');
|
||||
|
||||
$file = str_replace("\\", '/', $file);
|
||||
$file = str_replace(get_home_path(), '', $file);
|
||||
$del_file_path = str_replace("wp-content/uploads/", '', $file);
|
||||
$del_file_body = "[\"$del_file_path\"]";
|
||||
|
||||
$signStr = "/oss/file/delete.json?bucket=$bucket" . "\n" . $del_file_body;
|
||||
$sign = hash_hmac('sha1', $signStr, $secretKey);
|
||||
$authorization = "TOKEN " . $accessKey . ":" . $sign;
|
||||
|
||||
$url = "https://api.dogecloud.com/oss/file/delete.json?bucket=$bucket";
|
||||
$headers = array('Host' => 'api.dogecloud.com', 'Content-Type' => 'application/json', 'Authorization' => $authorization);
|
||||
|
||||
$request = new WP_Http;
|
||||
$result = $request->request($url, array('method' => 'POST', 'body' => $del_file_body, 'headers' => $headers));
|
||||
|
||||
return $file;
|
||||
}
|
||||
add_action('wp_delete_file', 'dogecloud_delete_remote_file', 100);
|
||||
|
||||
// 修改图片地址
|
||||
function custom_upload_dir($uploads)
|
||||
{
|
||||
$upload_path = '';
|
||||
$upload_url_path = kratos_option('g_cos_url');
|
||||
|
||||
if (empty($upload_path) || 'wp-content/uploads' == $upload_path) {
|
||||
$uploads['basedir'] = WP_CONTENT_DIR . '/uploads';
|
||||
} elseif (0 !== strpos($upload_path, ABSPATH)) {
|
||||
$uploads['basedir'] = path_join(ABSPATH, $upload_path);
|
||||
} else {
|
||||
$uploads['basedir'] = $upload_path;
|
||||
}
|
||||
|
||||
$uploads['path'] = $uploads['basedir'] . $uploads['subdir'];
|
||||
|
||||
if ($upload_url_path) {
|
||||
$uploads['baseurl'] = $upload_url_path;
|
||||
$uploads['url'] = $uploads['baseurl'] . $uploads['subdir'];
|
||||
}
|
||||
return $uploads;
|
||||
}
|
||||
add_filter('upload_dir', 'custom_upload_dir');
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Bootstrap Navwalker
|
||||
*
|
||||
* @package WP-Bootstrap-Navwalker
|
||||
*
|
||||
* @wordpress-plugin
|
||||
* Plugin Name: WP Bootstrap Navwalker
|
||||
* Plugin URI: https://github.com/wp-bootstrap/wp-bootstrap-navwalker
|
||||
* Description: A custom WordPress nav walker class to implement the Bootstrap 4 navigation style in a custom theme using the WordPress built in menu manager.
|
||||
* Author: Edward McIntyre - @twittem, WP Bootstrap, William Patton - @pattonwebz
|
||||
* Version: 4.3.0
|
||||
* Author URI: https://github.com/wp-bootstrap
|
||||
* GitHub Plugin URI: https://github.com/wp-bootstrap/wp-bootstrap-navwalker
|
||||
* GitHub Branch: master
|
||||
* License: GPL-3.0+
|
||||
* License URI: http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
*/
|
||||
|
||||
function register_navmenus()
|
||||
{
|
||||
register_nav_menus(array('header_menu' => __('顶部菜单', 'kratos')));
|
||||
}
|
||||
add_action('after_setup_theme', 'register_navmenus');
|
||||
|
||||
// 删除原导航标签的多余属性
|
||||
function attributes_filter($var)
|
||||
{
|
||||
return is_array($var) ? array_intersect($var, array()) : '';
|
||||
}
|
||||
add_filter('nav_menu_css_class', 'attributes_filter', 100, 1);
|
||||
add_filter('nav_menu_item_id', 'attributes_filter', 100, 1);
|
||||
add_filter('page_css_class', 'attributes_filter', 100, 1);
|
||||
|
||||
if (!class_exists('WP_Bootstrap_Navwalker')) {
|
||||
/**
|
||||
* WP_Bootstrap_Navwalker class.
|
||||
*
|
||||
* @extends Walker_Nav_Menu
|
||||
*/
|
||||
class WP_Bootstrap_Navwalker extends Walker_Nav_Menu
|
||||
{
|
||||
|
||||
/**
|
||||
* Starts the list before the elements are added.
|
||||
*
|
||||
* @since WP 3.0.0
|
||||
*
|
||||
* @see Walker_Nav_Menu::start_lvl()
|
||||
*
|
||||
* @param string $output Used to append additional content (passed by reference).
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
* @param stdClass $args An object of wp_nav_menu() arguments.
|
||||
*/
|
||||
public function start_lvl(&$output, $depth = 0, $args = array())
|
||||
{
|
||||
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
|
||||
$t = '';
|
||||
$n = '';
|
||||
} else {
|
||||
$t = "\t";
|
||||
$n = "\n";
|
||||
}
|
||||
$indent = str_repeat($t, $depth);
|
||||
// Default class to add to the file.
|
||||
$classes = array('dropdown-menu');
|
||||
/**
|
||||
* Filters the CSS class(es) applied to a menu list element.
|
||||
*
|
||||
* @since WP 4.8.0
|
||||
*
|
||||
* @param array $classes The CSS classes that are applied to the menu `<ul>` element.
|
||||
* @param stdClass $args An object of `wp_nav_menu()` arguments.
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
*/
|
||||
$class_names = join(' ', apply_filters('nav_menu_submenu_css_class', $classes, $args, $depth));
|
||||
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
|
||||
|
||||
/*
|
||||
* The `.dropdown-menu` container needs to have a labelledby
|
||||
* attribute which points to it's trigger link.
|
||||
*
|
||||
* Form a string for the labelledby attribute from the the latest
|
||||
* link with an id that was added to the $output.
|
||||
*/
|
||||
$labelledby = '';
|
||||
// Find all links with an id in the output.
|
||||
preg_match_all('/(<a.*?id=\"|\')(.*?)\"|\'.*?>/im', $output, $matches);
|
||||
// With pointer at end of array check if we got an ID match.
|
||||
if (end($matches[2])) {
|
||||
// Build a string to use as aria-labelledby.
|
||||
$labelledby = 'aria-labelledby="' . esc_attr(end($matches[2])) . '"';
|
||||
}
|
||||
$output .= "{$n}{$indent}<ul$class_names $labelledby role=\"menu\">{$n}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the element output.
|
||||
*
|
||||
* @since WP 3.0.0
|
||||
* @since WP 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
|
||||
*
|
||||
* @see Walker_Nav_Menu::start_el()
|
||||
*
|
||||
* @param string $output Used to append additional content (passed by reference).
|
||||
* @param WP_Post $item Menu item data object.
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
* @param stdClass $args An object of wp_nav_menu() arguments.
|
||||
* @param int $id Current item ID.
|
||||
*/
|
||||
public function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0)
|
||||
{
|
||||
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
|
||||
$t = '';
|
||||
$n = '';
|
||||
} else {
|
||||
$t = "\t";
|
||||
$n = "\n";
|
||||
}
|
||||
$indent = ($depth) ? str_repeat($t, $depth) : '';
|
||||
|
||||
$classes = empty($item->classes) ? array() : (array) $item->classes;
|
||||
|
||||
/*
|
||||
* Initialize some holder variables to store specially handled item
|
||||
* wrappers and icons.
|
||||
*/
|
||||
$linkmod_classes = array();
|
||||
$icon_classes = array();
|
||||
|
||||
/*
|
||||
* Get an updated $classes array without linkmod or icon classes.
|
||||
*
|
||||
* NOTE: linkmod and icon class arrays are passed by reference and
|
||||
* are maybe modified before being used later in this function.
|
||||
*/
|
||||
$classes = self::separate_linkmods_and_icons_from_classes($classes, $linkmod_classes, $icon_classes, $depth);
|
||||
|
||||
// Join any icon classes plucked from $classes into a string.
|
||||
$icon_class_string = join(' ', $icon_classes);
|
||||
|
||||
/**
|
||||
* Filters the arguments for a single nav menu item.
|
||||
*
|
||||
* WP 4.4.0
|
||||
*
|
||||
* @param stdClass $args An object of wp_nav_menu() arguments.
|
||||
* @param WP_Post $item Menu item data object.
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
*/
|
||||
$args = apply_filters('nav_menu_item_args', $args, $item, $depth);
|
||||
|
||||
// Add .dropdown or .active classes where they are needed.
|
||||
if (isset($args->has_children) && $args->has_children) {
|
||||
$classes[] = 'dropdown';
|
||||
}
|
||||
if (in_array('current-menu-item', $classes, true) || in_array('current-menu-parent', $classes, true)) {
|
||||
$classes[] = 'active';
|
||||
}
|
||||
|
||||
// Add some additional default classes to the item.
|
||||
$classes[] = 'menu-item-' . $item->ID;
|
||||
$classes[] = 'nav-item';
|
||||
|
||||
// Allow filtering the classes.
|
||||
$classes = apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth);
|
||||
|
||||
// Form a string of classes in format: class="class_names".
|
||||
$class_names = join(' ', $classes);
|
||||
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
|
||||
|
||||
/**
|
||||
* Filters the ID applied to a menu item's list item element.
|
||||
*
|
||||
* @since WP 3.0.1
|
||||
* @since WP 4.1.0 The `$depth` parameter was added.
|
||||
*
|
||||
* @param string $menu_id The ID that is applied to the menu item's `<li>` element.
|
||||
* @param WP_Post $item The current menu item.
|
||||
* @param stdClass $args An object of wp_nav_menu() arguments.
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
*/
|
||||
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
|
||||
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
|
||||
|
||||
if ($args->has_children) {
|
||||
$output .= $indent . '<li' . $id . ' class="nav-item dropdown" ' . '>';
|
||||
} else {
|
||||
$output .= $indent . '<li' . $id . ' class="nav-item" ' . '>';
|
||||
}
|
||||
|
||||
// Initialize array for holding the $atts for the link item.
|
||||
$atts = array();
|
||||
|
||||
/*
|
||||
* Set title from item to the $atts array - if title is empty then
|
||||
* default to item title.
|
||||
*/
|
||||
if (empty($item->attr_title)) {
|
||||
$atts['title'] = !empty($item->title) ? strip_tags($item->title) : '';
|
||||
} else {
|
||||
$atts['title'] = $item->attr_title;
|
||||
}
|
||||
|
||||
$atts['target'] = !empty($item->target) ? $item->target : '';
|
||||
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
|
||||
// If the item has children, add atts to the <a>.
|
||||
if (isset($args->has_children) && $args->has_children && 0 === $depth && $args->depth > 1) {
|
||||
$atts['href'] = '#';
|
||||
$atts['data-toggle'] = 'dropdown';
|
||||
$atts['aria-haspopup'] = 'true';
|
||||
$atts['aria-expanded'] = 'false';
|
||||
$atts['class'] = 'dropdown-toggle nav-link';
|
||||
$atts['id'] = 'menu-item-dropdown-' . $item->ID;
|
||||
} else {
|
||||
$atts['href'] = !empty($item->url) ? $item->url : '#';
|
||||
// For items in dropdowns use .dropdown-item instead of .nav-link.
|
||||
if ($depth > 0) {
|
||||
$atts['class'] = 'dropdown-item';
|
||||
} else {
|
||||
$atts['class'] = 'nav-link';
|
||||
}
|
||||
}
|
||||
|
||||
$atts['aria-current'] = $item->current ? 'page' : '';
|
||||
|
||||
// Update atts of this item based on any custom linkmod classes.
|
||||
$atts = self::update_atts_for_linkmod_type($atts, $linkmod_classes);
|
||||
// Allow filtering of the $atts array before using it.
|
||||
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
|
||||
|
||||
// Build a string of html containing all the atts for the item.
|
||||
$attributes = '';
|
||||
foreach ($atts as $attr => $value) {
|
||||
if (!empty($value)) {
|
||||
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
|
||||
$attributes .= ' ' . $attr . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
// Set a typeflag to easily test if this is a linkmod or not.
|
||||
$linkmod_type = self::get_linkmod_type($linkmod_classes);
|
||||
|
||||
// START appending the internal item contents to the output.
|
||||
$item_output = isset($args->before) ? $args->before : '';
|
||||
|
||||
/*
|
||||
* This is the start of the internal nav item. Depending on what
|
||||
* kind of linkmod we have we may need different wrapper elements.
|
||||
*/
|
||||
if ('' !== $linkmod_type) {
|
||||
// Is linkmod, output the required element opener.
|
||||
$item_output .= self::linkmod_element_open($linkmod_type, $attributes);
|
||||
} else {
|
||||
// With no link mod type set this must be a standard <a> tag.
|
||||
$item_output .= '<a' . $attributes . '>';
|
||||
}
|
||||
|
||||
/*
|
||||
* Initiate empty icon var, then if we have a string containing any
|
||||
* icon classes form the icon markup with an <i> element. This is
|
||||
* output inside of the item before the $title (the link text).
|
||||
*/
|
||||
$icon_html = '';
|
||||
if (!empty($icon_class_string)) {
|
||||
// Append an <i> with the icon classes to what is output before links.
|
||||
$icon_html = '<i class="' . esc_attr($icon_class_string) . '" aria-hidden="true"></i> ';
|
||||
}
|
||||
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
$title = apply_filters('the_title', $item->title, $item->ID);
|
||||
|
||||
/**
|
||||
* Filters a menu item's title.
|
||||
*
|
||||
* @since WP 4.4.0
|
||||
*
|
||||
* @param string $title The menu item's title.
|
||||
* @param WP_Post $item The current menu item.
|
||||
* @param stdClass $args An object of wp_nav_menu() arguments.
|
||||
* @param int $depth Depth of menu item. Used for padding.
|
||||
*/
|
||||
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
|
||||
|
||||
// If the .sr-only class was set apply to the nav items text only.
|
||||
if (in_array('sr-only', $linkmod_classes, true)) {
|
||||
$title = self::wrap_for_screen_reader($title);
|
||||
$keys_to_unset = array_keys($linkmod_classes, 'sr-only', true);
|
||||
foreach ($keys_to_unset as $k) {
|
||||
unset($linkmod_classes[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Put the item contents into $output.
|
||||
$item_output .= isset($args->link_before) ? $args->link_before . $icon_html . $title . $args->link_after : '';
|
||||
|
||||
/*
|
||||
* This is the end of the internal nav item. We need to close the
|
||||
* correct element depending on the type of link or link mod.
|
||||
*/
|
||||
if ('' !== $linkmod_type) {
|
||||
// Is linkmod, output the required closing element.
|
||||
$item_output .= self::linkmod_element_close($linkmod_type);
|
||||
} else {
|
||||
// With no link mod type set this must be a standard <a> tag.
|
||||
$item_output .= '</a>';
|
||||
}
|
||||
|
||||
$item_output .= isset($args->after) ? $args->after : '';
|
||||
|
||||
// END appending the internal item contents to the output.
|
||||
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse elements to create list from elements.
|
||||
*
|
||||
* Display one element if the element doesn't have any children otherwise,
|
||||
* display the element and its children. Will only traverse up to the max
|
||||
* depth and no ignore elements under that depth. It is possible to set the
|
||||
* max depth to include all depths, see walk() method.
|
||||
*
|
||||
* This method should not be called directly, use the walk() method instead.
|
||||
*
|
||||
* @since WP 2.5.0
|
||||
*
|
||||
* @see Walker::start_lvl()
|
||||
*
|
||||
* @param object $element Data object.
|
||||
* @param array $children_elements List of elements to continue traversing (passed by reference).
|
||||
* @param int $max_depth Max depth to traverse.
|
||||
* @param int $depth Depth of current element.
|
||||
* @param array $args An array of arguments.
|
||||
* @param string $output Used to append additional content (passed by reference).
|
||||
*/
|
||||
public function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output)
|
||||
{
|
||||
if (!$element) {
|
||||
return;}
|
||||
$id_field = $this->db_fields['id'];
|
||||
// Display this element.
|
||||
if (is_object($args[0])) {
|
||||
$args[0]->has_children = !empty($children_elements[$element->$id_field]);}
|
||||
parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Fallback.
|
||||
*
|
||||
* If this function is assigned to the wp_nav_menu's fallback_cb variable
|
||||
* and a menu has not been assigned to the theme location in the WordPress
|
||||
* menu manager the function with display nothing to a non-logged in user,
|
||||
* and will add a link to the WordPress menu manager if logged in as an admin.
|
||||
*
|
||||
* @param array $args passed from the wp_nav_menu function.
|
||||
*/
|
||||
public static function fallback($args)
|
||||
{
|
||||
if (current_user_can('edit_theme_options')) {
|
||||
|
||||
// Get Arguments.
|
||||
$container = $args['container'];
|
||||
$container_id = $args['container_id'];
|
||||
$container_class = $args['container_class'];
|
||||
$menu_class = $args['menu_class'];
|
||||
$menu_id = $args['menu_id'];
|
||||
|
||||
// Initialize var to store fallback html.
|
||||
$fallback_output = '';
|
||||
|
||||
if ($container) {
|
||||
$fallback_output .= '<' . esc_attr($container);
|
||||
if ($container_id) {
|
||||
$fallback_output .= ' id="' . esc_attr($container_id) . '"';
|
||||
}
|
||||
if ($container_class) {
|
||||
$fallback_output .= ' class="' . esc_attr($container_class) . '"';
|
||||
}
|
||||
$fallback_output .= '>';
|
||||
}
|
||||
$fallback_output .= '<ul';
|
||||
if ($menu_id) {
|
||||
$fallback_output .= ' id="' . esc_attr($menu_id) . '"';}
|
||||
if ($menu_class) {
|
||||
$fallback_output .= ' class="' . esc_attr($menu_class) . '"';}
|
||||
$fallback_output .= '>';
|
||||
$fallback_output .= '<li class="nav-item"><a href="' . esc_url(admin_url('nav-menus.php')) . '" class="nav-link">' . esc_attr__('添加导航', 'kratos') . '</a></li>';
|
||||
$fallback_output .= '</ul>';
|
||||
if ($container) {
|
||||
$fallback_output .= '</' . esc_attr($container) . '>';
|
||||
}
|
||||
|
||||
// If $args has 'echo' key and it's true echo, otherwise return.
|
||||
if (array_key_exists('echo', $args) && $args['echo']) {
|
||||
echo $fallback_output; // WPCS: XSS OK.
|
||||
} else {
|
||||
return $fallback_output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any custom linkmod or icon classes and store in their holder
|
||||
* arrays then remove them from the main classes array.
|
||||
*
|
||||
* Supported linkmods: .disabled, .dropdown-header, .dropdown-divider, .sr-only
|
||||
* Supported iconsets: Font Awesome 4/5, Glypicons
|
||||
*
|
||||
* NOTE: This accepts the linkmod and icon arrays by reference.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $classes an array of classes currently assigned to the item.
|
||||
* @param array $linkmod_classes an array to hold linkmod classes.
|
||||
* @param array $icon_classes an array to hold icon classes.
|
||||
* @param integer $depth an integer holding current depth level.
|
||||
*
|
||||
* @return array $classes a maybe modified array of classnames.
|
||||
*/
|
||||
private function separate_linkmods_and_icons_from_classes($classes, &$linkmod_classes, &$icon_classes, $depth)
|
||||
{
|
||||
// Loop through $classes array to find linkmod or icon classes.
|
||||
foreach ($classes as $key => $class) {
|
||||
/*
|
||||
* If any special classes are found, store the class in it's
|
||||
* holder array and and unset the item from $classes.
|
||||
*/
|
||||
if (preg_match('/^disabled|^sr-only/i', $class)) {
|
||||
// Test for .disabled or .sr-only classes.
|
||||
$linkmod_classes[] = $class;
|
||||
unset($classes[$key]);
|
||||
} elseif (preg_match('/^dropdown-header|^dropdown-divider|^dropdown-item-text/i', $class) && $depth > 0) {
|
||||
/*
|
||||
* Test for .dropdown-header or .dropdown-divider and a
|
||||
* depth greater than 0 - IE inside a dropdown.
|
||||
*/
|
||||
$linkmod_classes[] = $class;
|
||||
unset($classes[$key]);
|
||||
} elseif (preg_match('/^fa-(\S*)?|^fa(s|r|l|b)?(\s?)?$/i', $class)) {
|
||||
// Font Awesome.
|
||||
$icon_classes[] = $class;
|
||||
unset($classes[$key]);
|
||||
} elseif (preg_match('/^glyphicon-(\S*)?|^glyphicon(\s?)$/i', $class)) {
|
||||
// Glyphicons.
|
||||
$icon_classes[] = $class;
|
||||
unset($classes[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string containing a linkmod type and update $atts array
|
||||
* accordingly depending on the decided.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $linkmod_classes array of any link modifier classes.
|
||||
*
|
||||
* @return string empty for default, a linkmod type string otherwise.
|
||||
*/
|
||||
private function get_linkmod_type($linkmod_classes = array())
|
||||
{
|
||||
$linkmod_type = '';
|
||||
// Loop through array of linkmod classes to handle their $atts.
|
||||
if (!empty($linkmod_classes)) {
|
||||
foreach ($linkmod_classes as $link_class) {
|
||||
if (!empty($link_class)) {
|
||||
|
||||
// Check for special class types and set a flag for them.
|
||||
if ('dropdown-header' === $link_class) {
|
||||
$linkmod_type = 'dropdown-header';
|
||||
} elseif ('dropdown-divider' === $link_class) {
|
||||
$linkmod_type = 'dropdown-divider';
|
||||
} elseif ('dropdown-item-text' === $link_class) {
|
||||
$linkmod_type = 'dropdown-item-text';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $linkmod_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attributes of a nav item depending on the limkmod classes.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $atts array of atts for the current link in nav item.
|
||||
* @param array $linkmod_classes an array of classes that modify link or nav item behaviors or displays.
|
||||
*
|
||||
* @return array maybe updated array of attributes for item.
|
||||
*/
|
||||
private function update_atts_for_linkmod_type($atts = array(), $linkmod_classes = array())
|
||||
{
|
||||
if (!empty($linkmod_classes)) {
|
||||
foreach ($linkmod_classes as $link_class) {
|
||||
if (!empty($link_class)) {
|
||||
/*
|
||||
* Update $atts with a space and the extra classname
|
||||
* so long as it's not a sr-only class.
|
||||
*/
|
||||
if ('sr-only' !== $link_class) {
|
||||
$atts['class'] .= ' ' . esc_attr($link_class);
|
||||
}
|
||||
// Check for special class types we need additional handling for.
|
||||
if ('disabled' === $link_class) {
|
||||
// Convert link to '#' and unset open targets.
|
||||
$atts['href'] = '#';
|
||||
unset($atts['target']);
|
||||
} elseif ('dropdown-header' === $link_class || 'dropdown-divider' === $link_class || 'dropdown-item-text' === $link_class) {
|
||||
// Store a type flag and unset href and target.
|
||||
unset($atts['href']);
|
||||
unset($atts['target']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $atts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the passed text in a screen reader only class.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $text the string of text to be wrapped in a screen reader class.
|
||||
* @return string the string wrapped in a span with the class.
|
||||
*/
|
||||
private function wrap_for_screen_reader($text = '')
|
||||
{
|
||||
if ($text) {
|
||||
$text = '<span class="sr-only">' . $text . '</span>';
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the correct opening element and attributes for a linkmod.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $linkmod_type a sting containing a linkmod type flag.
|
||||
* @param string $attributes a string of attributes to add to the element.
|
||||
*
|
||||
* @return string a string with the openign tag for the element with attribibutes added.
|
||||
*/
|
||||
private function linkmod_element_open($linkmod_type, $attributes = '')
|
||||
{
|
||||
$output = '';
|
||||
if ('dropdown-item-text' === $linkmod_type) {
|
||||
$output .= '<span class="dropdown-item-text"' . $attributes . '>';
|
||||
} elseif ('dropdown-header' === $linkmod_type) {
|
||||
/*
|
||||
* For a header use a span with the .h6 class instead of a real
|
||||
* header tag so that it doesn't confuse screen readers.
|
||||
*/
|
||||
$output .= '<span class="dropdown-header h6"' . $attributes . '>';
|
||||
} elseif ('dropdown-divider' === $linkmod_type) {
|
||||
// This is a divider.
|
||||
$output .= '<div class="dropdown-divider"' . $attributes . '>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the correct closing tag for the linkmod element.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $linkmod_type a string containing a special linkmod type.
|
||||
*
|
||||
* @return string a string with the closing tag for this linkmod type.
|
||||
*/
|
||||
private function linkmod_element_close($linkmod_type)
|
||||
{
|
||||
$output = '';
|
||||
if ('dropdown-header' === $linkmod_type || 'dropdown-item-text' === $linkmod_type) {
|
||||
/*
|
||||
* For a header use a span with the .h6 class instead of a real
|
||||
* header tag so that it doesn't confuse screen readers.
|
||||
*/
|
||||
$output .= '</span>';
|
||||
} elseif ('dropdown-divider' === $linkmod_type) {
|
||||
// This is a divider.
|
||||
$output .= '</div>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
/**
|
||||
* 主题选项
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.09.21
|
||||
*/
|
||||
|
||||
function getrobots()
|
||||
{
|
||||
$site_url = parse_url(site_url());
|
||||
$path = (!empty($site_url['path'])) ? $site_url['path'] : '';
|
||||
|
||||
$robots = "User-agent: *\n\n";
|
||||
$robots .= "Disallow: $path/wp-admin/\n";
|
||||
$robots .= "Disallow: $path/wp-includes/\n";
|
||||
$robots .= "Disallow: $path/wp-content/plugins/\n";
|
||||
$robots .= "Disallow: $path/wp-content/themes/\n";
|
||||
|
||||
return $robots;
|
||||
}
|
||||
|
||||
function kratos_options()
|
||||
{
|
||||
$sitename = get_bloginfo('name');
|
||||
|
||||
$imagepath = ASSET_PATH . '/assets/img/options/';
|
||||
|
||||
$seorobots = '<a href="' . home_url() . '/robots.txt" target="_blank">robots.txt</a>';
|
||||
$seoreading = '<a href="' . admin_url('options-reading.php') . '" target="_blank">' . __('设置-阅读-对搜索引擎的可见性', 'kratos') . '</a>';
|
||||
|
||||
$cc_array = array(
|
||||
'one' => __('知识共享署名 4.0 国际许可协议', 'kratos'),
|
||||
'two' => __('知识共享署名-非商业性使用 4.0 国际许可协议', 'kratos'),
|
||||
'three' => __('知识共享署名-禁止演绎 4.0 国际许可协议', 'kratos'),
|
||||
'four' => __('知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议', 'kratos'),
|
||||
'five' => __('知识共享署名-相同方式共享 4.0 国际许可协议', 'kratos'),
|
||||
'six' => __('知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议', 'kratos'),
|
||||
);
|
||||
|
||||
$top_array = array(
|
||||
'banner' => __( '图片导航', 'kratos' ),
|
||||
'color' => __( '颜色导航', 'kratos' ),
|
||||
);
|
||||
|
||||
$options = array();
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('全站配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('图片 Logo', 'kratos'),
|
||||
'desc' => __('不选择图片则显示文字标题', 'kratos'),
|
||||
'id' => 'g_logo',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('Favicon 图片', 'kratos'),
|
||||
'desc' => __('浏览器收藏夹和地址栏中显示的图标', 'kratos'),
|
||||
'id' => 'g_icon',
|
||||
'type' => 'upload',
|
||||
);
|
||||
$options[] = array(
|
||||
'name' => __('是否添加国旗', 'kratos'),
|
||||
'desc' => __('在页脚添加国旗', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'g_flag',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
$options[] = array(
|
||||
'name' => __('国旗地址', 'kratos'),
|
||||
'desc' => __('选择国旗图片地址', 'kratos'),
|
||||
'id' => 'g_flag_url',
|
||||
'type' => 'upload',
|
||||
);
|
||||
$options[] = array(
|
||||
'name' => __('背景颜色', 'kratos'),
|
||||
'desc' => __('全站页面的背景颜色,需填写十六进制颜色码', 'kratos'),
|
||||
'id' => 'g_background',
|
||||
'std' => '#f5f5f5',
|
||||
'type' => 'color',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('前台管理员导航', 'kratos'),
|
||||
'desc' => __('开启前台管理员导航', 'kratos'),
|
||||
'std' => '1',
|
||||
'id' => 'g_adminbar',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('多人模式', 'kratos'),
|
||||
'desc' => __('在文章列表显示当前文章作者,在文章页面页脚显示当前作者介绍', 'kratos'),
|
||||
'id' => 'multiusers',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('CSS 动画库', 'kratos'),
|
||||
'desc' => __('开启 animate.css 效果', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'g_animate',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('Font Awesome', 'kratos'),
|
||||
'desc' => __('开启 Font Awesome 字体', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'g_fontawesome',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('禁止生成缩略图', 'kratos'),
|
||||
'desc' => __('是否禁止生成多种尺寸图片资源', 'kratos'),
|
||||
'id' => 'g_removeimgsize',
|
||||
'std' => '0',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('Gutenberg 编辑器', 'kratos'),
|
||||
'desc' => __('开启 Gutenberg 编辑器', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'g_gutenberg',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('Gravatar 加速', 'kratos'),
|
||||
'desc' => __('开启 Gravatar 头像加速', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'g_gravatar',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('Chrome 导航栏颜色', 'kratos'),
|
||||
'desc' => __('Chrome 移动端浏览器导航栏的颜色', 'kratos'),
|
||||
'id' => 'g_chrome',
|
||||
'std' => '#282a2c',
|
||||
'type' => 'color',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('微信二维码', 'kratos'),
|
||||
'desc' => __('开启页面右下角浮动微信二维码', 'kratos'),
|
||||
'id' => 's_wechat',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'id' => 's_wechat_url',
|
||||
'std' => ASSET_PATH . '/assets/img/wechat.png',
|
||||
'type' => 'upload',
|
||||
'class' => 'hidden',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('404 页面图片', 'kratos'),
|
||||
'id' => 'g_404',
|
||||
'std' => ASSET_PATH . '/assets/img/404.jpg',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('收录配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('分享图片', 'kratos'),
|
||||
'desc' => __('搜索引擎或者社交工具分享首页时抓取的图片', 'kratos'),
|
||||
'id' => 'seo_shareimg',
|
||||
'std' => ASSET_PATH . '/assets/img/default.jpg',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('关键词', 'kratos'),
|
||||
'desc' => __('每个关键词之间需要用「英文逗号」分割', 'kratos'),
|
||||
'id' => 'seo_keywords',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('站点描述', 'kratos'),
|
||||
'id' => 'seo_description',
|
||||
'type' => 'textarea',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('统计代码', 'kratos'),
|
||||
'desc' => __('注意:输入 HTML/JS 代码时请注意辨别代码安全!', 'kratos'),
|
||||
'id' => 'seo_statistical',
|
||||
'type' => 'textarea',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('robots.txt 配置', 'kratos'),
|
||||
'desc' => __('- 需要 ', 'kratos') . $seoreading . __(' 是开启的状态,下面的配置才会生效', 'kratos'),
|
||||
'type' => 'info',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'desc' => __('- 如果网站根目录下已经有 robots.txt 文件,下面的配置不会生效', 'kratos'),
|
||||
'type' => 'info',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'desc' => __('- 点击 ', 'kratos') . $seorobots . __(' 查看配置是否生效,如果网站开启了 CDN,可能需要刷新缓存才会生效', 'kratos'),
|
||||
'type' => 'info',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'id' => 'seo_robots',
|
||||
'std' => getrobots(),
|
||||
'type' => 'textarea',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('首页配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('哀悼功能', 'kratos'),
|
||||
'desc' => __('开启站点首页黑白功能(用于R.I.P.)', 'kratos'),
|
||||
'id' => 'g_rip',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('特色图片', 'kratos'),
|
||||
'desc' => __('开启站点首页特色图片功能', 'kratos'),
|
||||
'std' => '1',
|
||||
'id' => 'g_thumbnail',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('默认特色图', 'kratos'),
|
||||
'desc' => __('当文章中没有图片并且没有设置特色图时在首页显示', 'kratos'),
|
||||
'id' => 'g_postthumbnail',
|
||||
'class' => 'hidden',
|
||||
'std' => ASSET_PATH . '/assets/img/default.jpg',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('无内容图片', 'kratos'),
|
||||
'desc' => __('当搜索不到文章或文章分类中没有文章时显示', 'kratos'),
|
||||
'id' => 'g_nothing',
|
||||
'std' => ASSET_PATH . '/assets/img/nothing.svg',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('文章配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('文章打赏', 'kratos'),
|
||||
'desc' => __('开启文章页面打赏功能', 'kratos'),
|
||||
'id' => 'g_donate',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('支付宝二维码', 'kratos'),
|
||||
'id' => 'g_donate_alipay',
|
||||
'std' => ASSET_PATH . '/assets/img/donate.png',
|
||||
'type' => 'upload',
|
||||
'class' => 'hidden',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('微信二维码', 'kratos'),
|
||||
'id' => 'g_donate_wechat',
|
||||
'std' => ASSET_PATH . '/assets/img/donate.png',
|
||||
'type' => 'upload',
|
||||
'class' => 'hidden',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('页面布局', 'kratos'),
|
||||
'desc' => __('是否显示侧边栏小工具(默认显示侧边栏),仅在文章页面生效', 'kratos'),
|
||||
'id' => "g_article_widgets",
|
||||
'std' => "two_side",
|
||||
'type' => "images",
|
||||
'options' => array(
|
||||
'one_side' => $imagepath . 'col-12.png',
|
||||
'two_side' => $imagepath . 'col-8.png')
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('站长配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('个人头像', 'kratos'),
|
||||
'id' => 'a_gravatar',
|
||||
'std' => ASSET_PATH . '/assets/img/gravatar.png',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('个人昵称', 'kratos'),
|
||||
'id' => 'a_nickname',
|
||||
'std' => 'Kratos',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('个人简介', 'kratos'),
|
||||
'std' => __('保持饥渴的专注,追求最佳的品质', 'kratos'),
|
||||
'id' => 'a_about',
|
||||
'type' => 'textarea',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('邮件配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('SMTP 服务', 'kratos'),
|
||||
'desc' => __('开启 SMTP 服务功能', 'kratos'),
|
||||
'id' => 'm_smtp',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('邮件服务器', 'kratos'),
|
||||
'desc' => __('填写发件服务器地址', 'kratos'),
|
||||
'id' => 'm_host',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('服务器端口', 'kratos'),
|
||||
'desc' => __('填写发件服务器端口', 'kratos'),
|
||||
'id' => 'm_port',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('授权方式', 'kratos'),
|
||||
'desc' => __('填写登录鉴权的方式,ssl 或 tls', 'kratos'),
|
||||
'id' => 'm_sec',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('邮箱帐号', 'kratos'),
|
||||
'desc' => __('填写邮箱账号', 'kratos'),
|
||||
'id' => 'm_username',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('邮箱密码', 'kratos'),
|
||||
'desc' => __('填写邮箱密码', 'kratos'),
|
||||
'id' => 'm_passwd',
|
||||
'class' => 'hidden',
|
||||
'type' => 'password',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'id' => 'm_sendmail',
|
||||
'class' => 'hidden',
|
||||
'type' => 'sendmail',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('顶部配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __( '顶部样式', 'kratos' ),
|
||||
'desc' => __('请选择顶部样式(颜色导航或图片导航)', 'kratos'),
|
||||
'id' => 'top_select',
|
||||
'std' => 'banner',
|
||||
'type' => 'select',
|
||||
'options' => $top_array
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('颜色导航', 'kratos'),
|
||||
'id' => 'top_color',
|
||||
'std' => '#24292e',
|
||||
'class' => 'hidden',
|
||||
'type' => 'color',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('图片导航', 'kratos'),
|
||||
'id' => 'top_img',
|
||||
'std' => ASSET_PATH . '/assets/img/background.png',
|
||||
'class' => 'hidden',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('副标题', 'kratos'),
|
||||
'id' => 'top_title',
|
||||
'std' => 'Kratos',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('标题描述', 'kratos'),
|
||||
'std' => __('一款专注于用户阅读体验的响应式博客主题', 'kratos'),
|
||||
'id' => 'top_describe',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('页脚配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('工信部备案信息', 'kratos'),
|
||||
'id' => 's_icp',
|
||||
'placeholder' => __('例如:京ICP证xxxxxx号', 'kratos'),
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('公安网备案信息', 'kratos'),
|
||||
'id' => 's_gov',
|
||||
'placeholder' => __('例如:京公网安备 xxxxxxxxxxxx号', 'kratos'),
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('公安网备案连接', 'kratos'),
|
||||
'id' => 's_gov_link',
|
||||
'placeholder' => __('例如:http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=xxxxx', 'kratos'),
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('版权信息', 'kratos'),
|
||||
'id' => 's_copyright',
|
||||
'std' => 'COPYRIGHT © 2020 ' . $sitename . '. ALL RIGHTS RESERVED.',
|
||||
'type' => 'textarea',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('广告配置', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('文章页面广告', 'kratos'),
|
||||
'desc' => __('开启顶部广告', 'kratos'),
|
||||
'id' => 's_singletop',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'id' => 's_singletop_url',
|
||||
'class' => 'hidden',
|
||||
'std' => ASSET_PATH . '/assets/img/ad.png',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'desc' => __('选填广告连接,如果不填则只显示图片', 'kratos'),
|
||||
'id' => 's_singletop_links',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'desc' => __('开启底部广告', 'kratos'),
|
||||
'id' => 's_singledown',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'id' => 's_singledown_url',
|
||||
'class' => 'hidden',
|
||||
'std' => ASSET_PATH . '/assets/img/ad.png',
|
||||
'type' => 'upload',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'desc' => __('选填广告连接,如果不填则只显示图片', 'kratos'),
|
||||
'id' => 's_singledown_links',
|
||||
'class' => 'hidden',
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
|
||||
return $options;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* 站点相关函数
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.08.04
|
||||
*/
|
||||
|
||||
// 标题配置
|
||||
function title($title, $sep)
|
||||
{
|
||||
global $paged, $page;
|
||||
if (is_feed()) {
|
||||
return $title;
|
||||
}
|
||||
$title .= get_bloginfo('name');
|
||||
$site_description = get_bloginfo('description', 'display');
|
||||
if ($site_description && (is_home() || is_front_page())) {
|
||||
$title = "{$title} {$sep} {$site_description}";
|
||||
}
|
||||
if ($paged >= 2 || $page >= 2) {
|
||||
$title = "{$title} {$sep} " . sprintf(__('第 %s 页', 'kratos'), max($paged, $page));
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
add_filter('wp_title', 'title', 10, 2);
|
||||
|
||||
// Keywords 配置
|
||||
function keywords()
|
||||
{
|
||||
global $post;
|
||||
$keywords = '';
|
||||
if (is_home()) {
|
||||
$keywords = kratos_option('seo_keywords');
|
||||
} elseif (is_single()) {
|
||||
$keywords = get_post_meta($post->ID, "seo_keywords_value", true);
|
||||
if ($keywords == '') {
|
||||
$tags = wp_get_post_tags($post->ID);
|
||||
foreach ($tags as $tag) {
|
||||
$keywords = $keywords . $tag->name . ",";
|
||||
}
|
||||
$keywords = rtrim($keywords, ',');
|
||||
}
|
||||
} elseif (is_page()) {
|
||||
$keywords = get_post_meta($post->ID, "seo_keywords_value", true);
|
||||
if ($keywords == '') {
|
||||
$keywords = kratos_option('seo_keywords');
|
||||
}
|
||||
} else {
|
||||
$keywords = single_tag_title('', false);
|
||||
}
|
||||
return trim(strip_tags($keywords));
|
||||
}
|
||||
|
||||
// Description 配置
|
||||
function description()
|
||||
{
|
||||
global $post;
|
||||
$description = '';
|
||||
if (is_home()) {
|
||||
$description = kratos_option('seo_description');
|
||||
} elseif (is_single()) {
|
||||
$description = get_post_meta($post->ID, "seo_description_value", true);
|
||||
if ($description == '') {
|
||||
$description = get_the_excerpt();
|
||||
}
|
||||
if ($description == '') {
|
||||
$description = str_replace("\n", "", mb_strimwidth(strip_tags($post->post_content), 0, 200, "…", 'utf-8'));
|
||||
}
|
||||
} elseif (is_category()) {
|
||||
$description = category_description();
|
||||
} elseif (is_tag()) {
|
||||
$description = tag_description();
|
||||
} elseif (is_page()) {
|
||||
$description = get_post_meta($post->ID, "seo_description_value", true);
|
||||
if ($description == '') {
|
||||
$description = kratos_option('seo_description');
|
||||
}
|
||||
}
|
||||
return trim(strip_tags($description));
|
||||
}
|
||||
|
||||
// robots.txt 配置
|
||||
add_filter('robots_txt', function ($output, $public) {
|
||||
if ('0' == $public) {
|
||||
return "User-agent: *\nDisallow: /\n";
|
||||
} else {
|
||||
if (!empty(kratos_option('seo_robots'))) {
|
||||
$output = esc_attr(strip_tags(kratos_option('seo_robots')));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}, 10, 2);
|
||||
|
||||
// 哀悼黑白站点
|
||||
function mourning()
|
||||
{
|
||||
if (is_home() && kratos_option('g_rip', false)) {
|
||||
echo '<style type="text/css">html{filter: grayscale(100%);-webkit-filter: grayscale(100%);-moz-filter: grayscale(100%);-ms-filter: grayscale(100%);-o-filter: grayscale(100%);filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);filter: gray;-webkit-filter: grayscale(1); } </style>';
|
||||
}
|
||||
}
|
||||
|
||||
// 抓取图片链接(搜索引擎或者社交工具分享时抓取图片的链接)
|
||||
function share_thumbnail_url()
|
||||
{
|
||||
global $post;
|
||||
if (has_post_thumbnail($post->ID)) {
|
||||
$post_thumbnail_id = get_post_thumbnail_id($post);
|
||||
$img = wp_get_attachment_image_src($post_thumbnail_id, 'full');
|
||||
$img = $img[0];
|
||||
} else {
|
||||
$content = $post->post_content;
|
||||
preg_match_all('/<img.*?(?: |\\t|\\r|\\n)?src=[\'"]?(.+?)[\'"]?(?:(?: |\\t|\\r|\\n)+.*?); ?>/sim', $content, $strResult, PREG_PATTERN_ORDER);
|
||||
if (!empty($strResult[1])) {
|
||||
$img = $strResult[1][0];
|
||||
} else {
|
||||
$img = kratos_option('seo_shareimg', ASSET_PATH . '/assets/img/default.jpg');
|
||||
}
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
// 支持上传 svg
|
||||
add_filter('upload_mimes', 'upload_svg');
|
||||
function upload_svg($existing_mimes = array())
|
||||
{
|
||||
$existing_mimes['svg'] = 'image/svg+xml';
|
||||
return $existing_mimes;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
/**
|
||||
* 文章短代码
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.06.25
|
||||
*/
|
||||
|
||||
function h2title($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<h2 class="title">';
|
||||
$return .= $content;
|
||||
$return .= '</h2>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('h2title', 'h2title');
|
||||
|
||||
function success($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="alert alert-success">';
|
||||
$return .= $content;
|
||||
$return .= '</div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('success', 'success');
|
||||
|
||||
function info($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="alert alert-info">';
|
||||
$return .= $content;
|
||||
$return .= '</div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('info', 'info');
|
||||
|
||||
function warning($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="alert alert-warning">';
|
||||
$return .= $content;
|
||||
$return .= '</div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('warning', 'warning');
|
||||
|
||||
function danger($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="alert alert-danger">';
|
||||
$return .= $content;
|
||||
$return .= '</div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('danger', 'danger');
|
||||
|
||||
function wymusic($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="mb-3"><iframe style="width:100%" frameborder="no" border="0" marginwidth="0" marginheight="0" height=86 src="//music.163.com/outchain/player?type=2&id=';
|
||||
$return .= $content;
|
||||
$return .= '&auto=' . kratos_option('g_163mic', false) . '&height=66"></iframe></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('music', 'wymusic');
|
||||
|
||||
function bdbtn($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<a class="downbtn" href="';
|
||||
$return .= $content;
|
||||
$return .= '" target="_blank"><i class="kicon i-download mr-1"></i>立即下载</a>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('bdbtn', 'bdbtn');
|
||||
|
||||
function kbd($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<kbd>';
|
||||
$return .= $content;
|
||||
$return .= '</kbd>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('kbd', 'kbd');
|
||||
|
||||
function nrmark($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<mark>';
|
||||
$return .= $content;
|
||||
$return .= '</mark>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('mark', 'nrmark');
|
||||
|
||||
function striped($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="progress"><div class="progress-bar" role="progressbar" style="width:';
|
||||
$return .= $content;
|
||||
$return .= '%;" aria-valuenow="';
|
||||
$return .= $content;
|
||||
$return .= '" aria-valuemin="0" aria-valuemax="100">';
|
||||
$return .= $content;
|
||||
$return .= '%</div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('striped', 'striped');
|
||||
|
||||
function successbox($atts, $content = null, $code = "")
|
||||
{
|
||||
extract(shortcode_atts(array("title" => __('标题内容', 'kratos')), $atts));
|
||||
$return = '<div class="card border-success text-white mb-3"><div class="card-header bg-success">';
|
||||
$return .= $title;
|
||||
$return .= '</div><div class="card-body"><p class="card-text">';
|
||||
$return .= $content;
|
||||
$return .= '</p></div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('successbox', 'successbox');
|
||||
|
||||
function infobox($atts, $content = null, $code = "")
|
||||
{
|
||||
extract(shortcode_atts(array("title" => __('标题内容', 'kratos')), $atts));
|
||||
$return = '<div class="card border-info text-white mb-3"><div class="card-header bg-info">';
|
||||
$return .= $title;
|
||||
$return .= '</div><div class="card-body"><p class="card-text">';
|
||||
$return .= $content;
|
||||
$return .= '</p></div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('infobox', 'infobox');
|
||||
|
||||
function warningbox($atts, $content = null, $code = "")
|
||||
{
|
||||
extract(shortcode_atts(array("title" => __('标题内容', 'kratos')), $atts));
|
||||
$return = '<div class="card border-warning text-white mb-3"><div class="card-header bg-warning">';
|
||||
$return .= $title;
|
||||
$return .= '</div><div class="card-body"><p class="card-text">';
|
||||
$return .= $content;
|
||||
$return .= '</p></div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('warningbox', 'warningbox');
|
||||
|
||||
function dangerbox($atts, $content = null, $code = "")
|
||||
{
|
||||
extract(shortcode_atts(array("title" => __('标题内容', 'kratos')), $atts));
|
||||
$return = '<div class="card border-danger text-white mb-3"><div class="card-header bg-danger">';
|
||||
$return .= $title;
|
||||
$return .= '</div><div class="card-body"><p class="card-text">';
|
||||
$return .= $content;
|
||||
$return .= '</p></div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('dangerbox', 'dangerbox');
|
||||
|
||||
function vqq($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="video-container"><iframe frameborder="0" src="https://v.qq.com/txp/iframe/player.html?vid=';
|
||||
$return .= $content;
|
||||
$return .= '" allowFullScreen="true"></iframe></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('vqq', 'vqq');
|
||||
|
||||
function youtube($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="video-container"><iframe height="498" width="750" src="https://www.youtube.com/embed/';
|
||||
$return .= $content;
|
||||
$return .= '" frameborder="0" allowfullscreen="allowfullscreen"></iframe></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('youtube', 'youtube');
|
||||
|
||||
function bilibili($atts, $content = null, $code = "")
|
||||
{
|
||||
$return = '<div class="video-container"><iframe src="//player.bilibili.com/player.html?bvid=';
|
||||
$return .= $content;
|
||||
$return .= '&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('bilibili', 'bilibili');
|
||||
|
||||
function reply($atts, $content = null)
|
||||
{
|
||||
extract(shortcode_atts(array("notice" => '<div class="alert alert-primary text-center" role="alert">'.__('温馨提示:此处内容已隐藏,<a href="#comments">回复</a>后刷新页面即可查看!', 'kratos').'</div>'), $atts));
|
||||
$userEmail = null;
|
||||
$user_ID = (int) wp_get_current_user()->ID;
|
||||
if ($user_ID > 0) {
|
||||
$userEmail = get_userdata($user_ID)->user_email;
|
||||
$adminUsers = get_users('role=Administrator');
|
||||
$adminEmails = array();
|
||||
foreach ($adminUsers as $user) {
|
||||
$adminEmails[] = $user->user_email;
|
||||
}
|
||||
$authorEmail = get_the_author_meta('user_email');
|
||||
array_push($adminEmails, $authorEmail);
|
||||
if (in_array($userEmail, $adminEmails)) {
|
||||
return $content;
|
||||
}
|
||||
} else {
|
||||
if (isset($_COOKIE['comment_author_email_' . COOKIEHASH])) {
|
||||
$userEmail = str_replace('%40', '@', $_COOKIE['comment_author_email_' . COOKIEHASH]);
|
||||
} else {
|
||||
return $notice;
|
||||
}
|
||||
}
|
||||
if (empty($userEmail)) {
|
||||
return $notice;
|
||||
}
|
||||
global $wpdb;
|
||||
$post_id = get_the_ID();
|
||||
$query = "SELECT `comment_ID` FROM {$wpdb->comments} WHERE `comment_post_ID`={$post_id} and `comment_approved`='1' and `comment_author_email`='{$userEmail}' LIMIT 1";
|
||||
if ($wpdb->get_results($query)) {
|
||||
return do_shortcode($content);
|
||||
} else {
|
||||
return $notice;
|
||||
}
|
||||
}
|
||||
add_shortcode('reply', 'reply');
|
||||
|
||||
function accordion($atts, $content=null, $code=""){
|
||||
extract(shortcode_atts(array("title"=>__('标题内容','kratos')),$atts));
|
||||
$return = '<div class="accordion"><div class="acheader"><div class="icon"><i class="kicon i-plus"></i></div><span>';
|
||||
$return .= $title;
|
||||
$return .= '</span></div><div class="contents"><div class="inner">';
|
||||
$return .= do_shortcode($content);
|
||||
$return .= '</div></div></div>';
|
||||
return $return;
|
||||
}
|
||||
add_shortcode('accordion','accordion');
|
||||
|
||||
add_action('init', 'more_button');
|
||||
function more_button()
|
||||
{
|
||||
if (!current_user_can('edit_posts') && !current_user_can('edit_pages')) {
|
||||
return;
|
||||
}
|
||||
if (get_user_option('rich_editing') == 'true') {
|
||||
add_filter('mce_external_plugins', 'add_plugin');
|
||||
add_filter('mce_buttons', 'register_button');
|
||||
}
|
||||
}
|
||||
|
||||
function add_more_buttons($buttons) {
|
||||
$buttons[] = 'hr';
|
||||
$buttons[] = 'wp_page';
|
||||
$buttons[] = 'fontsizeselect';
|
||||
$buttons[] = 'styleselect';
|
||||
return $buttons;
|
||||
}
|
||||
add_filter("mce_buttons", "add_more_buttons");
|
||||
|
||||
function register_button($buttons)
|
||||
{
|
||||
array_push($buttons, " ", "h2title");
|
||||
array_push($buttons, " ", "kbd");
|
||||
array_push($buttons, " ", "mark");
|
||||
array_push($buttons, " ", "striped");
|
||||
array_push($buttons, " ", "bdbtn");
|
||||
array_push($buttons, " ", "reply");
|
||||
array_push($buttons, " ", "accordion");
|
||||
array_push($buttons, " ", "music");
|
||||
array_push($buttons, " ", "vqq");
|
||||
array_push($buttons, " ", "youtube");
|
||||
array_push($buttons, " ", "bilibili");
|
||||
array_push($buttons, " ", "success");
|
||||
array_push($buttons, " ", "info");
|
||||
array_push($buttons, " ", "warning");
|
||||
array_push($buttons, " ", "danger");
|
||||
array_push($buttons, " ", "successbox");
|
||||
array_push($buttons, " ", "infoboxs");
|
||||
array_push($buttons, " ", "warningbox");
|
||||
array_push($buttons, " ", "dangerbox");
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
function add_plugin($plugin_array)
|
||||
{
|
||||
$plugin_array['h2title'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['kbd'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['mark'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['striped'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['bdbtn'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['reply'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['accordion'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['music'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['vqq'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['youtube'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['bilibili'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['success'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['info'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['warning'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['danger'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['successbox'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['infoboxs'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['warningbox'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
$plugin_array['dangerbox'] = ASSET_PATH . '/assets/js/buttons/more.js';
|
||||
return $plugin_array;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* SMTP 配置
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.02.15
|
||||
*/
|
||||
|
||||
if (kratos_option('m_smtp', false)) {
|
||||
function mail_smtp($phpmailer)
|
||||
{
|
||||
$phpmailer->isSMTP();
|
||||
$phpmailer->SMTPAuth = true;
|
||||
$phpmailer->CharSet = "utf-8";
|
||||
$phpmailer->SMTPSecure = kratos_option('m_sec');
|
||||
$phpmailer->Port = kratos_option('m_port');
|
||||
$phpmailer->Host = kratos_option('m_host');
|
||||
$phpmailer->From = kratos_option('m_username');
|
||||
$phpmailer->Username = kratos_option('m_username');
|
||||
$phpmailer->Password = kratos_option('m_passwd');
|
||||
}
|
||||
add_action('phpmailer_init', 'mail_smtp');
|
||||
}
|
||||
|
||||
// Debug
|
||||
function wp_mail_debug($wp_error)
|
||||
{
|
||||
return error_log(print_r($wp_error, true));
|
||||
}
|
||||
// add_action('wp_mail_failed', 'wp_mail_debug', 10, 1);
|
||||
|
||||
function comment_approved($comment)
|
||||
{
|
||||
if (is_email($comment->comment_author_email)) {
|
||||
$wp_email = kratos_option('m_username');
|
||||
$to = trim($comment->comment_author_email);
|
||||
$post_link = get_permalink($comment->comment_post_ID);
|
||||
$subject = __('[通知]您的留言已经通过审核', 'kratos');
|
||||
$message = '
|
||||
<div style="background:#ececec;width: 100%;padding: 50px 0;text-align:center;">
|
||||
<div style="background:#fff;width:750px;text-align:left;position:relative;margin:0 auto;font-size:14px;line-height:1.5;">
|
||||
<div style="zoom:1;padding:25px 40px;background:#518bcb; border-bottom:1px solid #467ec3;">
|
||||
<h1 style="color:#fff; font-size:25px;line-height:30px; margin:0;"><a href="' . get_option('home') . '" style="text-decoration: none;color: #FFF;">' . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . '</a></h1>
|
||||
</div>
|
||||
<div style="padding:35px 40px 30px;">
|
||||
<h2 style="font-size:18px;margin:5px 0;">' . __('您好,', 'kratos') . trim($comment->comment_author) . ':</h2>
|
||||
<p style="color:#313131;line-height:20px;font-size:15px;margin:20px 0;">' . __('您的留言已经通过了管理员的审核,摘要信息如下:', 'kratos') . '</p>
|
||||
<table cellspacing="0" style="font-size:14px;text-align:center;border:1px solid #ccc;table-layout:fixed;width:500px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="280px;">' . __('文章', 'kratos') . '</th>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="270px;">' . __('内容', 'kratos') . '</th>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="110px;">' . __('操作', 'kratos') . '</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">《' . get_the_title($comment->comment_post_ID) . '》</td>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' . trim($comment->comment_content) . '</td>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><a href="' . get_comment_link($comment->comment_ID) . '" style="color:#1E5494;text-decoration:none;vertical-align:middle;" target="_blank">查看留言</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<div style="font-size:13px;color:#a0a0a0;padding-top:10px">' . __('该邮件由系统自动发出,如果不是您本人操作,请忽略此邮件。', 'kratos') . '</div>
|
||||
<div class="qmSysSign" style="padding-top:20px;font-size:12px;color:#a0a0a0;">
|
||||
<p style="color:#a0a0a0;line-height:18px;font-size:12px;margin:5px 0;">' . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . '</p>
|
||||
<p style="color:#a0a0a0;line-height:18px;font-size:12px;margin:5px 0;"><span style="border-bottom:1px dashed #ccc;" t="5" times="">' . date("Y年m月d日", time()) . '</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
$from = "From: \"" . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . "\" <$wp_email>";
|
||||
$headers = "$from\nContent-Type: text/html; charset=" . get_option('blog_charset') . "\n";
|
||||
wp_mail($to, $subject, $message, $headers);
|
||||
}
|
||||
}
|
||||
add_action('comment_unapproved_to_approved', 'comment_approved');
|
||||
|
||||
function comment_notify($comment_id)
|
||||
{
|
||||
$comment = get_comment($comment_id);
|
||||
$parent_id = $comment->comment_parent ? $comment->comment_parent : '';
|
||||
$spam_confirmed = $comment->comment_approved;
|
||||
if (($parent_id != '') && ($spam_confirmed != 'spam')) {
|
||||
$wp_email = kratos_option('m_username');
|
||||
$to = trim(get_comment($parent_id)->comment_author_email);
|
||||
$subject = __('[通知]您的留言有了新的回复', 'kratos');
|
||||
$message = '
|
||||
<div style="background:#ececec;width: 100%;padding: 50px 0;text-align:center;">
|
||||
<div style="background:#fff;width:750px;text-align:left;position:relative;margin:0 auto;font-size:14px;line-height:1.5;">
|
||||
<div style="zoom:1;padding:25px 40px;background:#518bcb; border-bottom:1px solid #467ec3;">
|
||||
<h1 style="color:#fff; font-size:25px;line-height:30px; margin:0;"><a href="' . get_option('home') . '" style="text-decoration: none;color: #FFF;">' . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . '</a></h1>
|
||||
</div>
|
||||
<div style="padding:35px 40px 30px;">
|
||||
<h2 style="font-size:18px;margin:5px 0;">' . __('您好,', 'kratos') . trim(get_comment($parent_id)->comment_author) . ':</h2>
|
||||
<p style="color:#313131;line-height:20px;font-size:15px;margin:20px 0;">' . __('您的留言有了新的回复,摘要信息如下:', 'kratos') . '</p>
|
||||
<table cellspacing="0" style="font-size:14px;text-align:center;border:1px solid #ccc;table-layout:fixed;width:500px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="235px;">' . __('原文', 'kratos') . '</th>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="235px;">' . __('回复', 'kratos') . '</th>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="100px;">' . __('作者', 'kratos') . '</th>
|
||||
<th style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:normal;color:#a0a0a0;background:#eee;border-color:#dfdfdf;" width="90px;" >' . __('操作', 'kratos') . '</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' . trim(get_comment($parent_id)->comment_content) . '</td>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' . trim($comment->comment_content) . '</td>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' . trim($comment->comment_author) . '</td>
|
||||
<td style="padding:5px 0;text-indent:8px;border:1px solid #eee;border-width:0 1px 1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><a href="' . get_comment_link($comment->comment_ID) . '" style="color:#1E5494;text-decoration:none;vertical-align:middle;" target="_blank">' . __('查看回复', 'kratos') . '</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<div style="font-size:13px;color:#a0a0a0;padding-top:10px">' . __('该邮件由系统自动发出,如果不是您本人操作,请忽略此邮件。', 'kratos') . '</div>
|
||||
<div class="qmSysSign" style="padding-top:20px;font-size:12px;color:#a0a0a0;">
|
||||
<p style="color:#a0a0a0;line-height:18px;font-size:12px;margin:5px 0;">' . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . '</p>
|
||||
<p style="color:#a0a0a0;line-height:18px;font-size:12px;margin:5px 0;"><span style="border-bottom:1px dashed #ccc;" t="5" times="">' . date("Y年m月d日", time()) . '</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
$from = "From: \"" . htmlspecialchars_decode(get_option('blogname'), ENT_QUOTES) . "\" <$wp_email>";
|
||||
$headers = "$from\nContent-Type: text/html; charset=" . get_option('blog_charset') . "\n";
|
||||
wp_mail($to, $subject, $message, $headers);
|
||||
}
|
||||
}
|
||||
add_action('comment_post', 'comment_notify');
|
||||
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
/**
|
||||
* 侧栏小工具
|
||||
* @author Seaton Jiang <seaton@vtrois.com>
|
||||
* @license MIT License
|
||||
* @version 2020.08.03
|
||||
*/
|
||||
|
||||
// 添加小工具
|
||||
function widgets_init()
|
||||
{
|
||||
register_sidebar(array(
|
||||
'name' => __('侧边栏工具', 'kratos'),
|
||||
'id' => 'sidebar_tool',
|
||||
'before_widget' => '<div class="widget %2$s">',
|
||||
'after_widget' => '</div>',
|
||||
'before_title' => '<div class="title">',
|
||||
'after_title' => '</div>',
|
||||
));
|
||||
// 去掉默认小工具
|
||||
$wp_widget = array(
|
||||
'WP_Widget_Pages',
|
||||
'WP_Widget_Meta',
|
||||
'WP_Widget_Recent_Posts',
|
||||
'WP_Widget_Recent_Comments',
|
||||
'WP_Widget_RSS',
|
||||
'WP_Widget_Search',
|
||||
'WP_Widget_Tag_Cloud',
|
||||
'WP_Nav_Menu_Widget',
|
||||
);
|
||||
|
||||
foreach ($wp_widget as $wp_widget) {
|
||||
unregister_widget($wp_widget);
|
||||
}
|
||||
}
|
||||
add_action('widgets_init', 'widgets_init');
|
||||
|
||||
// 小工具文章聚合 - 热点文章
|
||||
function most_comm_posts($days = 30, $nums = 6)
|
||||
{
|
||||
global $wpdb;
|
||||
date_default_timezone_set("PRC");
|
||||
$today = date("Y-m-d H:i:s");
|
||||
$daysago = date("Y-m-d H:i:s", strtotime($today) - ($days * 24 * 60 * 60));
|
||||
$result = $wpdb->get_results("SELECT comment_count, ID, post_title, post_date FROM $wpdb->posts WHERE post_date BETWEEN '$daysago' AND '$today' and post_type='post' and post_status='publish' ORDER BY comment_count DESC LIMIT 0 , $nums");
|
||||
$output = '';
|
||||
if (!empty($result)) {
|
||||
foreach ($result as $topten) {
|
||||
$postid = $topten->ID;
|
||||
$title = $topten->post_title;
|
||||
$commentcount = $topten->comment_count;
|
||||
if ($commentcount >= 0) {
|
||||
$output .= '<a class="bookmark-item" title="' . $title . '" href="' . get_permalink($postid) . '" rel="bookmark"><i class="kicon i-book"></i>';
|
||||
$output .= strip_tags($title);
|
||||
$output .= '</a>';
|
||||
}
|
||||
}
|
||||
}
|
||||
echo $output;
|
||||
}
|
||||
|
||||
class widget_search extends WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
$widget_ops = array(
|
||||
'classname' => 'widget_search',
|
||||
'description' => __( 'A search form for your site.' ),
|
||||
'customize_selective_refresh' => true,
|
||||
);
|
||||
parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops );
|
||||
}
|
||||
|
||||
public function widget( $args, $instance ) {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
|
||||
echo '<div class="widget w-search">';
|
||||
if ( $title ) {
|
||||
echo '<div class="title">'. $title .'</div>';
|
||||
}
|
||||
echo '<div class="item"> <form role="search" method="get" id="searchform" class="searchform" action="'. home_url('/') .'"> <div class="input-group mt-2 mb-2"> <input type="text" name="s" id="search" class="form-control" placeholder="'. __('搜点什么呢?', 'kratos') .'"> <div class="input-group-append"> <button class="btn btn-primary btn-search" type="submit" id="searchsubmit">'. __('搜索', 'kratos') .'</button> </div> </div> </form>';
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
public function form( $instance ) {
|
||||
$instance = wp_parse_args( (array) $instance, array( 'title' => '' ) );
|
||||
$title = $instance['title'];
|
||||
?>
|
||||
<p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" /></label></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update( $new_instance, $old_instance ) {
|
||||
$instance = $old_instance;
|
||||
$new_instance = wp_parse_args( (array) $new_instance, array( 'title' => '' ) );
|
||||
$instance['title'] = sanitize_text_field( $new_instance['title'] );
|
||||
return $instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class widget_ad extends WP_Widget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action('admin_enqueue_scripts', array($this, 'scripts'));
|
||||
|
||||
$widget_ops = array(
|
||||
'name' => __('图片广告', 'kratos'),
|
||||
'description' => __('显示自定义图片广告的工具', 'kratos'),
|
||||
);
|
||||
|
||||
parent::__construct(false, false, $widget_ops);
|
||||
}
|
||||
|
||||
public function scripts()
|
||||
{
|
||||
wp_enqueue_script('media-upload');
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery'));
|
||||
wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array());
|
||||
}
|
||||
|
||||
public function widget($args, $instance)
|
||||
{
|
||||
$subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos');
|
||||
$image = !empty($instance['image']) ? $instance['image'] : '';
|
||||
$url = !empty($instance['url']) ? $instance['url'] : '';
|
||||
|
||||
echo '<div class="widget w-ad">';
|
||||
echo '<a href="' . $url . '" target="_blank" rel="noreferrer"><img src="' . $image . '"><div class="prompt">' . $subtitle . '</div></a>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance)
|
||||
{
|
||||
$instance = array();
|
||||
|
||||
$instance['subtitle'] = (!empty($new_instance['subtitle'])) ? $new_instance['subtitle'] : '';
|
||||
$instance['image'] = (!empty($new_instance['image'])) ? $new_instance['image'] : '';
|
||||
$instance['url'] = (!empty($new_instance['url'])) ? $new_instance['url'] : '';
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function form($instance)
|
||||
{
|
||||
$subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos');
|
||||
$image = !empty($instance['image']) ? $instance['image'] : '';
|
||||
$url = !empty($instance['url']) ? $instance['url'] : '';
|
||||
?>
|
||||
<div class="media-widget-control">
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('subtitle'); ?>"><?php _e('副标题:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('subtitle'); ?>" name="<?php echo $this->get_field_name('subtitle'); ?>" type="text" value="<?php echo esc_attr($subtitle); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('url'); ?>"><?php _e('链接地址:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('url'); ?>" name="<?php echo $this->get_field_name('url'); ?>" type="text" value="<?php echo esc_attr($url); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('image'); ?>"><?php _e('广告图片:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('image'); ?>" name="<?php echo $this->get_field_name('image'); ?>" type="text" value="<?php echo esc_url($image); ?>" />
|
||||
<button type="button" class="button-update-media upload_ad"><?php _e('选择图片', 'kratos');?></button>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
class widget_about extends WP_Widget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action('admin_enqueue_scripts', array($this, 'scripts'));
|
||||
|
||||
$widget_ops = array(
|
||||
'name' => __('个人简介', 'kratos'),
|
||||
'description' => __('可跳转后台的个人简介展示工具', 'kratos'),
|
||||
);
|
||||
|
||||
parent::__construct(false, false, $widget_ops);
|
||||
}
|
||||
|
||||
public function scripts()
|
||||
{
|
||||
wp_enqueue_script('media-upload');
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery'));
|
||||
wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array());
|
||||
}
|
||||
|
||||
public function widget($args, $instance)
|
||||
{
|
||||
$introduce = kratos_option('a_about', __('保持饥渴的专注,追求最佳的品质', 'kratos'));
|
||||
$username = kratos_option('a_nickname', __('Kratos', 'kratos'));
|
||||
$avatar = kratos_option('a_gravatar', ASSET_PATH . '/assets/img/gravatar.png');
|
||||
$background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png';
|
||||
|
||||
echo '<div class="widget w-about">';
|
||||
echo '<div class="background" style="background:url(' . $background . ') no-repeat center center;-webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover;"></div><div class="wrapper text-center">';
|
||||
if (current_user_can('manage_options')) {
|
||||
echo '<a href="' . admin_url() . '">';
|
||||
} else {
|
||||
echo '<a href="' . wp_login_url() . '">';
|
||||
}
|
||||
echo '<img src="' . $avatar . '"></a>';
|
||||
echo '</div><div class="textwidget text-center"><p class="username">'. $username .'</p><p class="about">' . $introduce . '</p></div>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance)
|
||||
{
|
||||
$instance = array();
|
||||
|
||||
$instance['background'] = (!empty($new_instance['background'])) ? $new_instance['background'] : '';
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function form($instance)
|
||||
{
|
||||
$background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png';
|
||||
?>
|
||||
<div class="media-widget-control">
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('background'); ?>"><?php _e('背景图片:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('background'); ?>" name="<?php echo $this->get_field_name('background'); ?>" type="text" value="<?php echo esc_attr($background); ?>">
|
||||
<button type="button" class="button-update-media upload_background"><?php _e('选择图片', 'kratos');?></button>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
class widget_tags extends WP_Widget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$widget_ops = array(
|
||||
'name' => __('标签聚合', 'kratos'),
|
||||
'description' => __('文章标签的展示工具', 'kratos'),
|
||||
);
|
||||
|
||||
parent::__construct(false, false, $widget_ops);
|
||||
}
|
||||
|
||||
public function widget($args, $instance)
|
||||
{
|
||||
$number = !empty($instance['number']) ? $instance['number'] : '8';
|
||||
$order = !empty($instance['order']) ? $instance['order'] : 'RAND';
|
||||
$tags = wp_tag_cloud(array(
|
||||
'unit' => 'px',
|
||||
'smallest' => 14,
|
||||
'largest' => 14,
|
||||
'number' => $number,
|
||||
'format' => 'flat',
|
||||
'orderby' => 'count',
|
||||
'order' => $order,
|
||||
'echo' => false,
|
||||
)
|
||||
);
|
||||
echo '<div class="widget w-tags">';
|
||||
echo '<div class="title">' . __('标签聚合', 'kratos') . '</div>';
|
||||
echo '<div class="item">' . $tags . '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance)
|
||||
{
|
||||
$instance = array();
|
||||
|
||||
$instance['number'] = (!empty($new_instance['number'])) ? $new_instance['number'] : '';
|
||||
$instance['order'] = (!empty($new_instance['order'])) ? $new_instance['order'] : '';
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function form($instance)
|
||||
{
|
||||
global $wpdb;
|
||||
$number = !empty($instance['number']) ? $instance['number'] : '8';
|
||||
$order = !empty($instance['order']) ? $instance['order'] : 'RAND';
|
||||
?>
|
||||
<div class="media-widget-control">
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('number'); ?>"><?php _e('显示数量:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('number'); ?>" name="<?php echo $this->get_field_name('number'); ?>" type="text" value="<?php echo esc_attr($number); ?>" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('order'); ?>"><?php _e('显示排序:', 'kratos');?></label>
|
||||
<select name="<?php echo $this->get_field_name("order"); ?>" id='<?php echo $this->get_field_id("order"); ?>'>
|
||||
<option value="DESC" <?php echo ($order == 'DESC') ? 'selected' : ''; ?>><?php _e('降序', 'kratos');?></option>
|
||||
<option value="ASC" <?php echo ($order == 'ASC') ? 'selected' : ''; ?>><?php _e('升序', 'kratos');?></option>
|
||||
<option value="RAND" <?php echo ($order == 'RAND') ? 'selected' : ''; ?>><?php _e('随机', 'kratos');?></option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
class widget_posts extends WP_Widget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$widget_ops = array(
|
||||
'name' => __('文章聚合', 'kratos'),
|
||||
'description' => __('展示最热、随机、最新文章的工具', 'kratos'),
|
||||
);
|
||||
|
||||
parent::__construct(false, false, $widget_ops);
|
||||
}
|
||||
|
||||
public function widget($args, $instance)
|
||||
{
|
||||
$number = !empty($instance['number']) ? $instance['number'] : '6';
|
||||
$days = !empty($instance['days']) ? $instance['days'] : '30';
|
||||
|
||||
echo '<div class="widget w-recommended">';
|
||||
?>
|
||||
<div class="nav nav-tabs d-none d-xl-flex" id="nav-tab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="nav-new-tab" data-toggle="tab" href="#nav-new" role="tab" aria-controls="nav-new" aria-selected="false"><i class="kicon i-tabnew"></i><?php _e('最新', 'kratos');?></a>
|
||||
<a class="nav-item nav-link" id="nav-hot-tab" data-toggle="tab" href="#nav-hot" role="tab" aria-controls="nav-hot" aria-selected="true"><i class="kicon i-tabhot"></i><?php _e('热点', 'kratos');?></a>
|
||||
<a class="nav-item nav-link" id="nav-random-tab" data-toggle="tab" href="#nav-random" role="tab" aria-controls="nav-random" aria-selected="false"><i class="kicon i-tabrandom"></i><?php _e('随机', 'kratos');?></a>
|
||||
</div>
|
||||
<div class="nav nav-tabs d-xl-none" id="nav-tab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="nav-new-tab" data-toggle="tab" href="#nav-new" role="tab" aria-controls="nav-new" aria-selected="false"><?php _e('最新', 'kratos');?></a>
|
||||
<a class="nav-item nav-link" id="nav-hot-tab" data-toggle="tab" href="#nav-hot" role="tab" aria-controls="nav-hot" aria-selected="true"><?php _e('热点', 'kratos');?></a>
|
||||
<a class="nav-item nav-link" id="nav-random-tab" data-toggle="tab" href="#nav-random" role="tab" aria-controls="nav-random" aria-selected="false"><?php _e('随机', 'kratos');?></a>
|
||||
</div>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane fade show active" id="nav-new" role="tabpanel" aria-labelledby="nav-new-tab">
|
||||
<?php $myposts = get_posts('numberposts=' . $number . ' & offset=0');foreach ($myposts as $post): ?>
|
||||
<a class="bookmark-item" title="<?php echo $post->post_title; ?>" href="<?php echo get_permalink($post->ID); ?>" rel="bookmark"><i class="kicon i-book"></i><?php echo strip_tags($post->post_title) ?></a>
|
||||
<?php endforeach;?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-hot" role="tabpanel" aria-labelledby="nav-hot-tab">
|
||||
<?php if (function_exists('most_comm_posts')) {most_comm_posts($days, $number);}?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-random" role="tabpanel" aria-labelledby="nav-random-tab">
|
||||
<?php $myposts = get_posts('numberposts=' . $number . ' & offset=0 & orderby=rand');foreach ($myposts as $post): ?>
|
||||
<a class="bookmark-item" title="<?php echo $post->post_title; ?>" href="<?php echo get_permalink($post->ID); ?>" rel="bookmark"><i class="kicon i-book"></i><?php echo strip_tags($post->post_title) ?></a>
|
||||
<?php endforeach;?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo '</div>';
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance)
|
||||
{
|
||||
$instance = array();
|
||||
|
||||
$instance['number'] = (!empty($new_instance['number'])) ? $new_instance['number'] : '';
|
||||
$instance['days'] = (!empty($new_instance['days'])) ? $new_instance['days'] : '';
|
||||
|
||||
return $instance;
|
||||
}
|
||||
public function form($instance)
|
||||
{
|
||||
global $wpdb;
|
||||
$number = !empty($instance['number']) ? $instance['number'] : '6';
|
||||
$days = !empty($instance['days']) ? $instance['days'] : '30';
|
||||
?>
|
||||
<div class="media-widget-control">
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('number'); ?>"><?php _e('展示数量:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('number'); ?>" name="<?php echo $this->get_field_name('number'); ?>" type="text" value="<?php echo esc_attr($number); ?>" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('days'); ?>"><?php _e('统计天数:', 'kratos');?></label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('days'); ?>" name="<?php echo $this->get_field_name('days'); ?>" type="text" value="<?php echo esc_attr($days); ?>" />
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
function register_widgets()
|
||||
{
|
||||
register_widget('widget_ad');
|
||||
register_widget('widget_about');
|
||||
register_widget('widget_tags');
|
||||
register_widget('widget_search');
|
||||
register_widget('widget_posts');
|
||||
}
|
||||
add_action('widgets_init', 'register_widgets');
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4_Factory', false) ):
|
||||
|
||||
class Puc_v4_Factory extends Puc_v4p9_Factory { }
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Autoloader', false) ):
|
||||
|
||||
class Puc_v4p9_Autoloader {
|
||||
private $prefix = '';
|
||||
private $rootDir = '';
|
||||
private $libraryDir = '';
|
||||
|
||||
private $staticMap;
|
||||
|
||||
public function __construct() {
|
||||
$this->rootDir = dirname(__FILE__) . '/';
|
||||
$nameParts = explode('_', __CLASS__, 3);
|
||||
$this->prefix = $nameParts[0] . '_' . $nameParts[1] . '_';
|
||||
|
||||
$this->libraryDir = realpath($this->rootDir . '../..') . '/';
|
||||
$this->staticMap = array(
|
||||
'PucReadmeParser' => 'vendor/PucReadmeParser.php',
|
||||
'Parsedown' => 'vendor/Parsedown.php',
|
||||
'Puc_v4_Factory' => 'Puc/v4/Factory.php',
|
||||
);
|
||||
|
||||
spl_autoload_register(array($this, 'autoload'));
|
||||
}
|
||||
|
||||
public function autoload($className) {
|
||||
if ( isset($this->staticMap[$className]) && file_exists($this->libraryDir . $this->staticMap[$className]) ) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
include ($this->libraryDir . $this->staticMap[$className]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos($className, $this->prefix) === 0) {
|
||||
$path = substr($className, strlen($this->prefix));
|
||||
$path = str_replace('_', '/', $path);
|
||||
$path = $this->rootDir . $path . '.php';
|
||||
|
||||
if (file_exists($path)) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
include $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_DebugBar_Extension', false) ):
|
||||
|
||||
class Puc_v4p9_DebugBar_Extension {
|
||||
const RESPONSE_BODY_LENGTH_LIMIT = 4000;
|
||||
|
||||
/** @var Puc_v4p9_UpdateChecker */
|
||||
protected $updateChecker;
|
||||
protected $panelClass = 'Puc_v4p9_DebugBar_Panel';
|
||||
|
||||
public function __construct($updateChecker, $panelClass = null) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
if ( isset($panelClass) ) {
|
||||
$this->panelClass = $panelClass;
|
||||
}
|
||||
|
||||
add_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
|
||||
add_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
|
||||
|
||||
add_action('wp_ajax_puc_v4_debug_check_now', array($this, 'ajaxCheckNow'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the PUC Debug Bar panel.
|
||||
*
|
||||
* @param array $panels
|
||||
* @return array
|
||||
*/
|
||||
public function addDebugBarPanel($panels) {
|
||||
if ( $this->updateChecker->userCanInstallUpdates() ) {
|
||||
$panels[] = new $this->panelClass($this->updateChecker);
|
||||
}
|
||||
return $panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue our Debug Bar scripts and styles.
|
||||
*/
|
||||
public function enqueuePanelDependencies() {
|
||||
wp_enqueue_style(
|
||||
'puc-debug-bar-style-v4',
|
||||
$this->getLibraryUrl("/css/puc-debug-bar.css"),
|
||||
array('debug-bar'),
|
||||
'20171124'
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'puc-debug-bar-js-v4',
|
||||
$this->getLibraryUrl("/js/debug-bar.js"),
|
||||
array('jquery'),
|
||||
'20170516'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an update check and output the result. Useful for making sure that
|
||||
* the update checking process works as expected.
|
||||
*/
|
||||
public function ajaxCheckNow() {
|
||||
if ( $_POST['uid'] !== $this->updateChecker->getUniqueName('uid') ) {
|
||||
return;
|
||||
}
|
||||
$this->preAjaxRequest();
|
||||
$update = $this->updateChecker->checkForUpdates();
|
||||
if ( $update !== null ) {
|
||||
echo "An update is available:";
|
||||
echo '<pre>', htmlentities(print_r($update, true)), '</pre>';
|
||||
} else {
|
||||
echo 'No updates found.';
|
||||
}
|
||||
|
||||
$errors = $this->updateChecker->getLastRequestApiErrors();
|
||||
if ( !empty($errors) ) {
|
||||
printf('<p>The update checker encountered %d API error%s.</p>', count($errors), (count($errors) > 1) ? 's' : '');
|
||||
|
||||
foreach (array_values($errors) as $num => $item) {
|
||||
$wpError = $item['error'];
|
||||
/** @var WP_Error $wpError */
|
||||
printf('<h4>%d) %s</h4>', $num + 1, esc_html($wpError->get_error_message()));
|
||||
|
||||
echo '<dl>';
|
||||
printf('<dt>Error code:</dt><dd><code>%s</code></dd>', esc_html($wpError->get_error_code()));
|
||||
|
||||
if ( isset($item['url']) ) {
|
||||
printf('<dt>Requested URL:</dt><dd><code>%s</code></dd>', esc_html($item['url']));
|
||||
}
|
||||
|
||||
if ( isset($item['httpResponse']) ) {
|
||||
if ( is_wp_error($item['httpResponse']) ) {
|
||||
$httpError = $item['httpResponse'];
|
||||
/** @var WP_Error $httpError */
|
||||
printf(
|
||||
'<dt>WordPress HTTP API error:</dt><dd>%s (<code>%s</code>)</dd>',
|
||||
esc_html($httpError->get_error_message()),
|
||||
esc_html($httpError->get_error_code())
|
||||
);
|
||||
} else {
|
||||
//Status code.
|
||||
printf(
|
||||
'<dt>HTTP status:</dt><dd><code>%d %s</code></dd>',
|
||||
wp_remote_retrieve_response_code($item['httpResponse']),
|
||||
wp_remote_retrieve_response_message($item['httpResponse'])
|
||||
);
|
||||
|
||||
//Headers.
|
||||
echo '<dt>Response headers:</dt><dd><pre>';
|
||||
foreach (wp_remote_retrieve_headers($item['httpResponse']) as $name => $value) {
|
||||
printf("%s: %s\n", esc_html($name), esc_html($value));
|
||||
}
|
||||
echo '</pre></dd>';
|
||||
|
||||
//Body.
|
||||
$body = wp_remote_retrieve_body($item['httpResponse']);
|
||||
if ( $body === '' ) {
|
||||
$body = '(Empty response.)';
|
||||
} else if ( strlen($body) > self::RESPONSE_BODY_LENGTH_LIMIT ) {
|
||||
$length = strlen($body);
|
||||
$body = substr($body, 0, self::RESPONSE_BODY_LENGTH_LIMIT)
|
||||
. sprintf("\n(Long string truncated. Total length: %d bytes.)", $length);
|
||||
}
|
||||
|
||||
printf('<dt>Response body:</dt><dd><pre>%s</pre></dd>', esc_html($body));
|
||||
}
|
||||
}
|
||||
echo '<dl>';
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access permissions and enable error display (for debugging).
|
||||
*/
|
||||
protected function preAjaxRequest() {
|
||||
if ( !$this->updateChecker->userCanInstallUpdates() ) {
|
||||
die('Access denied');
|
||||
}
|
||||
check_ajax_referer('puc-ajax');
|
||||
|
||||
error_reporting(E_ALL);
|
||||
@ini_set('display_errors', 'On');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
private function getLibraryUrl($filePath) {
|
||||
$absolutePath = realpath(dirname(__FILE__) . '/../../../' . ltrim($filePath, '/'));
|
||||
|
||||
//Where is the library located inside the WordPress directory structure?
|
||||
$absolutePath = Puc_v4p9_Factory::normalizePath($absolutePath);
|
||||
|
||||
$pluginDir = Puc_v4p9_Factory::normalizePath(WP_PLUGIN_DIR);
|
||||
$muPluginDir = Puc_v4p9_Factory::normalizePath(WPMU_PLUGIN_DIR);
|
||||
$themeDir = Puc_v4p9_Factory::normalizePath(get_theme_root());
|
||||
|
||||
if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
|
||||
//It's part of a plugin.
|
||||
return plugins_url(basename($absolutePath), $absolutePath);
|
||||
} else if ( strpos($absolutePath, $themeDir) === 0 ) {
|
||||
//It's part of a theme.
|
||||
$relativePath = substr($absolutePath, strlen($themeDir) + 1);
|
||||
$template = substr($relativePath, 0, strpos($relativePath, '/'));
|
||||
$baseUrl = get_theme_root_uri($template);
|
||||
|
||||
if ( !empty($baseUrl) && $relativePath ) {
|
||||
return $baseUrl . '/' . $relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_DebugBar_Panel', false) && class_exists('Debug_Bar_Panel', false) ):
|
||||
|
||||
class Puc_v4p9_DebugBar_Panel extends Debug_Bar_Panel {
|
||||
/** @var Puc_v4p9_UpdateChecker */
|
||||
protected $updateChecker;
|
||||
|
||||
private $responseBox = '<div class="puc-ajax-response" style="display: none;"></div>';
|
||||
|
||||
public function __construct($updateChecker) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
$title = sprintf(
|
||||
'<span class="puc-debug-menu-link-%s">PUC (%s)</span>',
|
||||
esc_attr($this->updateChecker->getUniqueName('uid')),
|
||||
$this->updateChecker->slug
|
||||
);
|
||||
parent::__construct($title);
|
||||
}
|
||||
|
||||
public function render() {
|
||||
printf(
|
||||
'<div class="puc-debug-bar-panel-v4" id="%1$s" data-slug="%2$s" data-uid="%3$s" data-nonce="%4$s">',
|
||||
esc_attr($this->updateChecker->getUniqueName('debug-bar-panel')),
|
||||
esc_attr($this->updateChecker->slug),
|
||||
esc_attr($this->updateChecker->getUniqueName('uid')),
|
||||
esc_attr(wp_create_nonce('puc-ajax'))
|
||||
);
|
||||
|
||||
$this->displayConfiguration();
|
||||
$this->displayStatus();
|
||||
$this->displayCurrentUpdate();
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function displayConfiguration() {
|
||||
echo '<h3>Configuration</h3>';
|
||||
echo '<table class="puc-debug-data">';
|
||||
$this->displayConfigHeader();
|
||||
$this->row('Slug', htmlentities($this->updateChecker->slug));
|
||||
$this->row('DB option', htmlentities($this->updateChecker->optionName));
|
||||
|
||||
$requestInfoButton = $this->getMetadataButton();
|
||||
$this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
|
||||
|
||||
$scheduler = $this->updateChecker->scheduler;
|
||||
if ( $scheduler->checkPeriod > 0 ) {
|
||||
$this->row('Automatic checks', 'Every ' . $scheduler->checkPeriod . ' hours');
|
||||
} else {
|
||||
$this->row('Automatic checks', 'Disabled');
|
||||
}
|
||||
|
||||
if ( isset($scheduler->throttleRedundantChecks) ) {
|
||||
if ( $scheduler->throttleRedundantChecks && ($scheduler->checkPeriod > 0) ) {
|
||||
$this->row(
|
||||
'Throttling',
|
||||
sprintf(
|
||||
'Enabled. If an update is already available, check for updates every %1$d hours instead of every %2$d hours.',
|
||||
$scheduler->throttledCheckPeriod,
|
||||
$scheduler->checkPeriod
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$this->row('Throttling', 'Disabled');
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateChecker->onDisplayConfiguration($this);
|
||||
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
protected function displayConfigHeader() {
|
||||
//Do nothing. This should be implemented in subclasses.
|
||||
}
|
||||
|
||||
protected function getMetadataButton() {
|
||||
return '';
|
||||
}
|
||||
|
||||
private function displayStatus() {
|
||||
echo '<h3>Status</h3>';
|
||||
echo '<table class="puc-debug-data">';
|
||||
$state = $this->updateChecker->getUpdateState();
|
||||
$checkNowButton = '';
|
||||
if ( function_exists('get_submit_button') ) {
|
||||
$checkNowButton = get_submit_button(
|
||||
'Check Now',
|
||||
'secondary',
|
||||
'puc-check-now-button',
|
||||
false,
|
||||
array('id' => $this->updateChecker->getUniqueName('check-now-button'))
|
||||
);
|
||||
}
|
||||
|
||||
if ( $state->getLastCheck() > 0 ) {
|
||||
$this->row('Last check', $this->formatTimeWithDelta($state->getLastCheck()) . ' ' . $checkNowButton . $this->responseBox);
|
||||
} else {
|
||||
$this->row('Last check', 'Never');
|
||||
}
|
||||
|
||||
$nextCheck = wp_next_scheduled($this->updateChecker->scheduler->getCronHookName());
|
||||
$this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
|
||||
|
||||
if ( $state->getCheckedVersion() !== '' ) {
|
||||
$this->row('Checked version', htmlentities($state->getCheckedVersion()));
|
||||
$this->row('Cached update', $state->getUpdate());
|
||||
}
|
||||
$this->row('Update checker class', htmlentities(get_class($this->updateChecker)));
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
private function displayCurrentUpdate() {
|
||||
$update = $this->updateChecker->getUpdate();
|
||||
if ( $update !== null ) {
|
||||
echo '<h3>An Update Is Available</h3>';
|
||||
echo '<table class="puc-debug-data">';
|
||||
$fields = $this->getUpdateFields();
|
||||
foreach($fields as $field) {
|
||||
if ( property_exists($update, $field) ) {
|
||||
$this->row(ucwords(str_replace('_', ' ', $field)), htmlentities($update->$field));
|
||||
}
|
||||
}
|
||||
echo '</table>';
|
||||
} else {
|
||||
echo '<h3>No updates currently available</h3>';
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUpdateFields() {
|
||||
return array('version', 'download_url', 'slug',);
|
||||
}
|
||||
|
||||
private function formatTimeWithDelta($unixTime) {
|
||||
if ( empty($unixTime) ) {
|
||||
return 'Never';
|
||||
}
|
||||
|
||||
$delta = time() - $unixTime;
|
||||
$result = human_time_diff(time(), $unixTime);
|
||||
if ( $delta < 0 ) {
|
||||
$result = 'after ' . $result;
|
||||
} else {
|
||||
$result = $result . ' ago';
|
||||
}
|
||||
$result .= ' (' . $this->formatTimestamp($unixTime) . ')';
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function formatTimestamp($unixTime) {
|
||||
return gmdate('Y-m-d H:i:s', $unixTime + (get_option('gmt_offset') * 3600));
|
||||
}
|
||||
|
||||
public function row($name, $value) {
|
||||
if ( is_object($value) || is_array($value) ) {
|
||||
$value = '<pre>' . htmlentities(print_r($value, true)) . '</pre>';
|
||||
} else if ($value === null) {
|
||||
$value = '<code>null</code>';
|
||||
}
|
||||
printf('<tr><th scope="row">%1$s</th> <td>%2$s</td></tr>', $name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_DebugBar_PluginExtension', false) ):
|
||||
|
||||
class Puc_v4p9_DebugBar_PluginExtension extends Puc_v4p9_DebugBar_Extension {
|
||||
/** @var Puc_v4p9_Plugin_UpdateChecker */
|
||||
protected $updateChecker;
|
||||
|
||||
public function __construct($updateChecker) {
|
||||
parent::__construct($updateChecker, 'Puc_v4p9_DebugBar_PluginPanel');
|
||||
|
||||
add_action('wp_ajax_puc_v4_debug_request_info', array($this, 'ajaxRequestInfo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request plugin info and output it.
|
||||
*/
|
||||
public function ajaxRequestInfo() {
|
||||
if ( $_POST['uid'] !== $this->updateChecker->getUniqueName('uid') ) {
|
||||
return;
|
||||
}
|
||||
$this->preAjaxRequest();
|
||||
$info = $this->updateChecker->requestInfo();
|
||||
if ( $info !== null ) {
|
||||
echo 'Successfully retrieved plugin info from the metadata URL:';
|
||||
echo '<pre>', htmlentities(print_r($info, true)), '</pre>';
|
||||
} else {
|
||||
echo 'Failed to retrieve plugin info from the metadata URL.';
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_DebugBar_PluginPanel', false) ):
|
||||
|
||||
class Puc_v4p9_DebugBar_PluginPanel extends Puc_v4p9_DebugBar_Panel {
|
||||
/**
|
||||
* @var Puc_v4p9_Plugin_UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
protected function displayConfigHeader() {
|
||||
$this->row('Plugin file', htmlentities($this->updateChecker->pluginFile));
|
||||
parent::displayConfigHeader();
|
||||
}
|
||||
|
||||
protected function getMetadataButton() {
|
||||
$requestInfoButton = '';
|
||||
if ( function_exists('get_submit_button') ) {
|
||||
$requestInfoButton = get_submit_button(
|
||||
'Request Info',
|
||||
'secondary',
|
||||
'puc-request-info-button',
|
||||
false,
|
||||
array('id' => $this->updateChecker->getUniqueName('request-info-button'))
|
||||
);
|
||||
}
|
||||
return $requestInfoButton;
|
||||
}
|
||||
|
||||
protected function getUpdateFields() {
|
||||
return array_merge(
|
||||
parent::getUpdateFields(),
|
||||
array('homepage', 'upgrade_notice', 'tested',)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_DebugBar_ThemePanel', false) ):
|
||||
|
||||
class Puc_v4p9_DebugBar_ThemePanel extends Puc_v4p9_DebugBar_Panel {
|
||||
/**
|
||||
* @var Puc_v4p9_Theme_UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
protected function displayConfigHeader() {
|
||||
$this->row('Theme directory', htmlentities($this->updateChecker->directoryName));
|
||||
parent::displayConfigHeader();
|
||||
}
|
||||
|
||||
protected function getUpdateFields() {
|
||||
return array_merge(parent::getUpdateFields(), array('details_url'));
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Factory', false) ):
|
||||
|
||||
/**
|
||||
* A factory that builds update checker instances.
|
||||
*
|
||||
* When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 4.0
|
||||
* and 4.1), this factory will always use the latest available minor version. Register class
|
||||
* versions by calling {@link PucFactory::addVersion()}.
|
||||
*
|
||||
* At the moment it can only build instances of the UpdateChecker class. Other classes are
|
||||
* intended mainly for internal use and refer directly to specific implementations.
|
||||
*/
|
||||
class Puc_v4p9_Factory {
|
||||
protected static $classVersions = array();
|
||||
protected static $sorted = false;
|
||||
|
||||
protected static $myMajorVersion = '';
|
||||
protected static $latestCompatibleVersion = '';
|
||||
|
||||
/**
|
||||
* Create a new instance of the update checker.
|
||||
*
|
||||
* This method automatically detects if you're using it for a plugin or a theme and chooses
|
||||
* the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
|
||||
*
|
||||
* @see Puc_v4p9_UpdateChecker::__construct
|
||||
*
|
||||
* @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
|
||||
* @param string $fullPath Full path to the main plugin file or to the theme directory.
|
||||
* @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
|
||||
* @param int $checkPeriod How often to check for updates (in hours).
|
||||
* @param string $optionName Where to store book-keeping info about update checks.
|
||||
* @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
|
||||
* @return Puc_v4p9_Plugin_UpdateChecker|Puc_v4p9_Theme_UpdateChecker|Puc_v4p9_Vcs_BaseChecker
|
||||
*/
|
||||
public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
|
||||
$fullPath = self::normalizePath($fullPath);
|
||||
$id = null;
|
||||
|
||||
//Plugin or theme?
|
||||
$themeDirectory = self::getThemeDirectoryName($fullPath);
|
||||
if ( self::isPluginFile($fullPath) ) {
|
||||
$type = 'Plugin';
|
||||
$id = $fullPath;
|
||||
} else if ( $themeDirectory !== null ) {
|
||||
$type = 'Theme';
|
||||
$id = $themeDirectory;
|
||||
} else {
|
||||
throw new RuntimeException(sprintf(
|
||||
'The update checker cannot determine if "%s" is a plugin or a theme. ' .
|
||||
'This is a bug. Please contact the PUC developer.',
|
||||
htmlentities($fullPath)
|
||||
));
|
||||
}
|
||||
|
||||
//Which hosting service does the URL point to?
|
||||
$service = self::getVcsService($metadataUrl);
|
||||
|
||||
$apiClass = null;
|
||||
if ( empty($service) ) {
|
||||
//The default is to get update information from a remote JSON file.
|
||||
$checkerClass = $type . '_UpdateChecker';
|
||||
} else {
|
||||
//You can also use a VCS repository like GitHub.
|
||||
$checkerClass = 'Vcs_' . $type . 'UpdateChecker';
|
||||
$apiClass = $service . 'Api';
|
||||
}
|
||||
|
||||
$checkerClass = self::getCompatibleClassVersion($checkerClass);
|
||||
if ( $checkerClass === null ) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'PUC %s does not support updates for %ss %s',
|
||||
htmlentities(self::$latestCompatibleVersion),
|
||||
strtolower($type),
|
||||
$service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
|
||||
),
|
||||
E_USER_ERROR
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( !isset($apiClass) ) {
|
||||
//Plain old update checker.
|
||||
return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
|
||||
} else {
|
||||
//VCS checker + an API client.
|
||||
$apiClass = self::getCompatibleClassVersion($apiClass);
|
||||
if ( $apiClass === null ) {
|
||||
trigger_error(sprintf(
|
||||
'PUC %s does not support %s',
|
||||
htmlentities(self::$latestCompatibleVersion),
|
||||
htmlentities($service)
|
||||
), E_USER_ERROR);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new $checkerClass(
|
||||
new $apiClass($metadataUrl),
|
||||
$id,
|
||||
$slug,
|
||||
$checkPeriod,
|
||||
$optionName,
|
||||
$muPluginFile
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Normalize a filesystem path. Introduced in WP 3.9.
|
||||
* Copying here allows use of the class on earlier versions.
|
||||
* This version adapted from WP 4.8.2 (unchanged since 4.5.0)
|
||||
*
|
||||
* @param string $path Path to normalize.
|
||||
* @return string Normalized path.
|
||||
*/
|
||||
public static function normalizePath($path) {
|
||||
if ( function_exists('wp_normalize_path') ) {
|
||||
return wp_normalize_path($path);
|
||||
}
|
||||
$path = str_replace('\\', '/', $path);
|
||||
$path = preg_replace('|(?<=.)/+|', '/', $path);
|
||||
if ( substr($path, 1, 1) === ':' ) {
|
||||
$path = ucfirst($path);
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the path points to a plugin file.
|
||||
*
|
||||
* @param string $absolutePath Normalized path.
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isPluginFile($absolutePath) {
|
||||
//Is the file inside the "plugins" or "mu-plugins" directory?
|
||||
$pluginDir = self::normalizePath(WP_PLUGIN_DIR);
|
||||
$muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
|
||||
if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
|
||||
if ( !is_file($absolutePath) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//Does it have a valid plugin header?
|
||||
//This is a last-ditch check for plugins symlinked from outside the WP root.
|
||||
if ( function_exists('get_file_data') ) {
|
||||
$headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
|
||||
return !empty($headers['Name']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the theme's directory from a full path to a file inside that directory.
|
||||
* E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
|
||||
*
|
||||
* Note that subdirectories are currently not supported. For example,
|
||||
* "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
|
||||
*
|
||||
* @param string $absolutePath Normalized path.
|
||||
* @return string|null Directory name, or NULL if the path doesn't point to a theme.
|
||||
*/
|
||||
protected static function getThemeDirectoryName($absolutePath) {
|
||||
if ( is_file($absolutePath) ) {
|
||||
$absolutePath = dirname($absolutePath);
|
||||
}
|
||||
|
||||
if ( file_exists($absolutePath . '/style.css') ) {
|
||||
return basename($absolutePath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the hosting service that the URL points to.
|
||||
*
|
||||
* @param string $metadataUrl
|
||||
* @return string|null
|
||||
*/
|
||||
private static function getVcsService($metadataUrl) {
|
||||
$service = null;
|
||||
|
||||
//Which hosting service does the URL point to?
|
||||
$host = parse_url($metadataUrl, PHP_URL_HOST);
|
||||
$path = parse_url($metadataUrl, PHP_URL_PATH);
|
||||
|
||||
//Check if the path looks like "/user-name/repository".
|
||||
//For GitLab.com it can also be "/user/group1/group2/.../repository".
|
||||
$repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
|
||||
if ( $host === 'gitlab.com' ) {
|
||||
$repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
|
||||
}
|
||||
if ( preg_match($repoRegex, $path) ) {
|
||||
$knownServices = array(
|
||||
'github.com' => 'GitHub',
|
||||
'bitbucket.org' => 'BitBucket',
|
||||
'gitlab.com' => 'GitLab',
|
||||
);
|
||||
if ( isset($knownServices[$host]) ) {
|
||||
$service = $knownServices[$host];
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version of the specified class that has the same major version number
|
||||
* as this factory class.
|
||||
*
|
||||
* @param string $class Partial class name.
|
||||
* @return string|null Full class name.
|
||||
*/
|
||||
protected static function getCompatibleClassVersion($class) {
|
||||
if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
|
||||
return self::$classVersions[$class][self::$latestCompatibleVersion];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specific class name for the latest available version of a class.
|
||||
*
|
||||
* @param string $class
|
||||
* @return null|string
|
||||
*/
|
||||
public static function getLatestClassVersion($class) {
|
||||
if ( !self::$sorted ) {
|
||||
self::sortVersions();
|
||||
}
|
||||
|
||||
if ( isset(self::$classVersions[$class]) ) {
|
||||
return reset(self::$classVersions[$class]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort available class versions in descending order (i.e. newest first).
|
||||
*/
|
||||
protected static function sortVersions() {
|
||||
foreach ( self::$classVersions as $class => $versions ) {
|
||||
uksort($versions, array(__CLASS__, 'compareVersions'));
|
||||
self::$classVersions[$class] = $versions;
|
||||
}
|
||||
self::$sorted = true;
|
||||
}
|
||||
|
||||
protected static function compareVersions($a, $b) {
|
||||
return -version_compare($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a version of a class.
|
||||
*
|
||||
* @access private This method is only for internal use by the library.
|
||||
*
|
||||
* @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
|
||||
* @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
|
||||
* @param string $version Version number, e.g. '1.2'.
|
||||
*/
|
||||
public static function addVersion($generalClass, $versionedClass, $version) {
|
||||
if ( empty(self::$myMajorVersion) ) {
|
||||
$nameParts = explode('_', __CLASS__, 3);
|
||||
self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
|
||||
}
|
||||
|
||||
//Store the greatest version number that matches our major version.
|
||||
$components = explode('.', $version);
|
||||
if ( $components[0] === self::$myMajorVersion ) {
|
||||
|
||||
if (
|
||||
empty(self::$latestCompatibleVersion)
|
||||
|| version_compare($version, self::$latestCompatibleVersion, '>')
|
||||
) {
|
||||
self::$latestCompatibleVersion = $version;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( !isset(self::$classVersions[$generalClass]) ) {
|
||||
self::$classVersions[$generalClass] = array();
|
||||
}
|
||||
self::$classVersions[$generalClass][$version] = $versionedClass;
|
||||
self::$sorted = false;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_InstalledPackage', false) ):
|
||||
|
||||
/**
|
||||
* This class represents a currently installed plugin or theme.
|
||||
*
|
||||
* Not to be confused with the "package" field in WP update API responses that contains
|
||||
* the download URL of a the new version.
|
||||
*/
|
||||
abstract class Puc_v4p9_InstalledPackage {
|
||||
/**
|
||||
* @var Puc_v4p9_UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
public function __construct($updateChecker) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently installed version of the plugin or theme.
|
||||
*
|
||||
* @return string|null Version number.
|
||||
*/
|
||||
abstract public function getInstalledVersion();
|
||||
|
||||
/**
|
||||
* Get the full path of the plugin or theme directory (without a trailing slash).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getAbsoluteDirectoryPath();
|
||||
|
||||
/**
|
||||
* Check whether a regular file exists in the package's directory.
|
||||
*
|
||||
* @param string $relativeFileName File name relative to the package directory.
|
||||
* @return bool
|
||||
*/
|
||||
public function fileExists($relativeFileName) {
|
||||
return is_file(
|
||||
$this->getAbsoluteDirectoryPath()
|
||||
. DIRECTORY_SEPARATOR
|
||||
. ltrim($relativeFileName, '/\\')
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* File header parsing
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse plugin or theme metadata from the header comment.
|
||||
*
|
||||
* This is basically a simplified version of the get_file_data() function from /wp-includes/functions.php.
|
||||
* It's intended as a utility for subclasses that detect updates by parsing files in a VCS.
|
||||
*
|
||||
* @param string|null $content File contents.
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFileHeader($content) {
|
||||
$content = (string)$content;
|
||||
|
||||
//WordPress only looks at the first 8 KiB of the file, so we do the same.
|
||||
$content = substr($content, 0, 8192);
|
||||
//Normalize line endings.
|
||||
$content = str_replace("\r", "\n", $content);
|
||||
|
||||
$headers = $this->getHeaderNames();
|
||||
$results = array();
|
||||
foreach ($headers as $field => $name) {
|
||||
$success = preg_match('/^[ \t\/*#@]*' . preg_quote($name, '/') . ':(.*)$/mi', $content, $matches);
|
||||
|
||||
if ( ($success === 1) && $matches[1] ) {
|
||||
$value = $matches[1];
|
||||
if ( function_exists('_cleanup_header_comment') ) {
|
||||
$value = _cleanup_header_comment($value);
|
||||
}
|
||||
$results[$field] = $value;
|
||||
} else {
|
||||
$results[$field] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Format: ['HeaderKey' => 'Header Name']
|
||||
*/
|
||||
abstract protected function getHeaderNames();
|
||||
|
||||
/**
|
||||
* Get the value of a specific plugin or theme header.
|
||||
*
|
||||
* @param string $headerName
|
||||
* @return string Either the value of the header, or an empty string if the header doesn't exist.
|
||||
*/
|
||||
abstract public function getHeaderValue($headerName);
|
||||
|
||||
}
|
||||
endif;
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Metadata', false) ):
|
||||
|
||||
/**
|
||||
* A base container for holding information about updates and plugin metadata.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2016
|
||||
* @access public
|
||||
*/
|
||||
abstract class Puc_v4p9_Metadata {
|
||||
|
||||
/**
|
||||
* Create an instance of this class from a JSON document.
|
||||
*
|
||||
* @abstract
|
||||
* @param string $json
|
||||
* @return self
|
||||
*/
|
||||
public static function fromJson(/** @noinspection PhpUnusedParameterInspection */ $json) {
|
||||
throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $json
|
||||
* @param self $target
|
||||
* @return bool
|
||||
*/
|
||||
protected static function createFromJson($json, $target) {
|
||||
/** @var StdClass $apiResponse */
|
||||
$apiResponse = json_decode($json);
|
||||
if ( empty($apiResponse) || !is_object($apiResponse) ){
|
||||
$errorMessage = "Failed to parse update metadata. Try validating your .json file with http://jsonlint.com/";
|
||||
do_action('puc_api_error', new WP_Error('puc-invalid-json', $errorMessage));
|
||||
trigger_error($errorMessage, E_USER_NOTICE);
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid = $target->validateMetadata($apiResponse);
|
||||
if ( is_wp_error($valid) ){
|
||||
do_action('puc_api_error', $valid);
|
||||
trigger_error($valid->get_error_message(), E_USER_NOTICE);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach(get_object_vars($apiResponse) as $key => $value){
|
||||
$target->$key = $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* No validation by default! Subclasses should check that the required fields are present.
|
||||
*
|
||||
* @param StdClass $apiResponse
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function validateMetadata(/** @noinspection PhpUnusedParameterInspection */ $apiResponse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance by copying the necessary fields from another object.
|
||||
*
|
||||
* @abstract
|
||||
* @param StdClass|self $object The source object.
|
||||
* @return self The new copy.
|
||||
*/
|
||||
public static function fromObject(/** @noinspection PhpUnusedParameterInspection */ $object) {
|
||||
throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of StdClass that can later be converted back to an
|
||||
* update or info container. Useful for serialization and caching, as it
|
||||
* avoids the "incomplete object" problem if the cached value is loaded
|
||||
* before this class.
|
||||
*
|
||||
* @return StdClass
|
||||
*/
|
||||
public function toStdClass() {
|
||||
$object = new stdClass();
|
||||
$this->copyFields($this, $object);
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the metadata into the format used by WordPress core.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
abstract public function toWpFormat();
|
||||
|
||||
/**
|
||||
* Copy known fields from one object to another.
|
||||
*
|
||||
* @param StdClass|self $from
|
||||
* @param StdClass|self $to
|
||||
*/
|
||||
protected function copyFields($from, $to) {
|
||||
$fields = $this->getFieldNames();
|
||||
|
||||
if ( property_exists($from, 'slug') && !empty($from->slug) ) {
|
||||
//Let plugins add extra fields without having to create subclasses.
|
||||
$fields = apply_filters($this->getPrefixedFilter('retain_fields') . '-' . $from->slug, $fields);
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ( property_exists($from, $field) ) {
|
||||
$to->$field = $from->$field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFieldNames() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tag
|
||||
* @return string
|
||||
*/
|
||||
protected function getPrefixedFilter($tag) {
|
||||
return 'puc_' . $tag;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_OAuthSignature', false) ):
|
||||
|
||||
/**
|
||||
* A basic signature generator for zero-legged OAuth 1.0.
|
||||
*/
|
||||
class Puc_v4p9_OAuthSignature {
|
||||
private $consumerKey = '';
|
||||
private $consumerSecret = '';
|
||||
|
||||
public function __construct($consumerKey, $consumerSecret) {
|
||||
$this->consumerKey = $consumerKey;
|
||||
$this->consumerSecret = $consumerSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a URL using OAuth 1.0.
|
||||
*
|
||||
* @param string $url The URL to be signed. It may contain query parameters.
|
||||
* @param string $method HTTP method such as "GET", "POST" and so on.
|
||||
* @return string The signed URL.
|
||||
*/
|
||||
public function sign($url, $method = 'GET') {
|
||||
$parameters = array();
|
||||
|
||||
//Parse query parameters.
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
if ( !empty($query) ) {
|
||||
parse_str($query, $parsedParams);
|
||||
if ( is_array($parameters) ) {
|
||||
$parameters = $parsedParams;
|
||||
}
|
||||
//Remove the query string from the URL. We'll replace it later.
|
||||
$url = substr($url, 0, strpos($url, '?'));
|
||||
}
|
||||
|
||||
$parameters = array_merge(
|
||||
$parameters,
|
||||
array(
|
||||
'oauth_consumer_key' => $this->consumerKey,
|
||||
'oauth_nonce' => $this->nonce(),
|
||||
'oauth_signature_method' => 'HMAC-SHA1',
|
||||
'oauth_timestamp' => time(),
|
||||
'oauth_version' => '1.0',
|
||||
)
|
||||
);
|
||||
unset($parameters['oauth_signature']);
|
||||
|
||||
//Parameters must be sorted alphabetically before signing.
|
||||
ksort($parameters);
|
||||
|
||||
//The most complicated part of the request - generating the signature.
|
||||
//The string to sign contains the HTTP method, the URL path, and all of
|
||||
//our query parameters. Everything is URL encoded. Then we concatenate
|
||||
//them with ampersands into a single string to hash.
|
||||
$encodedVerb = urlencode($method);
|
||||
$encodedUrl = urlencode($url);
|
||||
$encodedParams = urlencode(http_build_query($parameters, '', '&'));
|
||||
|
||||
$stringToSign = $encodedVerb . '&' . $encodedUrl . '&' . $encodedParams;
|
||||
|
||||
//Since we only have one OAuth token (the consumer secret) we only have
|
||||
//to use it as our HMAC key. However, we still have to append an & to it
|
||||
//as if we were using it with additional tokens.
|
||||
$secret = urlencode($this->consumerSecret) . '&';
|
||||
|
||||
//The signature is a hash of the consumer key and the base string. Note
|
||||
//that we have to get the raw output from hash_hmac and base64 encode
|
||||
//the binary data result.
|
||||
$parameters['oauth_signature'] = base64_encode(hash_hmac('sha1', $stringToSign, $secret, true));
|
||||
|
||||
return ($url . '?' . http_build_query($parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random nonce.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function nonce() {
|
||||
$mt = microtime();
|
||||
|
||||
$rand = null;
|
||||
if ( is_callable('random_bytes') ) {
|
||||
try {
|
||||
$rand = random_bytes(16);
|
||||
} catch (Exception $ex) {
|
||||
//Fall back to mt_rand (below).
|
||||
}
|
||||
}
|
||||
if ( $rand === null ) {
|
||||
$rand = mt_rand();
|
||||
}
|
||||
|
||||
return md5($mt . '_' . $rand);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Plugin_Info', false) ):
|
||||
|
||||
/**
|
||||
* A container class for holding and transforming various plugin metadata.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2016
|
||||
* @access public
|
||||
*/
|
||||
class Puc_v4p9_Plugin_Info extends Puc_v4p9_Metadata {
|
||||
//Most fields map directly to the contents of the plugin's info.json file.
|
||||
//See the relevant docs for a description of their meaning.
|
||||
public $name;
|
||||
public $slug;
|
||||
public $version;
|
||||
public $homepage;
|
||||
public $sections = array();
|
||||
public $download_url;
|
||||
|
||||
public $banners;
|
||||
public $icons = array();
|
||||
public $translations = array();
|
||||
|
||||
public $author;
|
||||
public $author_homepage;
|
||||
|
||||
public $requires;
|
||||
public $tested;
|
||||
public $upgrade_notice;
|
||||
|
||||
public $rating;
|
||||
public $num_ratings;
|
||||
public $downloaded;
|
||||
public $active_installs;
|
||||
public $last_updated;
|
||||
|
||||
public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
|
||||
|
||||
public $filename; //Plugin filename relative to the plugins directory.
|
||||
|
||||
/**
|
||||
* Create a new instance of Plugin Info from JSON-encoded plugin info
|
||||
* returned by an external update API.
|
||||
*
|
||||
* @param string $json Valid JSON string representing plugin info.
|
||||
* @return self|null New instance of Plugin Info, or NULL on error.
|
||||
*/
|
||||
public static function fromJson($json){
|
||||
$instance = new self();
|
||||
|
||||
if ( !parent::createFromJson($json, $instance) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//json_decode decodes assoc. arrays as objects. We want them as arrays.
|
||||
$instance->sections = (array)$instance->sections;
|
||||
$instance->icons = (array)$instance->icons;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Very, very basic validation.
|
||||
*
|
||||
* @param StdClass $apiResponse
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function validateMetadata($apiResponse) {
|
||||
if (
|
||||
!isset($apiResponse->name, $apiResponse->version)
|
||||
|| empty($apiResponse->name)
|
||||
|| empty($apiResponse->version)
|
||||
) {
|
||||
return new WP_Error(
|
||||
'puc-invalid-metadata',
|
||||
"The plugin metadata file does not contain the required 'name' and/or 'version' keys."
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transform plugin info into the format used by the native WordPress.org API
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function toWpFormat(){
|
||||
$info = new stdClass;
|
||||
|
||||
//The custom update API is built so that many fields have the same name and format
|
||||
//as those returned by the native WordPress.org API. These can be assigned directly.
|
||||
$sameFormat = array(
|
||||
'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',
|
||||
'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',
|
||||
);
|
||||
foreach($sameFormat as $field){
|
||||
if ( isset($this->$field) ) {
|
||||
$info->$field = $this->$field;
|
||||
} else {
|
||||
$info->$field = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Other fields need to be renamed and/or transformed.
|
||||
$info->download_link = $this->download_url;
|
||||
$info->author = $this->getFormattedAuthor();
|
||||
$info->sections = array_merge(array('description' => ''), $this->sections);
|
||||
|
||||
if ( !empty($this->banners) ) {
|
||||
//WP expects an array with two keys: "high" and "low". Both are optional.
|
||||
//Docs: https://wordpress.org/plugins/about/faq/#banners
|
||||
$info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;
|
||||
$info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
protected function getFormattedAuthor() {
|
||||
if ( !empty($this->author_homepage) ){
|
||||
/** @noinspection HtmlUnknownTarget */
|
||||
return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);
|
||||
}
|
||||
return $this->author;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Plugin_Package', false) ):
|
||||
|
||||
class Puc_v4p9_Plugin_Package extends Puc_v4p9_InstalledPackage {
|
||||
/**
|
||||
* @var Puc_v4p9_Plugin_UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
/**
|
||||
* @var string Full path of the main plugin file.
|
||||
*/
|
||||
protected $pluginAbsolutePath = '';
|
||||
|
||||
/**
|
||||
* @var string Plugin basename.
|
||||
*/
|
||||
private $pluginFile;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $cachedInstalledVersion = null;
|
||||
|
||||
public function __construct($pluginAbsolutePath, $updateChecker) {
|
||||
$this->pluginAbsolutePath = $pluginAbsolutePath;
|
||||
$this->pluginFile = plugin_basename($this->pluginAbsolutePath);
|
||||
|
||||
parent::__construct($updateChecker);
|
||||
|
||||
//Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
|
||||
add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
|
||||
add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
|
||||
}
|
||||
|
||||
public function getInstalledVersion() {
|
||||
if ( isset($this->cachedInstalledVersion) ) {
|
||||
return $this->cachedInstalledVersion;
|
||||
}
|
||||
|
||||
$pluginHeader = $this->getPluginHeader();
|
||||
if ( isset($pluginHeader['Version']) ) {
|
||||
$this->cachedInstalledVersion = $pluginHeader['Version'];
|
||||
return $pluginHeader['Version'];
|
||||
} else {
|
||||
//This can happen if the filename points to something that is not a plugin.
|
||||
$this->updateChecker->triggerError(
|
||||
sprintf(
|
||||
"Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",
|
||||
$this->updateChecker->pluginFile
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached plugin version. This method can be set up as a filter (hook) and will
|
||||
* return the filter argument unmodified.
|
||||
*
|
||||
* @param mixed $filterArgument
|
||||
* @return mixed
|
||||
*/
|
||||
public function clearCachedVersion($filterArgument = null) {
|
||||
$this->cachedInstalledVersion = null;
|
||||
return $filterArgument;
|
||||
}
|
||||
|
||||
public function getAbsoluteDirectoryPath() {
|
||||
return dirname($this->pluginAbsolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a specific plugin or theme header.
|
||||
*
|
||||
* @param string $headerName
|
||||
* @param string $defaultValue
|
||||
* @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
|
||||
*/
|
||||
public function getHeaderValue($headerName, $defaultValue = '') {
|
||||
$headers = $this->getPluginHeader();
|
||||
if ( isset($headers[$headerName]) && ($headers[$headerName] !== '') ) {
|
||||
return $headers[$headerName];
|
||||
}
|
||||
return $defaultValue;
|
||||
}
|
||||
|
||||
protected function getHeaderNames() {
|
||||
return array(
|
||||
'Name' => 'Plugin Name',
|
||||
'PluginURI' => 'Plugin URI',
|
||||
'Version' => 'Version',
|
||||
'Description' => 'Description',
|
||||
'Author' => 'Author',
|
||||
'AuthorURI' => 'Author URI',
|
||||
'TextDomain' => 'Text Domain',
|
||||
'DomainPath' => 'Domain Path',
|
||||
'Network' => 'Network',
|
||||
|
||||
//The newest WordPress version that this plugin requires or has been tested with.
|
||||
//We support several different formats for compatibility with other libraries.
|
||||
'Tested WP' => 'Tested WP',
|
||||
'Requires WP' => 'Requires WP',
|
||||
'Tested up to' => 'Tested up to',
|
||||
'Requires at least' => 'Requires at least',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated plugin title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginTitle() {
|
||||
$title = '';
|
||||
$header = $this->getPluginHeader();
|
||||
if ( $header && !empty($header['Name']) && isset($header['TextDomain']) ) {
|
||||
$title = translate($header['Name'], $header['TextDomain']);
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin's metadata from its file header.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginHeader() {
|
||||
if ( !is_file($this->pluginAbsolutePath) ) {
|
||||
//This can happen if the plugin filename is wrong.
|
||||
$this->updateChecker->triggerError(
|
||||
sprintf(
|
||||
"Can't to read the plugin header for '%s'. The file does not exist.",
|
||||
$this->updateChecker->pluginFile
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( !function_exists('get_plugin_data') ) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once(ABSPATH . '/wp-admin/includes/plugin.php');
|
||||
}
|
||||
return get_plugin_data($this->pluginAbsolutePath, false, false);
|
||||
}
|
||||
|
||||
public function removeHooks() {
|
||||
remove_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
|
||||
remove_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin file is inside the mu-plugins directory.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMuPlugin() {
|
||||
static $cachedResult = null;
|
||||
|
||||
if ( $cachedResult === null ) {
|
||||
if ( !defined('WPMU_PLUGIN_DIR') || !is_string(WPMU_PLUGIN_DIR) ) {
|
||||
$cachedResult = false;
|
||||
return $cachedResult;
|
||||
}
|
||||
|
||||
//Convert both paths to the canonical form before comparison.
|
||||
$muPluginDir = realpath(WPMU_PLUGIN_DIR);
|
||||
$pluginPath = realpath($this->pluginAbsolutePath);
|
||||
//If realpath() fails, just normalize the syntax instead.
|
||||
if (($muPluginDir === false) || ($pluginPath === false)) {
|
||||
$muPluginDir = Puc_v4p9_Factory::normalizePath(WPMU_PLUGIN_DIR);
|
||||
$pluginPath = Puc_v4p9_Factory::normalizePath($this->pluginAbsolutePath);
|
||||
}
|
||||
|
||||
$cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
|
||||
}
|
||||
|
||||
return $cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Plugin_Ui', false) ):
|
||||
/**
|
||||
* Additional UI elements for plugins.
|
||||
*/
|
||||
class Puc_v4p9_Plugin_Ui {
|
||||
private $updateChecker;
|
||||
private $manualCheckErrorTransient = '';
|
||||
|
||||
/**
|
||||
* @param Puc_v4p9_Plugin_UpdateChecker $updateChecker
|
||||
*/
|
||||
public function __construct($updateChecker) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
$this->manualCheckErrorTransient = $this->updateChecker->getUniqueName('manual_check_errors');
|
||||
|
||||
add_action('admin_init', array($this, 'onAdminInit'));
|
||||
}
|
||||
|
||||
public function onAdminInit() {
|
||||
if ( $this->updateChecker->userCanInstallUpdates() ) {
|
||||
$this->handleManualCheck();
|
||||
|
||||
add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
|
||||
add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
|
||||
add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "View Details" link to the plugin row in the "Plugins" page. By default,
|
||||
* the new link will appear before the "Visit plugin site" link (if present).
|
||||
*
|
||||
* You can change the link text by using the "puc_view_details_link-$slug" filter.
|
||||
* Returning an empty string from the filter will disable the link.
|
||||
*
|
||||
* You can change the position of the link using the
|
||||
* "puc_view_details_link_position-$slug" filter.
|
||||
* Returning 'before' or 'after' will place the link immediately before/after
|
||||
* the "Visit plugin site" link.
|
||||
* Returning 'append' places the link after any existing links at the time of the hook.
|
||||
* Returning 'replace' replaces the "Visit plugin site" link.
|
||||
* Returning anything else disables the link when there is a "Visit plugin site" link.
|
||||
*
|
||||
* If there is no "Visit plugin site" link 'append' is always used!
|
||||
*
|
||||
* @param array $pluginMeta Array of meta links.
|
||||
* @param string $pluginFile
|
||||
* @param array $pluginData Array of plugin header data.
|
||||
* @return array
|
||||
*/
|
||||
public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
|
||||
if ( $this->isMyPluginFile($pluginFile) && !isset($pluginData['slug']) ) {
|
||||
$linkText = apply_filters($this->updateChecker->getUniqueName('view_details_link'), __('查看详情', 'kratos'));
|
||||
if ( !empty($linkText) ) {
|
||||
$viewDetailsLinkPosition = 'append';
|
||||
|
||||
//Find the "Visit plugin site" link (if present).
|
||||
$visitPluginSiteLinkIndex = count($pluginMeta) - 1;
|
||||
if ( $pluginData['PluginURI'] ) {
|
||||
$escapedPluginUri = esc_url($pluginData['PluginURI']);
|
||||
foreach ($pluginMeta as $linkIndex => $existingLink) {
|
||||
if ( strpos($existingLink, $escapedPluginUri) !== false ) {
|
||||
$visitPluginSiteLinkIndex = $linkIndex;
|
||||
$viewDetailsLinkPosition = apply_filters(
|
||||
$this->updateChecker->getUniqueName('view_details_link_position'),
|
||||
'before'
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewDetailsLink = sprintf('<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
|
||||
esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->updateChecker->slug) .
|
||||
'&TB_iframe=true&width=600&height=550')),
|
||||
esc_attr(sprintf(__('%s 的详细内容','kratos'), $pluginData['Name'])),
|
||||
esc_attr($pluginData['Name']),
|
||||
$linkText
|
||||
);
|
||||
switch ($viewDetailsLinkPosition) {
|
||||
case 'before':
|
||||
array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
|
||||
break;
|
||||
case 'after':
|
||||
array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
|
||||
break;
|
||||
case 'replace':
|
||||
$pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
|
||||
break;
|
||||
case 'append':
|
||||
default:
|
||||
$pluginMeta[] = $viewDetailsLink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pluginMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
|
||||
* the new link will appear after the "Visit plugin site" link if present, otherwise
|
||||
* after the "View plugin details" link.
|
||||
*
|
||||
* You can change the link text by using the "puc_manual_check_link-$slug" filter.
|
||||
* Returning an empty string from the filter will disable the link.
|
||||
*
|
||||
* @param array $pluginMeta Array of meta links.
|
||||
* @param string $pluginFile
|
||||
* @return array
|
||||
*/
|
||||
public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
|
||||
if ( $this->isMyPluginFile($pluginFile) ) {
|
||||
$linkUrl = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'puc_check_for_updates' => 1,
|
||||
'puc_slug' => $this->updateChecker->slug,
|
||||
),
|
||||
self_admin_url('plugins.php')
|
||||
),
|
||||
'puc_check_for_updates'
|
||||
);
|
||||
|
||||
$linkText = apply_filters(
|
||||
$this->updateChecker->getUniqueName('manual_check_link'),
|
||||
__('检查更新', 'kratos')
|
||||
);
|
||||
if ( !empty($linkText) ) {
|
||||
/** @noinspection HtmlUnknownTarget */
|
||||
$pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
|
||||
}
|
||||
}
|
||||
return $pluginMeta;
|
||||
}
|
||||
|
||||
protected function isMyPluginFile($pluginFile) {
|
||||
return ($pluginFile == $this->updateChecker->pluginFile)
|
||||
|| (!empty($this->updateChecker->muPluginFile) && ($pluginFile == $this->updateChecker->muPluginFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates when the user clicks the "Check for updates" link.
|
||||
*
|
||||
* @see self::addCheckForUpdatesLink()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleManualCheck() {
|
||||
$shouldCheck =
|
||||
isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
|
||||
&& $_GET['puc_slug'] == $this->updateChecker->slug
|
||||
&& check_admin_referer('puc_check_for_updates');
|
||||
|
||||
if ( $shouldCheck ) {
|
||||
$update = $this->updateChecker->checkForUpdates();
|
||||
$status = ($update === null) ? 'no_update' : 'update_available';
|
||||
|
||||
if ( ($update === null) && !empty($this->lastRequestApiErrors) ) {
|
||||
//Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
|
||||
//file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
|
||||
//from working. Maybe the plugin simply doesn't have a readme.
|
||||
//Let's only show important errors.
|
||||
$foundCriticalErrors = false;
|
||||
$questionableErrorCodes = array(
|
||||
'puc-github-http-error',
|
||||
'puc-gitlab-http-error',
|
||||
'puc-bitbucket-http-error',
|
||||
);
|
||||
|
||||
foreach ($this->lastRequestApiErrors as $item) {
|
||||
$wpError = $item['error'];
|
||||
/** @var WP_Error $wpError */
|
||||
if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
|
||||
$foundCriticalErrors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $foundCriticalErrors ) {
|
||||
$status = 'error';
|
||||
set_site_transient($this->manualCheckErrorTransient, $this->lastRequestApiErrors, 60);
|
||||
}
|
||||
}
|
||||
|
||||
wp_redirect(add_query_arg(
|
||||
array(
|
||||
'puc_update_check_result' => $status,
|
||||
'puc_slug' => $this->updateChecker->slug,
|
||||
),
|
||||
self_admin_url('plugins.php')
|
||||
));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the results of a manual update check.
|
||||
*
|
||||
* @see self::handleManualCheck()
|
||||
*
|
||||
* You can change the result message by using the "puc_manual_check_message-$slug" filter.
|
||||
*/
|
||||
public function displayManualCheckResult() {
|
||||
if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->updateChecker->slug) ) {
|
||||
$status = strval($_GET['puc_update_check_result']);
|
||||
$title = $this->updateChecker->getInstalledPackage()->getPluginTitle();
|
||||
$noticeClass = 'updated notice-success';
|
||||
$details = '';
|
||||
|
||||
if ( $status == 'no_update' ) {
|
||||
$message = sprintf(__('%s 是最新版本', 'kratos'), $title);
|
||||
} else if ( $status == 'update_available' ) {
|
||||
$message = sprintf(__('%s 有新版本了', 'kratos'), $title);
|
||||
} else if ( $status === 'error' ) {
|
||||
$message = sprintf(__('无法确定更新能否用于 %s', 'kratos'), $title);
|
||||
$noticeClass = 'error notice-error';
|
||||
|
||||
$details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
|
||||
delete_site_transient($this->manualCheckErrorTransient);
|
||||
} else {
|
||||
$message = sprintf(__('未知的状态:%s', 'kratos'), htmlentities($status));
|
||||
$noticeClass = 'error notice-error';
|
||||
}
|
||||
printf(
|
||||
'<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
|
||||
$noticeClass,
|
||||
apply_filters($this->updateChecker->getUniqueName('manual_check_message'), $message, $status),
|
||||
$details
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the list of errors that were thrown during an update check.
|
||||
*
|
||||
* @param array $errors
|
||||
* @return string
|
||||
*/
|
||||
protected function formatManualCheckErrors($errors) {
|
||||
if ( empty($errors) ) {
|
||||
return '';
|
||||
}
|
||||
$output = '';
|
||||
|
||||
$showAsList = count($errors) > 1;
|
||||
if ( $showAsList ) {
|
||||
$output .= '<ol>';
|
||||
$formatString = '<li>%1$s <code>%2$s</code></li>';
|
||||
} else {
|
||||
$formatString = '<p>%1$s <code>%2$s</code></p>';
|
||||
}
|
||||
foreach ($errors as $item) {
|
||||
$wpError = $item['error'];
|
||||
/** @var WP_Error $wpError */
|
||||
$output .= sprintf(
|
||||
$formatString,
|
||||
$wpError->get_error_message(),
|
||||
$wpError->get_error_code()
|
||||
);
|
||||
}
|
||||
if ( $showAsList ) {
|
||||
$output .= '</ol>';
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function removeHooks() {
|
||||
remove_action('admin_init', array($this, 'onAdminInit'));
|
||||
remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
|
||||
remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
|
||||
remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
|
||||
}
|
||||
}
|
||||
endif;
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Plugin_Update', false) ):
|
||||
|
||||
/**
|
||||
* A simple container class for holding information about an available update.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2016
|
||||
* @access public
|
||||
*/
|
||||
class Puc_v4p9_Plugin_Update extends Puc_v4p9_Update {
|
||||
public $id = 0;
|
||||
public $homepage;
|
||||
public $upgrade_notice;
|
||||
public $tested;
|
||||
public $icons = array();
|
||||
public $filename; //Plugin filename relative to the plugins directory.
|
||||
|
||||
protected static $extraFields = array(
|
||||
'id', 'homepage', 'tested', 'upgrade_notice', 'icons', 'filename',
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new instance of PluginUpdate from its JSON-encoded representation.
|
||||
*
|
||||
* @param string $json
|
||||
* @return Puc_v4p9_Plugin_Update|null
|
||||
*/
|
||||
public static function fromJson($json){
|
||||
//Since update-related information is simply a subset of the full plugin info,
|
||||
//we can parse the update JSON as if it was a plugin info string, then copy over
|
||||
//the parts that we care about.
|
||||
$pluginInfo = Puc_v4p9_Plugin_Info::fromJson($json);
|
||||
if ( $pluginInfo !== null ) {
|
||||
return self::fromPluginInfo($pluginInfo);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of PluginUpdate based on an instance of PluginInfo.
|
||||
* Basically, this just copies a subset of fields from one object to another.
|
||||
*
|
||||
* @param Puc_v4p9_Plugin_Info $info
|
||||
* @return Puc_v4p9_Plugin_Update
|
||||
*/
|
||||
public static function fromPluginInfo($info){
|
||||
return self::fromObject($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance by copying the necessary fields from another object.
|
||||
*
|
||||
* @param StdClass|Puc_v4p9_Plugin_Info|Puc_v4p9_Plugin_Update $object The source object.
|
||||
* @return Puc_v4p9_Plugin_Update The new copy.
|
||||
*/
|
||||
public static function fromObject($object) {
|
||||
$update = new self();
|
||||
$update->copyFields($object, $update);
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFieldNames() {
|
||||
return array_merge(parent::getFieldNames(), self::$extraFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the update into the format used by WordPress native plugin API.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function toWpFormat() {
|
||||
$update = parent::toWpFormat();
|
||||
|
||||
$update->id = $this->id;
|
||||
$update->url = $this->homepage;
|
||||
$update->tested = $this->tested;
|
||||
$update->plugin = $this->filename;
|
||||
|
||||
if ( !empty($this->upgrade_notice) ) {
|
||||
$update->upgrade_notice = $this->upgrade_notice;
|
||||
}
|
||||
|
||||
if ( !empty($this->icons) && is_array($this->icons) ) {
|
||||
//This should be an array with up to 4 keys: 'svg', '1x', '2x' and 'default'.
|
||||
//Docs: https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
|
||||
$icons = array_intersect_key(
|
||||
$this->icons,
|
||||
array('svg' => true, '1x' => true, '2x' => true, 'default' => true)
|
||||
);
|
||||
if ( !empty($icons) ) {
|
||||
$update->icons = $icons;
|
||||
|
||||
//It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
|
||||
//but lets set it just in case a future release needs it.
|
||||
if ( !isset($update->icons['default']) ) {
|
||||
$update->icons['default'] = current($update->icons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Plugin_UpdateChecker', false) ):
|
||||
|
||||
/**
|
||||
* A custom plugin update checker.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2018
|
||||
* @access public
|
||||
*/
|
||||
class Puc_v4p9_Plugin_UpdateChecker extends Puc_v4p9_UpdateChecker {
|
||||
protected $updateTransient = 'update_plugins';
|
||||
protected $translationType = 'plugin';
|
||||
|
||||
public $pluginAbsolutePath = ''; //Full path of the main plugin file.
|
||||
public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
|
||||
public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_Plugin_Package
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
private $extraUi = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param string $metadataUrl The URL of the plugin's metadata file.
|
||||
* @param string $pluginFile Fully qualified path to the main plugin file.
|
||||
* @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
|
||||
* @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.
|
||||
* @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
|
||||
* @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
|
||||
*/
|
||||
public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
|
||||
$this->pluginAbsolutePath = $pluginFile;
|
||||
$this->pluginFile = plugin_basename($this->pluginAbsolutePath);
|
||||
$this->muPluginFile = $muPluginFile;
|
||||
|
||||
//If no slug is specified, use the name of the main plugin file as the slug.
|
||||
//For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
|
||||
if ( empty($slug) ){
|
||||
$slug = basename($this->pluginFile, '.php');
|
||||
}
|
||||
|
||||
//Plugin slugs must be unique.
|
||||
$slugCheckFilter = 'puc_is_slug_in_use-' . $slug;
|
||||
$slugUsedBy = apply_filters($slugCheckFilter, false);
|
||||
if ( $slugUsedBy ) {
|
||||
$this->triggerError(sprintf(
|
||||
'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
|
||||
htmlentities($slug),
|
||||
htmlentities($slugUsedBy)
|
||||
), E_USER_ERROR);
|
||||
}
|
||||
add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
|
||||
|
||||
parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
|
||||
|
||||
//Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
|
||||
//it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
|
||||
if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
|
||||
$this->muPluginFile = $this->pluginFile;
|
||||
}
|
||||
|
||||
//To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
|
||||
//Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
|
||||
add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
|
||||
|
||||
$this->extraUi = new Puc_v4p9_Plugin_Ui($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the scheduler.
|
||||
*
|
||||
* @param int $checkPeriod
|
||||
* @return Puc_v4p9_Scheduler
|
||||
*/
|
||||
protected function createScheduler($checkPeriod) {
|
||||
$scheduler = new Puc_v4p9_Scheduler($this, $checkPeriod, array('load-plugins.php'));
|
||||
register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
|
||||
return $scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the hooks required to run periodic update checks and inject update info
|
||||
* into WP data structures.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function installHooks(){
|
||||
//Override requests for plugin information
|
||||
add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
|
||||
|
||||
parent::installHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove update checker hooks.
|
||||
*
|
||||
* The intent is to prevent a fatal error that can happen if the plugin has an uninstall
|
||||
* hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
|
||||
* the uninstall hook runs, WP deletes the plugin files and then updates some transients.
|
||||
* If PUC hooks are still around at this time, they could throw an error while trying to
|
||||
* autoload classes from files that no longer exist.
|
||||
*
|
||||
* The "site_transient_{$transient}" filter is the main problem here, but let's also remove
|
||||
* most other PUC hooks to be safe.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function removeHooks() {
|
||||
parent::removeHooks();
|
||||
$this->extraUi->removeHooks();
|
||||
$this->package->removeHooks();
|
||||
|
||||
remove_filter('plugins_api', array($this, 'injectInfo'), 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve plugin info from the configured API endpoint.
|
||||
*
|
||||
* @uses wp_remote_get()
|
||||
*
|
||||
* @param array $queryArgs Additional query arguments to append to the request. Optional.
|
||||
* @return Puc_v4p9_Plugin_Info
|
||||
*/
|
||||
public function requestInfo($queryArgs = array()) {
|
||||
list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p9_Plugin_Info', 'request_info', $queryArgs);
|
||||
|
||||
if ( $pluginInfo !== null ) {
|
||||
/** @var Puc_v4p9_Plugin_Info $pluginInfo */
|
||||
$pluginInfo->filename = $this->pluginFile;
|
||||
$pluginInfo->slug = $this->slug;
|
||||
}
|
||||
|
||||
$pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
|
||||
return $pluginInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the latest update (if any) from the configured API endpoint.
|
||||
*
|
||||
* @uses PluginUpdateChecker::requestInfo()
|
||||
*
|
||||
* @return Puc_v4p9_Update|null An instance of Plugin_Update, or NULL when no updates are available.
|
||||
*/
|
||||
public function requestUpdate() {
|
||||
//For the sake of simplicity, this function just calls requestInfo()
|
||||
//and transforms the result accordingly.
|
||||
$pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
|
||||
if ( $pluginInfo === null ){
|
||||
return null;
|
||||
}
|
||||
$update = Puc_v4p9_Plugin_Update::fromPluginInfo($pluginInfo);
|
||||
|
||||
$update = $this->filterUpdateResult($update);
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept plugins_api() calls that request information about our plugin and
|
||||
* use the configured API endpoint to satisfy them.
|
||||
*
|
||||
* @see plugins_api()
|
||||
*
|
||||
* @param mixed $result
|
||||
* @param string $action
|
||||
* @param array|object $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function injectInfo($result, $action = null, $args = null){
|
||||
$relevant = ($action == 'plugin_information') && isset($args->slug) && (
|
||||
($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
|
||||
);
|
||||
if ( !$relevant ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$pluginInfo = $this->requestInfo();
|
||||
$this->fixSupportedWordpressVersion($pluginInfo);
|
||||
|
||||
$pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
|
||||
if ( $pluginInfo ) {
|
||||
return $pluginInfo->toWpFormat();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function shouldShowUpdates() {
|
||||
//No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
|
||||
//is usually different from the main plugin file so the update wouldn't show up properly anyway.
|
||||
return !$this->isUnknownMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass|null $updates
|
||||
* @param stdClass $updateToAdd
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function addUpdateToList($updates, $updateToAdd) {
|
||||
if ( $this->package->isMuPlugin() ) {
|
||||
//WP does not support automatic update installation for mu-plugins, but we can
|
||||
//still display a notice.
|
||||
$updateToAdd->package = null;
|
||||
}
|
||||
return parent::addUpdateToList($updates, $updateToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass|null $updates
|
||||
* @return stdClass|null
|
||||
*/
|
||||
protected function removeUpdateFromList($updates) {
|
||||
$updates = parent::removeUpdateFromList($updates);
|
||||
if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
|
||||
unset($updates->response[$this->muPluginFile]);
|
||||
}
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* For plugins, the update array is indexed by the plugin filename relative to the "plugins"
|
||||
* directory. Example: "plugin-name/plugin.php".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getUpdateListKey() {
|
||||
if ( $this->package->isMuPlugin() ) {
|
||||
return $this->muPluginFile;
|
||||
}
|
||||
return $this->pluginFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for isBeingUpgraded().
|
||||
*
|
||||
* @deprecated
|
||||
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool
|
||||
*/
|
||||
public function isPluginBeingUpgraded($upgrader = null) {
|
||||
return $this->isBeingUpgraded($upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there an update being installed for this plugin, right now?
|
||||
*
|
||||
* @param WP_Upgrader|null $upgrader
|
||||
* @return bool
|
||||
*/
|
||||
public function isBeingUpgraded($upgrader = null) {
|
||||
return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of the currently available update, if any.
|
||||
*
|
||||
* If no updates are available, or if the last known update version is below or equal
|
||||
* to the currently installed version, this method will return NULL.
|
||||
*
|
||||
* Uses cached update data. To retrieve update information straight from
|
||||
* the metadata URL, call requestUpdate() instead.
|
||||
*
|
||||
* @return Puc_v4p9_Plugin_Update|null
|
||||
*/
|
||||
public function getUpdate() {
|
||||
$update = parent::getUpdate();
|
||||
if ( isset($update) ) {
|
||||
/** @var Puc_v4p9_Plugin_Update $update */
|
||||
$update->filename = $this->pluginFile;
|
||||
}
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated plugin title.
|
||||
*
|
||||
* @deprecated
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginTitle() {
|
||||
return $this->package->getPluginTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has the required permissions to install updates.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function userCanInstallUpdates() {
|
||||
return current_user_can('update_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin file is inside the mu-plugins directory.
|
||||
*
|
||||
* @deprecated
|
||||
* @return bool
|
||||
*/
|
||||
protected function isMuPlugin() {
|
||||
return $this->package->isMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* MU plugins are partially supported, but only when we know which file in mu-plugins
|
||||
* corresponds to this plugin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUnknownMuPlugin() {
|
||||
return empty($this->muPluginFile) && $this->package->isMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to the main plugin file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAbsolutePath() {
|
||||
return $this->pluginAbsolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering query arguments.
|
||||
*
|
||||
* The callback function should take one argument - an associative array of query arguments.
|
||||
* It should return a modified array of query arguments.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addQueryArgFilter($callback){
|
||||
$this->addFilter('request_info_query_args', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering arguments passed to wp_remote_get().
|
||||
*
|
||||
* The callback function should take one argument - an associative array of arguments -
|
||||
* and return a modified array or arguments. See the WP documentation on wp_remote_get()
|
||||
* for details on what arguments are available and how they work.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addHttpRequestArgFilter($callback) {
|
||||
$this->addFilter('request_info_options', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering the plugin info retrieved from the external API.
|
||||
*
|
||||
* The callback function should take two arguments. If the plugin info was retrieved
|
||||
* successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
|
||||
* it will be NULL. The second argument will be the corresponding return value of
|
||||
* wp_remote_get (see WP docs for details).
|
||||
*
|
||||
* The callback function should return a new or modified instance of PluginInfo or NULL.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addResultFilter($callback) {
|
||||
$this->addFilter('request_info_result', $callback, 10, 2);
|
||||
}
|
||||
|
||||
protected function createDebugBarExtension() {
|
||||
return new Puc_v4p9_DebugBar_PluginExtension($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a package instance that represents this plugin or theme.
|
||||
*
|
||||
* @return Puc_v4p9_InstalledPackage
|
||||
*/
|
||||
protected function createInstalledPackage() {
|
||||
return new Puc_v4p9_Plugin_Package($this->pluginAbsolutePath, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Puc_v4p9_Plugin_Package
|
||||
*/
|
||||
public function getInstalledPackage() {
|
||||
return $this->package;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Scheduler', false) ):
|
||||
|
||||
/**
|
||||
* The scheduler decides when and how often to check for updates.
|
||||
* It calls @see Puc_v4p9_UpdateChecker::checkForUpdates() to perform the actual checks.
|
||||
*/
|
||||
class Puc_v4p9_Scheduler {
|
||||
public $checkPeriod = 12; //How often to check for updates (in hours).
|
||||
public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.
|
||||
public $throttledCheckPeriod = 72;
|
||||
|
||||
protected $hourlyCheckHooks = array('load-update.php');
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
private $cronHook = null;
|
||||
|
||||
/**
|
||||
* Scheduler constructor.
|
||||
*
|
||||
* @param Puc_v4p9_UpdateChecker $updateChecker
|
||||
* @param int $checkPeriod How often to check for updates (in hours).
|
||||
* @param array $hourlyHooks
|
||||
*/
|
||||
public function __construct($updateChecker, $checkPeriod, $hourlyHooks = array('load-plugins.php')) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
$this->checkPeriod = $checkPeriod;
|
||||
|
||||
//Set up the periodic update checks
|
||||
$this->cronHook = $this->updateChecker->getUniqueName('cron_check_updates');
|
||||
if ( $this->checkPeriod > 0 ){
|
||||
|
||||
//Trigger the check via Cron.
|
||||
//Try to use one of the default schedules if possible as it's less likely to conflict
|
||||
//with other plugins and their custom schedules.
|
||||
$defaultSchedules = array(
|
||||
1 => 'hourly',
|
||||
12 => 'twicedaily',
|
||||
24 => 'daily',
|
||||
);
|
||||
if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {
|
||||
$scheduleName = $defaultSchedules[$this->checkPeriod];
|
||||
} else {
|
||||
//Use a custom cron schedule.
|
||||
$scheduleName = 'every' . $this->checkPeriod . 'hours';
|
||||
add_filter('cron_schedules', array($this, '_addCustomSchedule'));
|
||||
}
|
||||
|
||||
if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {
|
||||
//Randomly offset the schedule to help prevent update server traffic spikes. Without this
|
||||
//most checks may happen during times of day when people are most likely to install new plugins.
|
||||
$firstCheckTime = time() - rand(0, max($this->checkPeriod * 3600 - 15 * 60, 1));
|
||||
$firstCheckTime = apply_filters(
|
||||
$this->updateChecker->getUniqueName('first_check_time'),
|
||||
$firstCheckTime
|
||||
);
|
||||
wp_schedule_event($firstCheckTime, $scheduleName, $this->cronHook);
|
||||
}
|
||||
add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
|
||||
|
||||
//In case Cron is disabled or unreliable, we also manually trigger
|
||||
//the periodic checks while the user is browsing the Dashboard.
|
||||
add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );
|
||||
|
||||
//Like WordPress itself, we check more often on certain pages.
|
||||
/** @see wp_update_plugins */
|
||||
add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
|
||||
//"load-update.php" and "load-plugins.php" or "load-themes.php".
|
||||
$this->hourlyCheckHooks = array_merge($this->hourlyCheckHooks, $hourlyHooks);
|
||||
foreach($this->hourlyCheckHooks as $hook) {
|
||||
add_action($hook, array($this, 'maybeCheckForUpdates'));
|
||||
}
|
||||
//This hook fires after a bulk update is complete.
|
||||
add_action('upgrader_process_complete', array($this, 'upgraderProcessComplete'), 11, 2);
|
||||
|
||||
} else {
|
||||
//Periodic checks are disabled.
|
||||
wp_clear_scheduled_hook($this->cronHook);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs upon the WP action upgrader_process_complete.
|
||||
*
|
||||
* We look at the parameters to decide whether to call maybeCheckForUpdates() or not.
|
||||
*
|
||||
* @param WP_Upgrader $upgrader WP_Upgrader instance
|
||||
* @param array $upgradeInfo extra information about the upgrade
|
||||
*/
|
||||
public function upgraderProcessComplete(
|
||||
/** @noinspection PhpUnusedParameterInspection */
|
||||
$upgrader, $upgradeInfo
|
||||
) {
|
||||
|
||||
//Sanity check and limitation to relevant types.
|
||||
if (
|
||||
!is_array($upgradeInfo) || !isset($upgradeInfo['type'], $upgradeInfo['action'])
|
||||
|| 'update' !== $upgradeInfo['action'] || !in_array($upgradeInfo['type'], array('plugin', 'theme'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Filter out notifications of upgrades that should have no bearing upon whether or not our
|
||||
//current info is up-to-date.
|
||||
if ( is_a($this->updateChecker, 'Puc_v4p9_Theme_UpdateChecker') ) {
|
||||
if ( 'theme' !== $upgradeInfo['type'] || !isset($upgradeInfo['themes']) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Letting too many things going through for checks is not a real problem, so we compare widely.
|
||||
if ( !in_array(
|
||||
strtolower($this->updateChecker->directoryName),
|
||||
array_map('strtolower', $upgradeInfo['themes'])
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( is_a($this->updateChecker, 'Puc_v4p9_Plugin_UpdateChecker') ) {
|
||||
if ( 'plugin' !== $upgradeInfo['type'] || !isset($upgradeInfo['plugins']) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Themes pass in directory names in the information array, but plugins use the relative plugin path.
|
||||
if ( !in_array(
|
||||
strtolower($this->updateChecker->directoryName),
|
||||
array_map('dirname', array_map('strtolower', $upgradeInfo['plugins']))
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->maybeCheckForUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates if the configured check interval has already elapsed.
|
||||
* Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.
|
||||
*
|
||||
* You can override the default behaviour by using the "puc_check_now-$slug" filter.
|
||||
* The filter callback will be passed three parameters:
|
||||
* - Current decision. TRUE = check updates now, FALSE = don't check now.
|
||||
* - Last check time as a Unix timestamp.
|
||||
* - Configured check period in hours.
|
||||
* Return TRUE to check for updates immediately, or FALSE to cancel.
|
||||
*
|
||||
* This method is declared public because it's a hook callback. Calling it directly is not recommended.
|
||||
*/
|
||||
public function maybeCheckForUpdates() {
|
||||
if ( empty($this->checkPeriod) ){
|
||||
return;
|
||||
}
|
||||
|
||||
$state = $this->updateChecker->getUpdateState();
|
||||
$shouldCheck = ($state->timeSinceLastCheck() >= $this->getEffectiveCheckPeriod());
|
||||
|
||||
//Let plugin authors substitute their own algorithm.
|
||||
$shouldCheck = apply_filters(
|
||||
$this->updateChecker->getUniqueName('check_now'),
|
||||
$shouldCheck,
|
||||
$state->getLastCheck(),
|
||||
$this->checkPeriod
|
||||
);
|
||||
|
||||
if ( $shouldCheck ) {
|
||||
$this->updateChecker->checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the actual check period based on the current status and environment.
|
||||
*
|
||||
* @return int Check period in seconds.
|
||||
*/
|
||||
protected function getEffectiveCheckPeriod() {
|
||||
$currentFilter = current_filter();
|
||||
if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {
|
||||
//Check more often when the user visits "Dashboard -> Updates" or does a bulk update.
|
||||
$period = 60;
|
||||
} else if ( in_array($currentFilter, $this->hourlyCheckHooks) ) {
|
||||
//Also check more often on /wp-admin/update.php and the "Plugins" or "Themes" page.
|
||||
$period = 3600;
|
||||
} else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {
|
||||
//Check less frequently if it's already known that an update is available.
|
||||
$period = $this->throttledCheckPeriod * 3600;
|
||||
} else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {
|
||||
//WordPress cron schedules are not exact, so lets do an update check even
|
||||
//if slightly less than $checkPeriod hours have elapsed since the last check.
|
||||
$cronFuzziness = 20 * 60;
|
||||
$period = $this->checkPeriod * 3600 - $cronFuzziness;
|
||||
} else {
|
||||
$period = $this->checkPeriod * 3600;
|
||||
}
|
||||
|
||||
return $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add our custom schedule to the array of Cron schedules used by WP.
|
||||
*
|
||||
* @param array $schedules
|
||||
* @return array
|
||||
*/
|
||||
public function _addCustomSchedule($schedules) {
|
||||
if ( $this->checkPeriod && ($this->checkPeriod > 0) ){
|
||||
$scheduleName = 'every' . $this->checkPeriod . 'hours';
|
||||
$schedules[$scheduleName] = array(
|
||||
'interval' => $this->checkPeriod * 3600,
|
||||
'display' => sprintf('Every %d hours', $this->checkPeriod),
|
||||
);
|
||||
}
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the scheduled cron event that the library uses to check for updates.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeUpdaterCron() {
|
||||
wp_clear_scheduled_hook($this->cronHook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the update checker's WP-cron hook. Mostly useful for debugging.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCronHookName() {
|
||||
return $this->cronHook;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_StateStore', false) ):
|
||||
|
||||
class Puc_v4p9_StateStore {
|
||||
/**
|
||||
* @var int Last update check timestamp.
|
||||
*/
|
||||
protected $lastCheck = 0;
|
||||
|
||||
/**
|
||||
* @var string Version number.
|
||||
*/
|
||||
protected $checkedVersion = '';
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_Update|null Cached update.
|
||||
*/
|
||||
protected $update = null;
|
||||
|
||||
/**
|
||||
* @var string Site option name.
|
||||
*/
|
||||
private $optionName = '';
|
||||
|
||||
/**
|
||||
* @var bool Whether we've already tried to load the state from the database.
|
||||
*/
|
||||
private $isLoaded = false;
|
||||
|
||||
public function __construct($optionName) {
|
||||
$this->optionName = $optionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time elapsed since the last update check.
|
||||
*
|
||||
* If there are no recorded update checks, this method returns a large arbitrary number
|
||||
* (i.e. time since the Unix epoch).
|
||||
*
|
||||
* @return int Elapsed time in seconds.
|
||||
*/
|
||||
public function timeSinceLastCheck() {
|
||||
$this->lazyLoad();
|
||||
return time() - $this->lastCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastCheck() {
|
||||
$this->lazyLoad();
|
||||
return $this->lastCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time of the last update check to the current timestamp.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLastCheckToNow() {
|
||||
$this->lazyLoad();
|
||||
$this->lastCheck = time();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|Puc_v4p9_Update
|
||||
*/
|
||||
public function getUpdate() {
|
||||
$this->lazyLoad();
|
||||
return $this->update;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Puc_v4p9_Update|null $update
|
||||
* @return $this
|
||||
*/
|
||||
public function setUpdate(Puc_v4p9_Update $update = null) {
|
||||
$this->lazyLoad();
|
||||
$this->update = $update;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCheckedVersion() {
|
||||
$this->lazyLoad();
|
||||
return $this->checkedVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return $this
|
||||
*/
|
||||
public function setCheckedVersion($version) {
|
||||
$this->lazyLoad();
|
||||
$this->checkedVersion = strval($version);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation updates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTranslations() {
|
||||
$this->lazyLoad();
|
||||
if ( isset($this->update, $this->update->translations) ) {
|
||||
return $this->update->translations;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set translation updates.
|
||||
*
|
||||
* @param array $translationUpdates
|
||||
*/
|
||||
public function setTranslations($translationUpdates) {
|
||||
$this->lazyLoad();
|
||||
if ( isset($this->update) ) {
|
||||
$this->update->translations = $translationUpdates;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$state = new stdClass();
|
||||
|
||||
$state->lastCheck = $this->lastCheck;
|
||||
$state->checkedVersion = $this->checkedVersion;
|
||||
|
||||
if ( isset($this->update)) {
|
||||
$state->update = $this->update->toStdClass();
|
||||
|
||||
$updateClass = get_class($this->update);
|
||||
$state->updateClass = $updateClass;
|
||||
$prefix = $this->getLibPrefix();
|
||||
if ( Puc_v4p9_Utils::startsWith($updateClass, $prefix) ) {
|
||||
$state->updateBaseClass = substr($updateClass, strlen($prefix));
|
||||
}
|
||||
}
|
||||
|
||||
update_site_option($this->optionName, $state);
|
||||
$this->isLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function lazyLoad() {
|
||||
if ( !$this->isLoaded ) {
|
||||
$this->load();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function load() {
|
||||
$this->isLoaded = true;
|
||||
|
||||
$state = get_site_option($this->optionName, null);
|
||||
|
||||
if ( !is_object($state) ) {
|
||||
$this->lastCheck = 0;
|
||||
$this->checkedVersion = '';
|
||||
$this->update = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lastCheck = intval(Puc_v4p9_Utils::get($state, 'lastCheck', 0));
|
||||
$this->checkedVersion = Puc_v4p9_Utils::get($state, 'checkedVersion', '');
|
||||
$this->update = null;
|
||||
|
||||
if ( isset($state->update) ) {
|
||||
//This mess is due to the fact that the want the update class from this version
|
||||
//of the library, not the version that saved the update.
|
||||
|
||||
$updateClass = null;
|
||||
if ( isset($state->updateBaseClass) ) {
|
||||
$updateClass = $this->getLibPrefix() . $state->updateBaseClass;
|
||||
} else if ( isset($state->updateClass) && class_exists($state->updateClass) ) {
|
||||
$updateClass = $state->updateClass;
|
||||
}
|
||||
|
||||
if ( $updateClass !== null ) {
|
||||
$this->update = call_user_func(array($updateClass, 'fromObject'), $state->update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
delete_site_option($this->optionName);
|
||||
|
||||
$this->lastCheck = 0;
|
||||
$this->checkedVersion = '';
|
||||
$this->update = null;
|
||||
}
|
||||
|
||||
private function getLibPrefix() {
|
||||
$parts = explode('_', __CLASS__, 3);
|
||||
return $parts[0] . '_' . $parts[1] . '_';
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Theme_Package', false) ):
|
||||
|
||||
class Puc_v4p9_Theme_Package extends Puc_v4p9_InstalledPackage {
|
||||
/**
|
||||
* @var string Theme directory name.
|
||||
*/
|
||||
protected $stylesheet;
|
||||
|
||||
/**
|
||||
* @var WP_Theme Theme object.
|
||||
*/
|
||||
protected $theme;
|
||||
|
||||
public function __construct($stylesheet, $updateChecker) {
|
||||
$this->stylesheet = $stylesheet;
|
||||
$this->theme = wp_get_theme($this->stylesheet);
|
||||
|
||||
parent::__construct($updateChecker);
|
||||
}
|
||||
|
||||
public function getInstalledVersion() {
|
||||
return $this->theme->get('Version');
|
||||
}
|
||||
|
||||
public function getAbsoluteDirectoryPath() {
|
||||
if ( method_exists($this->theme, 'get_stylesheet_directory') ) {
|
||||
return $this->theme->get_stylesheet_directory(); //Available since WP 3.4.
|
||||
}
|
||||
return get_theme_root($this->stylesheet) . '/' . $this->stylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a specific plugin or theme header.
|
||||
*
|
||||
* @param string $headerName
|
||||
* @param string $defaultValue
|
||||
* @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
|
||||
*/
|
||||
public function getHeaderValue($headerName, $defaultValue = '') {
|
||||
$value = $this->theme->get($headerName);
|
||||
if ( ($headerName === false) || ($headerName === '') ) {
|
||||
return $defaultValue;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function getHeaderNames() {
|
||||
return array(
|
||||
'Name' => 'Theme Name',
|
||||
'ThemeURI' => 'Theme URI',
|
||||
'Description' => 'Description',
|
||||
'Author' => 'Author',
|
||||
'AuthorURI' => 'Author URI',
|
||||
'Version' => 'Version',
|
||||
'Template' => 'Template',
|
||||
'Status' => 'Status',
|
||||
'Tags' => 'Tags',
|
||||
'TextDomain' => 'Text Domain',
|
||||
'DomainPath' => 'Domain Path',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Theme_Update', false) ):
|
||||
|
||||
class Puc_v4p9_Theme_Update extends Puc_v4p9_Update {
|
||||
public $details_url = '';
|
||||
|
||||
protected static $extraFields = array('details_url');
|
||||
|
||||
/**
|
||||
* Transform the metadata into the format used by WordPress core.
|
||||
* Note the inconsistency: WP stores plugin updates as objects and theme updates as arrays.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toWpFormat() {
|
||||
$update = array(
|
||||
'theme' => $this->slug,
|
||||
'new_version' => $this->version,
|
||||
'url' => $this->details_url,
|
||||
);
|
||||
|
||||
if ( !empty($this->download_url) ) {
|
||||
$update['package'] = $this->download_url;
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of Theme_Update from its JSON-encoded representation.
|
||||
*
|
||||
* @param string $json Valid JSON string representing a theme information object.
|
||||
* @return self New instance of ThemeUpdate, or NULL on error.
|
||||
*/
|
||||
public static function fromJson($json) {
|
||||
$instance = new self();
|
||||
if ( !parent::createFromJson($json, $instance) ) {
|
||||
return null;
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance by copying the necessary fields from another object.
|
||||
*
|
||||
* @param StdClass|Puc_v4p9_Theme_Update $object The source object.
|
||||
* @return Puc_v4p9_Theme_Update The new copy.
|
||||
*/
|
||||
public static function fromObject($object) {
|
||||
$update = new self();
|
||||
$update->copyFields($object, $update);
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation.
|
||||
*
|
||||
* @param StdClass $apiResponse
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function validateMetadata($apiResponse) {
|
||||
$required = array('version', 'details_url');
|
||||
foreach($required as $key) {
|
||||
if ( !isset($apiResponse->$key) || empty($apiResponse->$key) ) {
|
||||
return new WP_Error(
|
||||
'tuc-invalid-metadata',
|
||||
sprintf('The theme metadata is missing the required "%s" key.', $key)
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getFieldNames() {
|
||||
return array_merge(parent::getFieldNames(), self::$extraFields);
|
||||
}
|
||||
|
||||
protected function getPrefixedFilter($tag) {
|
||||
return parent::getPrefixedFilter($tag) . '_theme';
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Theme_UpdateChecker', false) ):
|
||||
|
||||
class Puc_v4p9_Theme_UpdateChecker extends Puc_v4p9_UpdateChecker {
|
||||
protected $filterSuffix = 'theme';
|
||||
protected $updateTransient = 'update_themes';
|
||||
protected $translationType = 'theme';
|
||||
|
||||
/**
|
||||
* @var string Theme directory name.
|
||||
*/
|
||||
protected $stylesheet;
|
||||
|
||||
public function __construct($metadataUrl, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
|
||||
if ( $stylesheet === null ) {
|
||||
$stylesheet = get_stylesheet();
|
||||
}
|
||||
$this->stylesheet = $stylesheet;
|
||||
|
||||
parent::__construct(
|
||||
$metadataUrl,
|
||||
$stylesheet,
|
||||
$customSlug ? $customSlug : $stylesheet,
|
||||
$checkPeriod,
|
||||
$optionName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For themes, the update array is indexed by theme directory name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getUpdateListKey() {
|
||||
return $this->directoryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the latest update (if any) from the configured API endpoint.
|
||||
*
|
||||
* @return Puc_v4p9_Update|null An instance of Update, or NULL when no updates are available.
|
||||
*/
|
||||
public function requestUpdate() {
|
||||
list($themeUpdate, $result) = $this->requestMetadata('Puc_v4p9_Theme_Update', 'request_update');
|
||||
|
||||
if ( $themeUpdate !== null ) {
|
||||
/** @var Puc_v4p9_Theme_Update $themeUpdate */
|
||||
$themeUpdate->slug = $this->slug;
|
||||
}
|
||||
|
||||
$themeUpdate = $this->filterUpdateResult($themeUpdate, $result);
|
||||
return $themeUpdate;
|
||||
}
|
||||
|
||||
public function userCanInstallUpdates() {
|
||||
return current_user_can('update_themes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the scheduler.
|
||||
*
|
||||
* @param int $checkPeriod
|
||||
* @return Puc_v4p9_Scheduler
|
||||
*/
|
||||
protected function createScheduler($checkPeriod) {
|
||||
return new Puc_v4p9_Scheduler($this, $checkPeriod, array('load-themes.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there an update being installed right now for this theme?
|
||||
*
|
||||
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool
|
||||
*/
|
||||
public function isBeingUpgraded($upgrader = null) {
|
||||
return $this->upgraderStatus->isThemeBeingUpgraded($this->stylesheet, $upgrader);
|
||||
}
|
||||
|
||||
protected function createDebugBarExtension() {
|
||||
return new Puc_v4p9_DebugBar_Extension($this, 'Puc_v4p9_DebugBar_ThemePanel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering query arguments.
|
||||
*
|
||||
* The callback function should take one argument - an associative array of query arguments.
|
||||
* It should return a modified array of query arguments.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addQueryArgFilter($callback){
|
||||
$this->addFilter('request_update_query_args', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering arguments passed to wp_remote_get().
|
||||
*
|
||||
* The callback function should take one argument - an associative array of arguments -
|
||||
* and return a modified array or arguments. See the WP documentation on wp_remote_get()
|
||||
* for details on what arguments are available and how they work.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addHttpRequestArgFilter($callback) {
|
||||
$this->addFilter('request_update_options', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering theme updates retrieved from the external API.
|
||||
*
|
||||
* The callback function should take two arguments. If the theme update was retrieved
|
||||
* successfully, the first argument passed will be an instance of Theme_Update. Otherwise,
|
||||
* it will be NULL. The second argument will be the corresponding return value of
|
||||
* wp_remote_get (see WP docs for details).
|
||||
*
|
||||
* The callback function should return a new or modified instance of Theme_Update or NULL.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addResultFilter($callback) {
|
||||
$this->addFilter('request_update_result', $callback, 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a package instance that represents this plugin or theme.
|
||||
*
|
||||
* @return Puc_v4p9_InstalledPackage
|
||||
*/
|
||||
protected function createInstalledPackage() {
|
||||
return new Puc_v4p9_Theme_Package($this->stylesheet, $this);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Update', false) ):
|
||||
|
||||
/**
|
||||
* A simple container class for holding information about an available update.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @access public
|
||||
*/
|
||||
abstract class Puc_v4p9_Update extends Puc_v4p9_Metadata {
|
||||
public $slug;
|
||||
public $version;
|
||||
public $download_url;
|
||||
public $translations = array();
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFieldNames() {
|
||||
return array('slug', 'version', 'download_url', 'translations');
|
||||
}
|
||||
|
||||
public function toWpFormat() {
|
||||
$update = new stdClass();
|
||||
|
||||
$update->slug = $this->slug;
|
||||
$update->new_version = $this->version;
|
||||
$update->package = $this->download_url;
|
||||
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,897 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_UpdateChecker', false) ):
|
||||
|
||||
abstract class Puc_v4p9_UpdateChecker {
|
||||
protected $filterSuffix = '';
|
||||
protected $updateTransient = '';
|
||||
protected $translationType = ''; //"plugin" or "theme".
|
||||
|
||||
/**
|
||||
* Set to TRUE to enable error reporting. Errors are raised using trigger_error()
|
||||
* and should be logged to the standard PHP error log.
|
||||
* @var bool
|
||||
*/
|
||||
public $debugMode = null;
|
||||
|
||||
/**
|
||||
* @var string Where to store the update info.
|
||||
*/
|
||||
public $optionName = '';
|
||||
|
||||
/**
|
||||
* @var string The URL of the metadata file.
|
||||
*/
|
||||
public $metadataUrl = '';
|
||||
|
||||
/**
|
||||
* @var string Plugin or theme directory name.
|
||||
*/
|
||||
public $directoryName = '';
|
||||
|
||||
/**
|
||||
* @var string The slug that will be used in update checker hooks and remote API requests.
|
||||
* Usually matches the directory name unless the plugin/theme directory has been renamed.
|
||||
*/
|
||||
public $slug = '';
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_InstalledPackage
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_Scheduler
|
||||
*/
|
||||
public $scheduler;
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_UpgraderStatus
|
||||
*/
|
||||
protected $upgraderStatus;
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_StateStore
|
||||
*/
|
||||
protected $updateState;
|
||||
|
||||
/**
|
||||
* @var array List of API errors triggered during the last checkForUpdates() call.
|
||||
*/
|
||||
protected $lastRequestApiErrors = array();
|
||||
|
||||
public function __construct($metadataUrl, $directoryName, $slug = null, $checkPeriod = 12, $optionName = '') {
|
||||
$this->debugMode = (bool)(constant('WP_DEBUG'));
|
||||
$this->metadataUrl = $metadataUrl;
|
||||
$this->directoryName = $directoryName;
|
||||
$this->slug = !empty($slug) ? $slug : $this->directoryName;
|
||||
|
||||
$this->optionName = $optionName;
|
||||
if ( empty($this->optionName) ) {
|
||||
//BC: Initially the library only supported plugin updates and didn't use type prefixes
|
||||
//in the option name. Lets use the same prefix-less name when possible.
|
||||
if ( $this->filterSuffix === '' ) {
|
||||
$this->optionName = 'external_updates-' . $this->slug;
|
||||
} else {
|
||||
$this->optionName = $this->getUniqueName('external_updates');
|
||||
}
|
||||
}
|
||||
|
||||
$this->package = $this->createInstalledPackage();
|
||||
$this->scheduler = $this->createScheduler($checkPeriod);
|
||||
$this->upgraderStatus = new Puc_v4p9_UpgraderStatus();
|
||||
$this->updateState = new Puc_v4p9_StateStore($this->optionName);
|
||||
|
||||
$this->installHooks();
|
||||
}
|
||||
|
||||
protected function installHooks() {
|
||||
//Insert our update info into the update array maintained by WP.
|
||||
add_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
|
||||
|
||||
//Insert translation updates into the update list.
|
||||
add_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
|
||||
|
||||
//Clear translation updates when WP clears the update cache.
|
||||
//This needs to be done directly because the library doesn't actually remove obsolete plugin updates,
|
||||
//it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.
|
||||
add_action(
|
||||
'delete_site_transient_' . $this->updateTransient,
|
||||
array($this, 'clearCachedTranslationUpdates')
|
||||
);
|
||||
|
||||
//Rename the update directory to be the same as the existing directory.
|
||||
if ( $this->directoryName !== '.' ) {
|
||||
add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);
|
||||
}
|
||||
|
||||
//Allow HTTP requests to the metadata URL even if it's on a local host.
|
||||
add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
|
||||
|
||||
//DebugBar integration.
|
||||
if ( did_action('plugins_loaded') ) {
|
||||
$this->maybeInitDebugBar();
|
||||
} else {
|
||||
add_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove hooks that were added by this update checker instance.
|
||||
*/
|
||||
protected function removeHooks() {
|
||||
remove_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
|
||||
remove_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
|
||||
remove_action(
|
||||
'delete_site_transient_' . $this->updateTransient,
|
||||
array($this, 'clearCachedTranslationUpdates')
|
||||
);
|
||||
|
||||
remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
|
||||
remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
|
||||
remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has the required permissions to install updates.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function userCanInstallUpdates();
|
||||
|
||||
/**
|
||||
* Explicitly allow HTTP requests to the metadata URL.
|
||||
*
|
||||
* WordPress has a security feature where the HTTP API will reject all requests that are sent to
|
||||
* another site hosted on the same server as the current site (IP match), a local host, or a local
|
||||
* IP, unless the host exactly matches the current site.
|
||||
*
|
||||
* This feature is opt-in (at least in WP 4.4). Apparently some people enable it.
|
||||
*
|
||||
* That can be a problem when you're developing your plugin and you decide to host the update information
|
||||
* on the same server as your test site. Update requests will mysteriously fail.
|
||||
*
|
||||
* We fix that by adding an exception for the metadata host.
|
||||
*
|
||||
* @param bool $allow
|
||||
* @param string $host
|
||||
* @return bool
|
||||
*/
|
||||
public function allowMetadataHost($allow, $host) {
|
||||
static $metadataHost = 0; //Using 0 instead of NULL because parse_url can return NULL.
|
||||
if ( $metadataHost === 0 ) {
|
||||
$metadataHost = parse_url($this->metadataUrl, PHP_URL_HOST);
|
||||
}
|
||||
|
||||
if ( is_string($metadataHost) && (strtolower($host) === strtolower($metadataHost)) ) {
|
||||
return true;
|
||||
}
|
||||
return $allow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a package instance that represents this plugin or theme.
|
||||
*
|
||||
* @return Puc_v4p9_InstalledPackage
|
||||
*/
|
||||
abstract protected function createInstalledPackage();
|
||||
|
||||
/**
|
||||
* @return Puc_v4p9_InstalledPackage
|
||||
*/
|
||||
public function getInstalledPackage() {
|
||||
return $this->package;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the scheduler.
|
||||
*
|
||||
* This is implemented as a method to make it possible for plugins to subclass the update checker
|
||||
* and substitute their own scheduler.
|
||||
*
|
||||
* @param int $checkPeriod
|
||||
* @return Puc_v4p9_Scheduler
|
||||
*/
|
||||
abstract protected function createScheduler($checkPeriod);
|
||||
|
||||
/**
|
||||
* Check for updates. The results are stored in the DB option specified in $optionName.
|
||||
*
|
||||
* @return Puc_v4p9_Update|null
|
||||
*/
|
||||
public function checkForUpdates() {
|
||||
$installedVersion = $this->getInstalledVersion();
|
||||
//Fail silently if we can't find the plugin/theme or read its header.
|
||||
if ( $installedVersion === null ) {
|
||||
$this->triggerError(
|
||||
sprintf('Skipping update check for %s - installed version unknown.', $this->slug),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
//Start collecting API errors.
|
||||
$this->lastRequestApiErrors = array();
|
||||
add_action('puc_api_error', array($this, 'collectApiErrors'), 10, 4);
|
||||
|
||||
$state = $this->updateState;
|
||||
$state->setLastCheckToNow()
|
||||
->setCheckedVersion($installedVersion)
|
||||
->save(); //Save before checking in case something goes wrong
|
||||
|
||||
$state->setUpdate($this->requestUpdate());
|
||||
$state->save();
|
||||
|
||||
//Stop collecting API errors.
|
||||
remove_action('puc_api_error', array($this, 'collectApiErrors'), 10);
|
||||
|
||||
return $this->getUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the update checker state from the DB.
|
||||
*
|
||||
* @return Puc_v4p9_StateStore
|
||||
*/
|
||||
public function getUpdateState() {
|
||||
return $this->updateState->lazyLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset update checker state - i.e. last check time, cached update data and so on.
|
||||
*
|
||||
* Call this when your plugin is being uninstalled, or if you want to
|
||||
* clear the update cache.
|
||||
*/
|
||||
public function resetUpdateState() {
|
||||
$this->updateState->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of the currently available update, if any.
|
||||
*
|
||||
* If no updates are available, or if the last known update version is below or equal
|
||||
* to the currently installed version, this method will return NULL.
|
||||
*
|
||||
* Uses cached update data. To retrieve update information straight from
|
||||
* the metadata URL, call requestUpdate() instead.
|
||||
*
|
||||
* @return Puc_v4p9_Update|null
|
||||
*/
|
||||
public function getUpdate() {
|
||||
$update = $this->updateState->getUpdate();
|
||||
|
||||
//Is there an update available?
|
||||
if ( isset($update) ) {
|
||||
//Check if the update is actually newer than the currently installed version.
|
||||
$installedVersion = $this->getInstalledVersion();
|
||||
if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the latest update (if any) from the configured API endpoint.
|
||||
*
|
||||
* Subclasses should run the update through filterUpdateResult before returning it.
|
||||
*
|
||||
* @return Puc_v4p9_Update An instance of Update, or NULL when no updates are available.
|
||||
*/
|
||||
abstract public function requestUpdate();
|
||||
|
||||
/**
|
||||
* Filter the result of a requestUpdate() call.
|
||||
*
|
||||
* @param Puc_v4p9_Update|null $update
|
||||
* @param array|WP_Error|null $httpResult The value returned by wp_remote_get(), if any.
|
||||
* @return Puc_v4p9_Update
|
||||
*/
|
||||
protected function filterUpdateResult($update, $httpResult = null) {
|
||||
//Let plugins/themes modify the update.
|
||||
$update = apply_filters($this->getUniqueName('request_update_result'), $update, $httpResult);
|
||||
|
||||
$this->fixSupportedWordpressVersion($update);
|
||||
|
||||
if ( isset($update, $update->translations) ) {
|
||||
//Keep only those translation updates that apply to this site.
|
||||
$update->translations = $this->filterApplicableTranslations($update->translations);
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Tested up to" field in the plugin metadata is supposed to be in the form of "major.minor",
|
||||
* while WordPress core's list_plugin_updates() expects the $update->tested field to be an exact
|
||||
* version, e.g. "major.minor.patch", to say it's compatible. In other case it shows
|
||||
* "Compatibility: Unknown".
|
||||
* The function mimics how wordpress.org API crafts the "tested" field out of "Tested up to".
|
||||
*
|
||||
* @param Puc_v4p9_Metadata|null $update
|
||||
*/
|
||||
protected function fixSupportedWordpressVersion(Puc_v4p9_Metadata $update = null) {
|
||||
if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actualWpVersions = array();
|
||||
|
||||
$wpVersion = $GLOBALS['wp_version'];
|
||||
|
||||
if ( function_exists('get_preferred_from_update_core') ) {
|
||||
$coreUpdate = get_preferred_from_update_core();
|
||||
if ( isset($coreUpdate->current) && version_compare($coreUpdate->current, $wpVersion, '>') ) {
|
||||
$actualWpVersions[] = $coreUpdate->current;
|
||||
}
|
||||
}
|
||||
|
||||
$actualWpVersions[] = $wpVersion;
|
||||
|
||||
$actualWpPatchNumber = "999";
|
||||
foreach ($actualWpVersions as $version) {
|
||||
if ( preg_match('/^(?P<majorMinor>\d++\.\d++)\.(?P<patch>\d++)/', $version, $versionParts) ) {
|
||||
if ( $versionParts['majorMinor'] === $update->tested ) {
|
||||
$actualWpPatchNumber = $versionParts['patch'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$update->tested .= '.' . $actualWpPatchNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently installed version of the plugin or theme.
|
||||
*
|
||||
* @return string|null Version number.
|
||||
*/
|
||||
public function getInstalledVersion() {
|
||||
return $this->package->getInstalledVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of the plugin or theme directory.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAbsoluteDirectoryPath() {
|
||||
return $this->package->getAbsoluteDirectoryPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a PHP error, but only when $debugMode is enabled.
|
||||
*
|
||||
* @param string $message
|
||||
* @param int $errorType
|
||||
*/
|
||||
public function triggerError($message, $errorType) {
|
||||
if ( $this->isDebugModeEnabled() ) {
|
||||
trigger_error($message, $errorType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDebugModeEnabled() {
|
||||
if ( $this->debugMode === null ) {
|
||||
$this->debugMode = (bool)(constant('WP_DEBUG'));
|
||||
}
|
||||
return $this->debugMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full name of an update checker filter, action or DB entry.
|
||||
*
|
||||
* This method adds the "puc_" prefix and the "-$slug" suffix to the filter name.
|
||||
* For example, "pre_inject_update" becomes "puc_pre_inject_update-plugin-slug".
|
||||
*
|
||||
* @param string $baseTag
|
||||
* @return string
|
||||
*/
|
||||
public function getUniqueName($baseTag) {
|
||||
$name = 'puc_' . $baseTag;
|
||||
if ( $this->filterSuffix !== '' ) {
|
||||
$name .= '_' . $this->filterSuffix;
|
||||
}
|
||||
return $name . '-' . $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store API errors that are generated when checking for updates.
|
||||
*
|
||||
* @internal
|
||||
* @param WP_Error $error
|
||||
* @param array|null $httpResponse
|
||||
* @param string|null $url
|
||||
* @param string|null $slug
|
||||
*/
|
||||
public function collectApiErrors($error, $httpResponse = null, $url = null, $slug = null) {
|
||||
if ( isset($slug) && ($slug !== $this->slug) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lastRequestApiErrors[] = array(
|
||||
'error' => $error,
|
||||
'httpResponse' => $httpResponse,
|
||||
'url' => $url,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLastRequestApiErrors() {
|
||||
return $this->lastRequestApiErrors;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* PUC filters and filter utilities
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register a callback for one of the update checker filters.
|
||||
*
|
||||
* Identical to add_filter(), except it automatically adds the "puc_" prefix
|
||||
* and the "-$slug" suffix to the filter name. For example, "request_info_result"
|
||||
* becomes "puc_request_info_result-your_plugin_slug".
|
||||
*
|
||||
* @param string $tag
|
||||
* @param callable $callback
|
||||
* @param int $priority
|
||||
* @param int $acceptedArgs
|
||||
*/
|
||||
public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {
|
||||
add_filter($this->getUniqueName($tag), $callback, $priority, $acceptedArgs);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* Inject updates
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Insert the latest update (if any) into the update list maintained by WP.
|
||||
*
|
||||
* @param stdClass $updates Update list.
|
||||
* @return stdClass Modified update list.
|
||||
*/
|
||||
public function injectUpdate($updates) {
|
||||
//Is there an update to insert?
|
||||
$update = $this->getUpdate();
|
||||
|
||||
if ( !$this->shouldShowUpdates() ) {
|
||||
$update = null;
|
||||
}
|
||||
|
||||
if ( !empty($update) ) {
|
||||
//Let plugins filter the update info before it's passed on to WordPress.
|
||||
$update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
|
||||
$updates = $this->addUpdateToList($updates, $update->toWpFormat());
|
||||
} else {
|
||||
//Clean up any stale update info.
|
||||
$updates = $this->removeUpdateFromList($updates);
|
||||
}
|
||||
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass|null $updates
|
||||
* @param stdClass|array $updateToAdd
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function addUpdateToList($updates, $updateToAdd) {
|
||||
if ( !is_object($updates) ) {
|
||||
$updates = new stdClass();
|
||||
$updates->response = array();
|
||||
}
|
||||
|
||||
$updates->response[$this->getUpdateListKey()] = $updateToAdd;
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass|null $updates
|
||||
* @return stdClass|null
|
||||
*/
|
||||
protected function removeUpdateFromList($updates) {
|
||||
if ( isset($updates, $updates->response) ) {
|
||||
unset($updates->response[$this->getUpdateListKey()]);
|
||||
}
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key that will be used when adding updates to the update list that's maintained
|
||||
* by the WordPress core. The list is always an associative array, but the key is different
|
||||
* for plugins and themes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getUpdateListKey();
|
||||
|
||||
/**
|
||||
* Should we show available updates?
|
||||
*
|
||||
* Usually the answer is "yes", but there are exceptions. For example, WordPress doesn't
|
||||
* support automatic updates installation for mu-plugins, so PUC usually won't show update
|
||||
* notifications in that case. See the plugin-specific subclass for details.
|
||||
*
|
||||
* Note: This method only applies to updates that are displayed (or not) in the WordPress
|
||||
* admin. It doesn't affect APIs like requestUpdate and getUpdate.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldShowUpdates() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* JSON-based update API
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve plugin or theme metadata from the JSON document at $this->metadataUrl.
|
||||
*
|
||||
* @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
|
||||
* @param string $filterRoot
|
||||
* @param array $queryArgs Additional query arguments.
|
||||
* @return array [Puc_v4p9_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
|
||||
*/
|
||||
protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
|
||||
//Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
|
||||
$queryArgs = array_merge(
|
||||
array(
|
||||
'installed_version' => strval($this->getInstalledVersion()),
|
||||
'php' => phpversion(),
|
||||
'locale' => get_locale(),
|
||||
),
|
||||
$queryArgs
|
||||
);
|
||||
$queryArgs = apply_filters($this->getUniqueName($filterRoot . '_query_args'), $queryArgs);
|
||||
|
||||
//Various options for the wp_remote_get() call. Plugins can filter these, too.
|
||||
$options = array(
|
||||
'timeout' => 10, //seconds
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
$options = apply_filters($this->getUniqueName($filterRoot . '_options'), $options);
|
||||
|
||||
//The metadata file should be at 'http://your-api.com/url/here/$slug/info.json'
|
||||
$url = $this->metadataUrl;
|
||||
if ( !empty($queryArgs) ){
|
||||
$url = add_query_arg($queryArgs, $url);
|
||||
}
|
||||
|
||||
$result = wp_remote_get($url, $options);
|
||||
|
||||
$result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
|
||||
|
||||
//Try to parse the response
|
||||
$status = $this->validateApiResponse($result);
|
||||
$metadata = null;
|
||||
if ( !is_wp_error($status) ){
|
||||
$metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
|
||||
} else {
|
||||
do_action('puc_api_error', $status, $result, $url, $this->slug);
|
||||
$this->triggerError(
|
||||
sprintf('The URL %s does not point to a valid metadata file. ', $url)
|
||||
. $status->get_error_message(),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return array($metadata, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if $result is a successful update API response.
|
||||
*
|
||||
* @param array|WP_Error $result
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
protected function validateApiResponse($result) {
|
||||
if ( is_wp_error($result) ) { /** @var WP_Error $result */
|
||||
return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
|
||||
}
|
||||
|
||||
if ( !isset($result['response']['code']) ) {
|
||||
return new WP_Error(
|
||||
'puc_no_response_code',
|
||||
'wp_remote_get() returned an unexpected result.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $result['response']['code'] !== 200 ) {
|
||||
return new WP_Error(
|
||||
'puc_unexpected_response_code',
|
||||
'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
|
||||
);
|
||||
}
|
||||
|
||||
if ( empty($result['body']) ) {
|
||||
return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* Language packs / Translation updates
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Filter a list of translation updates and return a new list that contains only updates
|
||||
* that apply to the current site.
|
||||
*
|
||||
* @param array $translations
|
||||
* @return array
|
||||
*/
|
||||
protected function filterApplicableTranslations($translations) {
|
||||
$languages = array_flip(array_values(get_available_languages()));
|
||||
$installedTranslations = $this->getInstalledTranslations();
|
||||
|
||||
$applicableTranslations = array();
|
||||
foreach ($translations as $translation) {
|
||||
//Does it match one of the available core languages?
|
||||
$isApplicable = array_key_exists($translation->language, $languages);
|
||||
//Is it more recent than an already-installed translation?
|
||||
if ( isset($installedTranslations[$translation->language]) ) {
|
||||
$updateTimestamp = strtotime($translation->updated);
|
||||
$installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);
|
||||
$isApplicable = $updateTimestamp > $installedTimestamp;
|
||||
}
|
||||
|
||||
if ( $isApplicable ) {
|
||||
$applicableTranslations[] = $translation;
|
||||
}
|
||||
}
|
||||
|
||||
return $applicableTranslations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of installed translations for this plugin or theme.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getInstalledTranslations() {
|
||||
if ( !function_exists('wp_get_installed_translations') ) {
|
||||
return array();
|
||||
}
|
||||
$installedTranslations = wp_get_installed_translations($this->translationType . 's');
|
||||
if ( isset($installedTranslations[$this->directoryName]) ) {
|
||||
$installedTranslations = $installedTranslations[$this->directoryName];
|
||||
} else {
|
||||
$installedTranslations = array();
|
||||
}
|
||||
return $installedTranslations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert translation updates into the list maintained by WordPress.
|
||||
*
|
||||
* @param stdClass $updates
|
||||
* @return stdClass
|
||||
*/
|
||||
public function injectTranslationUpdates($updates) {
|
||||
$translationUpdates = $this->getTranslationUpdates();
|
||||
if ( empty($translationUpdates) ) {
|
||||
return $updates;
|
||||
}
|
||||
|
||||
//Being defensive.
|
||||
if ( !is_object($updates) ) {
|
||||
$updates = new stdClass();
|
||||
}
|
||||
if ( !isset($updates->translations) ) {
|
||||
$updates->translations = array();
|
||||
}
|
||||
|
||||
//In case there's a name collision with a plugin or theme hosted on wordpress.org,
|
||||
//remove any preexisting updates that match our thing.
|
||||
$updates->translations = array_values(array_filter(
|
||||
$updates->translations,
|
||||
array($this, 'isNotMyTranslation')
|
||||
));
|
||||
|
||||
//Add our updates to the list.
|
||||
foreach($translationUpdates as $update) {
|
||||
$convertedUpdate = array_merge(
|
||||
array(
|
||||
'type' => $this->translationType,
|
||||
'slug' => $this->directoryName,
|
||||
'autoupdate' => 0,
|
||||
//AFAICT, WordPress doesn't actually use the "version" field for anything.
|
||||
//But lets make sure it's there, just in case.
|
||||
'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),
|
||||
),
|
||||
(array)$update
|
||||
);
|
||||
|
||||
$updates->translations[] = $convertedUpdate;
|
||||
}
|
||||
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available translation updates.
|
||||
*
|
||||
* This method will return an empty array if there are no updates.
|
||||
* Uses cached update data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTranslationUpdates() {
|
||||
return $this->updateState->getTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all cached translation updates.
|
||||
*
|
||||
* @see wp_clean_update_cache
|
||||
*/
|
||||
public function clearCachedTranslationUpdates() {
|
||||
$this->updateState->setTranslations(array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter callback. Keeps only translations that *don't* match this plugin or theme.
|
||||
*
|
||||
* @param array $translation
|
||||
* @return bool
|
||||
*/
|
||||
protected function isNotMyTranslation($translation) {
|
||||
$isMatch = isset($translation['type'], $translation['slug'])
|
||||
&& ($translation['type'] === $this->translationType)
|
||||
&& ($translation['slug'] === $this->directoryName);
|
||||
|
||||
return !$isMatch;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* Fix directory name when installing updates
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rename the update directory to match the existing plugin/theme directory.
|
||||
*
|
||||
* When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain
|
||||
* exactly one directory, and that the directory name will be the same as the directory where
|
||||
* the plugin or theme is currently installed.
|
||||
*
|
||||
* GitHub and other repositories provide ZIP downloads, but they often use directory names like
|
||||
* "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.
|
||||
*
|
||||
* This is a hook callback. Don't call it from a plugin.
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param string $source The directory to copy to /wp-content/plugins or /wp-content/themes. Usually a subdirectory of $remoteSource.
|
||||
* @param string $remoteSource WordPress has extracted the update to this directory.
|
||||
* @param WP_Upgrader $upgrader
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
public function fixDirectoryName($source, $remoteSource, $upgrader) {
|
||||
global $wp_filesystem;
|
||||
/** @var WP_Filesystem_Base $wp_filesystem */
|
||||
|
||||
//Basic sanity checks.
|
||||
if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
//If WordPress is upgrading anything other than our plugin/theme, leave the directory name unchanged.
|
||||
if ( !$this->isBeingUpgraded($upgrader) ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
//Rename the source to match the existing directory.
|
||||
$correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
|
||||
if ( $source !== $correctedSource ) {
|
||||
//The update archive should contain a single directory that contains the rest of plugin/theme files.
|
||||
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
|
||||
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
|
||||
//after update.
|
||||
if ( $this->isBadDirectoryStructure($remoteSource) ) {
|
||||
return new WP_Error(
|
||||
'puc-incorrect-directory-structure',
|
||||
sprintf(
|
||||
'The directory structure of the update is incorrect. All files should be inside ' .
|
||||
'a directory named <span class="code">%s</span>, not at the root of the ZIP archive.',
|
||||
htmlentities($this->slug)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var WP_Upgrader_Skin $upgrader ->skin */
|
||||
$upgrader->skin->feedback(sprintf(
|
||||
'Renaming %s to %s…',
|
||||
'<span class="code">' . basename($source) . '</span>',
|
||||
'<span class="code">' . $this->directoryName . '</span>'
|
||||
));
|
||||
|
||||
if ( $wp_filesystem->move($source, $correctedSource, true) ) {
|
||||
$upgrader->skin->feedback('Directory successfully renamed.');
|
||||
return $correctedSource;
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'puc-rename-failed',
|
||||
'Unable to rename the update to match the existing directory.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there an update being installed right now, for this plugin or theme?
|
||||
*
|
||||
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isBeingUpgraded($upgrader = null);
|
||||
|
||||
/**
|
||||
* Check for incorrect update directory structure. An update must contain a single directory,
|
||||
* all other files should be inside that directory.
|
||||
*
|
||||
* @param string $remoteSource Directory path.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBadDirectoryStructure($remoteSource) {
|
||||
global $wp_filesystem;
|
||||
/** @var WP_Filesystem_Base $wp_filesystem */
|
||||
|
||||
$sourceFiles = $wp_filesystem->dirlist($remoteSource);
|
||||
if ( is_array($sourceFiles) ) {
|
||||
$sourceFiles = array_keys($sourceFiles);
|
||||
$firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];
|
||||
return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));
|
||||
}
|
||||
|
||||
//Assume it's fine.
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* DebugBar integration
|
||||
* -------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialize the update checker Debug Bar plugin/add-on thingy.
|
||||
*/
|
||||
public function maybeInitDebugBar() {
|
||||
if ( class_exists('Debug_Bar', false) && file_exists(dirname(__FILE__) . '/DebugBar') ) {
|
||||
$this->createDebugBarExtension();
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDebugBarExtension() {
|
||||
return new Puc_v4p9_DebugBar_Extension($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display additional configuration details in the Debug Bar panel.
|
||||
*
|
||||
* @param Puc_v4p9_DebugBar_Panel $panel
|
||||
*/
|
||||
public function onDisplayConfiguration($panel) {
|
||||
//Do nothing. Subclasses can use this to add additional info to the panel.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_UpgraderStatus', false) ):
|
||||
|
||||
/**
|
||||
* A utility class that helps figure out which plugin or theme WordPress is upgrading.
|
||||
*
|
||||
* It may seem strange to have a separate class just for that, but the task is surprisingly complicated.
|
||||
* Core classes like Plugin_Upgrader don't expose the plugin file name during an in-progress update (AFAICT).
|
||||
* This class uses a few workarounds and heuristics to get the file name.
|
||||
*/
|
||||
class Puc_v4p9_UpgraderStatus {
|
||||
private $currentType = null; //"plugin" or "theme".
|
||||
private $currentId = null; //Plugin basename or theme directory name.
|
||||
|
||||
public function __construct() {
|
||||
//Keep track of which plugin/theme WordPress is currently upgrading.
|
||||
add_filter('upgrader_pre_install', array($this, 'setUpgradedThing'), 10, 2);
|
||||
add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);
|
||||
add_filter('upgrader_post_install', array($this, 'clearUpgradedThing'), 10, 1);
|
||||
add_action('upgrader_process_complete', array($this, 'clearUpgradedThing'), 10, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there and update being installed RIGHT NOW, for a specific plugin?
|
||||
*
|
||||
* Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,
|
||||
* and upgrader implementations are liable to change without notice.
|
||||
*
|
||||
* @param string $pluginFile The plugin to check.
|
||||
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool True if the plugin identified by $pluginFile is being upgraded.
|
||||
*/
|
||||
public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {
|
||||
return $this->isBeingUpgraded('plugin', $pluginFile, $upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there an update being installed for a specific theme?
|
||||
*
|
||||
* @param string $stylesheet Theme directory name.
|
||||
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool
|
||||
*/
|
||||
public function isThemeBeingUpgraded($stylesheet, $upgrader = null) {
|
||||
return $this->isBeingUpgraded('theme', $stylesheet, $upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific theme or plugin is being upgraded.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
* @param Plugin_Upgrader|WP_Upgrader|null $upgrader
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBeingUpgraded($type, $id, $upgrader = null) {
|
||||
if ( isset($upgrader) ) {
|
||||
list($currentType, $currentId) = $this->getThingBeingUpgradedBy($upgrader);
|
||||
if ( $currentType !== null ) {
|
||||
$this->currentType = $currentType;
|
||||
$this->currentId = $currentId;
|
||||
}
|
||||
}
|
||||
return ($this->currentType === $type) && ($this->currentId === $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which theme or plugin is being upgraded by a WP_Upgrader instance.
|
||||
*
|
||||
* Returns an array with two items. The first item is the type of the thing that's being
|
||||
* upgraded: "plugin" or "theme". The second item is either the plugin basename or
|
||||
* the theme directory name. If we can't determine what the upgrader is doing, both items
|
||||
* will be NULL.
|
||||
*
|
||||
* Examples:
|
||||
* ['plugin', 'plugin-dir-name/plugin.php']
|
||||
* ['theme', 'theme-dir-name']
|
||||
*
|
||||
* @param Plugin_Upgrader|WP_Upgrader $upgrader
|
||||
* @return array
|
||||
*/
|
||||
private function getThingBeingUpgradedBy($upgrader) {
|
||||
if ( !isset($upgrader, $upgrader->skin) ) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
//Figure out which plugin or theme is being upgraded.
|
||||
$pluginFile = null;
|
||||
$themeDirectoryName = null;
|
||||
|
||||
$skin = $upgrader->skin;
|
||||
if ( isset($skin->theme_info) && ($skin->theme_info instanceof WP_Theme) ) {
|
||||
$themeDirectoryName = $skin->theme_info->get_stylesheet();
|
||||
} elseif ( $skin instanceof Plugin_Upgrader_Skin ) {
|
||||
if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {
|
||||
$pluginFile = $skin->plugin;
|
||||
}
|
||||
} elseif ( $skin instanceof Theme_Upgrader_Skin ) {
|
||||
if ( isset($skin->theme) && is_string($skin->theme) && ($skin->theme !== '') ) {
|
||||
$themeDirectoryName = $skin->theme;
|
||||
}
|
||||
} elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {
|
||||
//This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin
|
||||
//filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can
|
||||
//do is compare those headers to the headers of installed plugins.
|
||||
$pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);
|
||||
}
|
||||
|
||||
if ( $pluginFile !== null ) {
|
||||
return array('plugin', $pluginFile);
|
||||
} elseif ( $themeDirectoryName !== null ) {
|
||||
return array('theme', $themeDirectoryName);
|
||||
}
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify an installed plugin based on its headers.
|
||||
*
|
||||
* @param array $searchHeaders The plugin file header to look for.
|
||||
* @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.
|
||||
*/
|
||||
private function identifyPluginByHeaders($searchHeaders) {
|
||||
if ( !function_exists('get_plugins') ){
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
|
||||
}
|
||||
|
||||
$installedPlugins = get_plugins();
|
||||
$matches = array();
|
||||
foreach($installedPlugins as $pluginBasename => $headers) {
|
||||
$diff1 = array_diff_assoc($headers, $searchHeaders);
|
||||
$diff2 = array_diff_assoc($searchHeaders, $headers);
|
||||
if ( empty($diff1) && empty($diff2) ) {
|
||||
$matches[] = $pluginBasename;
|
||||
}
|
||||
}
|
||||
|
||||
//It's possible (though very unlikely) that there could be two plugins with identical
|
||||
//headers. In that case, we can't unambiguously identify the plugin that's being upgraded.
|
||||
if ( count($matches) !== 1 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return reset($matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* @access private
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param array $hookExtra
|
||||
* @return mixed Returns $input unaltered.
|
||||
*/
|
||||
public function setUpgradedThing($input, $hookExtra) {
|
||||
if ( !empty($hookExtra['plugin']) && is_string($hookExtra['plugin']) ) {
|
||||
$this->currentId = $hookExtra['plugin'];
|
||||
$this->currentType = 'plugin';
|
||||
} elseif ( !empty($hookExtra['theme']) && is_string($hookExtra['theme']) ) {
|
||||
$this->currentId = $hookExtra['theme'];
|
||||
$this->currentType = 'theme';
|
||||
} else {
|
||||
$this->currentType = null;
|
||||
$this->currentId = null;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @access private
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function setUpgradedPluginFromOptions($options) {
|
||||
if ( isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin']) ) {
|
||||
$this->currentType = 'plugin';
|
||||
$this->currentId = $options['hook_extra']['plugin'];
|
||||
} else {
|
||||
$this->currentType = null;
|
||||
$this->currentId = null;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @access private
|
||||
*
|
||||
* @param mixed $input
|
||||
* @return mixed Returns $input unaltered.
|
||||
*/
|
||||
public function clearUpgradedThing($input = null) {
|
||||
$this->currentId = null;
|
||||
$this->currentType = null;
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Utils', false) ):
|
||||
|
||||
class Puc_v4p9_Utils {
|
||||
/**
|
||||
* Get a value from a nested array or object based on a path.
|
||||
*
|
||||
* @param array|object|null $collection Get an entry from this array.
|
||||
* @param array|string $path A list of array keys in hierarchy order, or a string path like "foo.bar.baz".
|
||||
* @param mixed $default The value to return if the specified path is not found.
|
||||
* @param string $separator Path element separator. Only applies to string paths.
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get($collection, $path, $default = null, $separator = '.') {
|
||||
if ( is_string($path) ) {
|
||||
$path = explode($separator, $path);
|
||||
}
|
||||
|
||||
//Follow the $path into $input as far as possible.
|
||||
$currentValue = $collection;
|
||||
foreach ($path as $node) {
|
||||
if ( is_array($currentValue) && isset($currentValue[$node]) ) {
|
||||
$currentValue = $currentValue[$node];
|
||||
} else if ( is_object($currentValue) && isset($currentValue->$node) ) {
|
||||
$currentValue = $currentValue->$node;
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first array element that is not empty.
|
||||
*
|
||||
* @param array $values
|
||||
* @param mixed|null $default Returns this value if there are no non-empty elements.
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function findNotEmpty($values, $default = null) {
|
||||
if ( empty($values) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
foreach ($values as $value) {
|
||||
if ( !empty($value) ) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input string starts with the specified prefix.
|
||||
*
|
||||
* @param string $input
|
||||
* @param string $prefix
|
||||
* @return bool
|
||||
*/
|
||||
public static function startsWith($input, $prefix) {
|
||||
$length = strlen($prefix);
|
||||
return (substr($input, 0, $length) === $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Vcs_Api') ):
|
||||
|
||||
abstract class Puc_v4p9_Vcs_Api {
|
||||
protected $tagNameProperty = 'name';
|
||||
protected $slug = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $repositoryUrl = '';
|
||||
|
||||
/**
|
||||
* @var mixed Authentication details for private repositories. Format depends on service.
|
||||
*/
|
||||
protected $credentials = null;
|
||||
|
||||
/**
|
||||
* @var string The filter tag that's used to filter options passed to wp_remote_get.
|
||||
* For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
|
||||
*/
|
||||
protected $httpFilterName = '';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $localDirectory = null;
|
||||
|
||||
/**
|
||||
* Puc_v4p9_Vcs_Api constructor.
|
||||
*
|
||||
* @param string $repositoryUrl
|
||||
* @param array|string|null $credentials
|
||||
*/
|
||||
public function __construct($repositoryUrl, $credentials = null) {
|
||||
$this->repositoryUrl = $repositoryUrl;
|
||||
$this->setAuthentication($credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRepositoryUrl() {
|
||||
return $this->repositoryUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which reference (i.e tag or branch) contains the latest version.
|
||||
*
|
||||
* @param string $configBranch Start looking in this branch.
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
abstract public function chooseReference($configBranch);
|
||||
|
||||
/**
|
||||
* Get the readme.txt file from the remote repository and parse it
|
||||
* according to the plugin readme standard.
|
||||
*
|
||||
* @param string $ref Tag or branch name.
|
||||
* @return array Parsed readme.
|
||||
*/
|
||||
public function getRemoteReadme($ref = 'master') {
|
||||
$fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
|
||||
if ( empty($fileContents) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$parser = new PucReadmeParser();
|
||||
return $parser->parse_readme_contents($fileContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the case-sensitive name of the local readme.txt file.
|
||||
*
|
||||
* In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
|
||||
* "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
|
||||
* capitalization.
|
||||
*
|
||||
* Defaults to "readme.txt" (all lowercase).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalReadmeName() {
|
||||
static $fileName = null;
|
||||
if ( $fileName !== null ) {
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = 'readme.txt';
|
||||
if ( isset($this->localDirectory) ) {
|
||||
$files = scandir($this->localDirectory);
|
||||
if ( !empty($files) ) {
|
||||
foreach ($files as $possibleFileName) {
|
||||
if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
|
||||
$fileName = $possibleFileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
abstract public function getBranch($branchName);
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
abstract public function getTag($tagName);
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
* (Implementations should skip pre-release versions if possible.)
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
abstract public function getLatestTag();
|
||||
|
||||
/**
|
||||
* Check if a tag name string looks like a version number.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
protected function looksLikeVersion($name) {
|
||||
//Tag names may be prefixed with "v", e.g. "v1.2.3".
|
||||
$name = ltrim($name, 'v');
|
||||
|
||||
//The version string must start with a number.
|
||||
if ( !is_numeric(substr($name, 0, 1)) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
|
||||
return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag appears to be named like a version number.
|
||||
*
|
||||
* @param stdClass $tag
|
||||
* @return bool
|
||||
*/
|
||||
protected function isVersionTag($tag) {
|
||||
$property = $this->tagNameProperty;
|
||||
return isset($tag->$property) && $this->looksLikeVersion($tag->$property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a list of tags as if they were version numbers.
|
||||
* Tags that don't look like version number will be removed.
|
||||
*
|
||||
* @param stdClass[] $tags Array of tag objects.
|
||||
* @return stdClass[] Filtered array of tags sorted in descending order.
|
||||
*/
|
||||
protected function sortTagsByVersion($tags) {
|
||||
//Keep only those tags that look like version numbers.
|
||||
$versionTags = array_filter($tags, array($this, 'isVersionTag'));
|
||||
//Sort them in descending order.
|
||||
usort($versionTags, array($this, 'compareTagNames'));
|
||||
|
||||
return $versionTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two tags as if they were version number.
|
||||
*
|
||||
* @param stdClass $tag1 Tag object.
|
||||
* @param stdClass $tag2 Another tag object.
|
||||
* @return int
|
||||
*/
|
||||
protected function compareTagNames($tag1, $tag2) {
|
||||
$property = $this->tagNameProperty;
|
||||
if ( !isset($tag1->$property) ) {
|
||||
return 1;
|
||||
}
|
||||
if ( !isset($tag2->$property) ) {
|
||||
return -1;
|
||||
}
|
||||
return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
|
||||
*/
|
||||
abstract public function getRemoteFile($path, $ref = 'master');
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
abstract public function getLatestCommitTime($ref);
|
||||
|
||||
/**
|
||||
* Get the contents of the changelog file from the repository.
|
||||
*
|
||||
* @param string $ref
|
||||
* @param string $localDirectory Full path to the local plugin or theme directory.
|
||||
* @return null|string The HTML contents of the changelog.
|
||||
*/
|
||||
public function getRemoteChangelog($ref, $localDirectory) {
|
||||
$filename = $this->findChangelogName($localDirectory);
|
||||
if ( empty($filename) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$changelog = $this->getRemoteFile($filename, $ref);
|
||||
if ( $changelog === null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
return Parsedown::instance()->text($changelog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the name of the changelog file.
|
||||
*
|
||||
* @param string $directory
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findChangelogName($directory = null) {
|
||||
if ( !isset($directory) ) {
|
||||
$directory = $this->localDirectory;
|
||||
}
|
||||
if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
|
||||
$files = scandir($directory);
|
||||
$foundNames = array_intersect($possibleNames, $files);
|
||||
|
||||
if ( !empty($foundNames) ) {
|
||||
return reset($foundNames);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param $credentials
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
$this->credentials = $credentials;
|
||||
}
|
||||
|
||||
public function isAuthenticationEnabled() {
|
||||
return !empty($this->credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function signDownloadUrl($url) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filterName
|
||||
*/
|
||||
public function setHttpFilterName($filterName) {
|
||||
$this->httpFilterName = $filterName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
*/
|
||||
public function setLocalDirectory($directory) {
|
||||
if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
|
||||
$this->localDirectory = null;
|
||||
} else {
|
||||
$this->localDirectory = $directory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
*/
|
||||
public function setSlug($slug) {
|
||||
$this->slug = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
if ( !interface_exists('Puc_v4p9_Vcs_BaseChecker', false) ):
|
||||
|
||||
interface Puc_v4p9_Vcs_BaseChecker {
|
||||
/**
|
||||
* Set the repository branch to use for updates. Defaults to 'master'.
|
||||
*
|
||||
* @param string $branch
|
||||
* @return $this
|
||||
*/
|
||||
public function setBranch($branch);
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param array|string $credentials
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthentication($credentials);
|
||||
|
||||
/**
|
||||
* @return Puc_v4p9_Vcs_Api
|
||||
*/
|
||||
public function getVcsApi();
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Vcs_BitBucketApi', false) ):
|
||||
|
||||
class Puc_v4p9_Vcs_BitBucketApi extends Puc_v4p9_Vcs_Api {
|
||||
/**
|
||||
* @var Puc_v4p9_OAuthSignature
|
||||
*/
|
||||
private $oauth = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct($repositoryUrl, $credentials = array()) {
|
||||
$path = parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
$this->username = $matches['username'];
|
||||
$this->repository = $matches['repository'];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid BitBucket repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which reference (i.e tag or branch) contains the latest version.
|
||||
*
|
||||
* @param string $configBranch Start looking in this branch.
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
public function chooseReference($configBranch) {
|
||||
$updateSource = null;
|
||||
|
||||
//Check if there's a "Stable tag: 1.2.3" header that points to a valid tag.
|
||||
$updateSource = $this->getStableTag($configBranch);
|
||||
|
||||
//Look for version-like tags.
|
||||
if ( !$updateSource && ($configBranch === 'master') ) {
|
||||
$updateSource = $this->getLatestTag();
|
||||
}
|
||||
//If all else fails, use the specified branch itself.
|
||||
if ( !$updateSource ) {
|
||||
$updateSource = $this->getBranch($configBranch);
|
||||
}
|
||||
|
||||
return $updateSource;
|
||||
}
|
||||
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/refs/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $branch->name,
|
||||
'updated' => $branch->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($branch->name),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
$tag = $this->api('/refs/tags/' . $tagName);
|
||||
if ( is_wp_error($tag) || empty($tag) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'updated' => $tag->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($tag->name),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/refs/tags?sort=-target.date');
|
||||
if ( !isset($tags, $tags->values) || !is_array($tags->values) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Filter and sort the list of tags.
|
||||
$versionTags = $this->sortTagsByVersion($tags->values);
|
||||
|
||||
//Return the first result.
|
||||
if ( !empty($versionTags) ) {
|
||||
$tag = $versionTags[0];
|
||||
return new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'updated' => $tag->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($tag->name),
|
||||
));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag/ref specified by the "Stable tag" header in the readme.txt of a given branch.
|
||||
*
|
||||
* @param string $branch
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
protected function getStableTag($branch) {
|
||||
$remoteReadme = $this->getRemoteReadme($branch);
|
||||
if ( !empty($remoteReadme['stable_tag']) ) {
|
||||
$tag = $remoteReadme['stable_tag'];
|
||||
|
||||
//You can explicitly opt out of using tags by setting "Stable tag" to
|
||||
//"trunk" or the name of the current branch.
|
||||
if ( ($tag === $branch) || ($tag === 'trunk') ) {
|
||||
return $this->getBranch($branch);
|
||||
}
|
||||
|
||||
return $this->getTag($tag);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadUrl($ref) {
|
||||
return sprintf(
|
||||
'https://bitbucket.org/%s/%s/get/%s.zip',
|
||||
$this->username,
|
||||
$this->repository,
|
||||
$ref
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
|
||||
*/
|
||||
public function getRemoteFile($path, $ref = 'master') {
|
||||
$response = $this->api('src/' . $ref . '/' . ltrim($path));
|
||||
if ( is_wp_error($response) || !is_string($response) ) {
|
||||
return null;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLatestCommitTime($ref) {
|
||||
$response = $this->api('commits/' . $ref);
|
||||
if ( isset($response->values, $response->values[0], $response->values[0]->date) ) {
|
||||
return $response->values[0]->date;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a BitBucket API 2.0 request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $version
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
public function api($url, $version = '2.0') {
|
||||
$url = ltrim($url, '/');
|
||||
$isSrcResource = Puc_v4p9_Utils::startsWith($url, 'src/');
|
||||
|
||||
$url = implode('/', array(
|
||||
'https://api.bitbucket.org',
|
||||
$version,
|
||||
'repositories',
|
||||
$this->username,
|
||||
$this->repository,
|
||||
$url
|
||||
));
|
||||
$baseUrl = $url;
|
||||
|
||||
if ( $this->oauth ) {
|
||||
$url = $this->oauth->sign($url,'GET');
|
||||
}
|
||||
|
||||
$options = array('timeout' => 10);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
if ( $isSrcResource ) {
|
||||
//Most responses are JSON-encoded, but src resources just
|
||||
//return raw file contents.
|
||||
$document = $body;
|
||||
} else {
|
||||
$document = json_decode($body);
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
$error = new WP_Error(
|
||||
'puc-bitbucket-http-error',
|
||||
sprintf('BitBucket API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $credentials
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
|
||||
if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
|
||||
$this->oauth = new Puc_v4p9_OAuthSignature(
|
||||
$credentials['consumer_key'],
|
||||
$credentials['consumer_secret']
|
||||
);
|
||||
} else {
|
||||
$this->oauth = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function signDownloadUrl($url) {
|
||||
//Add authentication data to download URLs. Since OAuth signatures incorporate
|
||||
//timestamps, we have to do this immediately before inserting the update. Otherwise
|
||||
//authentication could fail due to a stale timestamp.
|
||||
if ( $this->oauth ) {
|
||||
$url = $this->oauth->sign($url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Vcs_GitHubApi', false) ):
|
||||
|
||||
class Puc_v4p9_Vcs_GitHubApi extends Puc_v4p9_Vcs_Api {
|
||||
/**
|
||||
* @var string GitHub username.
|
||||
*/
|
||||
protected $userName;
|
||||
/**
|
||||
* @var string GitHub repository name.
|
||||
*/
|
||||
protected $repositoryName;
|
||||
|
||||
/**
|
||||
* @var string Either a fully qualified repository URL, or just "user/repo-name".
|
||||
*/
|
||||
protected $repositoryUrl;
|
||||
|
||||
/**
|
||||
* @var string GitHub authentication token. Optional.
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* @var bool Whether to download release assets instead of the auto-generated source code archives.
|
||||
*/
|
||||
protected $releaseAssetsEnabled = false;
|
||||
|
||||
/**
|
||||
* @var string|null Regular expression that's used to filter release assets by name. Optional.
|
||||
*/
|
||||
protected $assetFilterRegex = null;
|
||||
|
||||
/**
|
||||
* @var string|null The unchanging part of a release asset URL. Used to identify download attempts.
|
||||
*/
|
||||
protected $assetApiBaseUrl = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $downloadFilterAdded = false;
|
||||
|
||||
public function __construct($repositoryUrl, $accessToken = null) {
|
||||
$path = parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
$this->userName = $matches['username'];
|
||||
$this->repositoryName = $matches['repository'];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest release from GitHub.
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getLatestRelease() {
|
||||
$release = $this->api('/repos/:user/:repo/releases/latest');
|
||||
if ( is_wp_error($release) || !is_object($release) || !isset($release->tag_name) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $release->tag_name,
|
||||
'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
|
||||
'downloadUrl' => $release->zipball_url,
|
||||
'updated' => $release->created_at,
|
||||
'apiResponse' => $release,
|
||||
));
|
||||
|
||||
if ( isset($release->assets[0]) ) {
|
||||
$reference->downloadCount = $release->assets[0]->download_count;
|
||||
}
|
||||
|
||||
if ( $this->releaseAssetsEnabled && isset($release->assets, $release->assets[0]) ) {
|
||||
//Use the first release asset that matches the specified regular expression.
|
||||
$matchingAssets = array_filter($release->assets, array($this, 'matchesAssetFilter'));
|
||||
if ( !empty($matchingAssets) ) {
|
||||
if ( $this->isAuthenticationEnabled() ) {
|
||||
/**
|
||||
* Keep in mind that we'll need to add an "Accept" header to download this asset.
|
||||
*
|
||||
* @see setUpdateDownloadHeaders()
|
||||
*/
|
||||
$reference->downloadUrl = $matchingAssets[0]->url;
|
||||
} else {
|
||||
//It seems that browser_download_url only works for public repositories.
|
||||
//Using an access_token doesn't help. Maybe OAuth would work?
|
||||
$reference->downloadUrl = $matchingAssets[0]->browser_download_url;
|
||||
}
|
||||
|
||||
$reference->downloadCount = $matchingAssets[0]->download_count;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty($release->body) ) {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
$reference->changelog = Parsedown::instance()->text($release->body);
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/repos/:user/:repo/tags');
|
||||
|
||||
if ( is_wp_error($tags) || !is_array($tags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$versionTags = $this->sortTagsByVersion($tags);
|
||||
if ( empty($versionTags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tag = $versionTags[0];
|
||||
return new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'downloadUrl' => $tag->zipball_url,
|
||||
'apiResponse' => $tag,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch by name.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $branch->name,
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
|
||||
'apiResponse' => $branch,
|
||||
));
|
||||
|
||||
if ( isset($branch->commit, $branch->commit->commit, $branch->commit->commit->author->date) ) {
|
||||
$reference->updated = $branch->commit->commit->author->date;
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest commit that changed the specified file.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return StdClass|null
|
||||
*/
|
||||
public function getLatestCommit($filename, $ref = 'master') {
|
||||
$commits = $this->api(
|
||||
'/repos/:user/:repo/commits',
|
||||
array(
|
||||
'path' => $filename,
|
||||
'sha' => $ref,
|
||||
)
|
||||
);
|
||||
if ( !is_wp_error($commits) && isset($commits[0]) ) {
|
||||
return $commits[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLatestCommitTime($ref) {
|
||||
$commits = $this->api('/repos/:user/:repo/commits', array('sha' => $ref));
|
||||
if ( !is_wp_error($commits) && isset($commits[0]) ) {
|
||||
return $commits[0]->commit->author->date;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a GitHub API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
protected function api($url, $queryParams = array()) {
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => 10);
|
||||
if ( $this->isAuthenticationEnabled() ) {
|
||||
$options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
|
||||
}
|
||||
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
$document = json_decode($body);
|
||||
return $document;
|
||||
}
|
||||
|
||||
$error = new WP_Error(
|
||||
'puc-github-http-error',
|
||||
sprintf('GitHub API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully qualified URL for an API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return string
|
||||
*/
|
||||
protected function buildApiUrl($url, $queryParams) {
|
||||
$variables = array(
|
||||
'user' => $this->userName,
|
||||
'repo' => $this->repositoryName,
|
||||
);
|
||||
foreach ($variables as $name => $value) {
|
||||
$url = str_replace('/:' . $name, '/' . urlencode($value), $url);
|
||||
}
|
||||
$url = 'https://api.github.com' . $url;
|
||||
|
||||
if ( !empty($queryParams) ) {
|
||||
$url = add_query_arg($queryParams, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
|
||||
*/
|
||||
public function getRemoteFile($path, $ref = 'master') {
|
||||
$apiUrl = '/repos/:user/:repo/contents/' . $path;
|
||||
$response = $this->api($apiUrl, array('ref' => $ref));
|
||||
|
||||
if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
|
||||
return null;
|
||||
}
|
||||
return base64_decode($response->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to download a ZIP archive of the specified branch/tag/etc.
|
||||
*
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
public function buildArchiveDownloadUrl($ref = 'master') {
|
||||
$url = sprintf(
|
||||
'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
|
||||
urlencode($this->userName),
|
||||
urlencode($this->repositoryName),
|
||||
urlencode($ref)
|
||||
);
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return void
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
//The current GitHub update checker doesn't use getTag, so I didn't bother to implement it.
|
||||
throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
|
||||
}
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
$this->accessToken = is_string($credentials) ? $credentials : null;
|
||||
|
||||
//Optimization: Instead of filtering all HTTP requests, let's do it only when
|
||||
//WordPress is about to download an update.
|
||||
add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which reference (i.e tag or branch) contains the latest version.
|
||||
*
|
||||
* @param string $configBranch Start looking in this branch.
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
public function chooseReference($configBranch) {
|
||||
$updateSource = null;
|
||||
|
||||
if ( $configBranch === 'master' ) {
|
||||
//Use the latest release.
|
||||
$updateSource = $this->getLatestRelease();
|
||||
if ( $updateSource === null ) {
|
||||
//Failing that, use the tag with the highest version number.
|
||||
$updateSource = $this->getLatestTag();
|
||||
}
|
||||
}
|
||||
//Alternatively, just use the branch itself.
|
||||
if ( empty($updateSource) ) {
|
||||
$updateSource = $this->getBranch($configBranch);
|
||||
}
|
||||
|
||||
return $updateSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable updating via release assets.
|
||||
*
|
||||
* If the latest release contains no usable assets, the update checker
|
||||
* will fall back to using the automatically generated ZIP archive.
|
||||
*
|
||||
* Private repositories will only work with WordPress 3.7 or later.
|
||||
*
|
||||
* @param string|null $fileNameRegex Optional. Use only those assets where the file name matches this regex.
|
||||
*/
|
||||
public function enableReleaseAssets($fileNameRegex = null) {
|
||||
$this->releaseAssetsEnabled = true;
|
||||
$this->assetFilterRegex = $fileNameRegex;
|
||||
$this->assetApiBaseUrl = sprintf(
|
||||
'//api.github.com/repos/%1$s/%2$s/releases/assets/',
|
||||
$this->userName,
|
||||
$this->repositoryName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this asset match the file name regex?
|
||||
*
|
||||
* @param stdClass $releaseAsset
|
||||
* @return bool
|
||||
*/
|
||||
protected function matchesAssetFilter($releaseAsset) {
|
||||
if ( $this->assetFilterRegex === null ) {
|
||||
//The default is to accept all assets.
|
||||
return true;
|
||||
}
|
||||
return isset($releaseAsset->name) && preg_match($this->assetFilterRegex, $releaseAsset->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param bool $result
|
||||
* @return bool
|
||||
*/
|
||||
public function addHttpRequestFilter($result) {
|
||||
if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
|
||||
add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
|
||||
add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
|
||||
$this->downloadFilterAdded = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP headers that are necessary to download updates from private repositories.
|
||||
*
|
||||
* See GitHub docs:
|
||||
* @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
|
||||
* @link https://developer.github.com/v3/auth/#basic-authentication
|
||||
*
|
||||
* @internal
|
||||
* @param array $requestArgs
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function setUpdateDownloadHeaders($requestArgs, $url = '') {
|
||||
//Is WordPress trying to download one of our release assets?
|
||||
if ( $this->releaseAssetsEnabled && (strpos($url, $this->assetApiBaseUrl) !== false) ) {
|
||||
$requestArgs['headers']['Accept'] = 'application/octet-stream';
|
||||
}
|
||||
//Use Basic authentication, but only if the download is from our repository.
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
|
||||
$requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
|
||||
}
|
||||
return $requestArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* When following a redirect, the Requests library will automatically forward
|
||||
* the authorization header to other hosts. We don't want that because it breaks
|
||||
* AWS downloads and can leak authorization information.
|
||||
*
|
||||
* @internal
|
||||
* @param string $location
|
||||
* @param array $headers
|
||||
*/
|
||||
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( strpos($location, $repoApiBaseUrl) === 0 ) {
|
||||
return; //This request is going to GitHub, so it's fine.
|
||||
}
|
||||
//Remove the header.
|
||||
if ( isset($headers['Authorization']) ) {
|
||||
unset($headers['Authorization']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the value of the "Authorization" header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizationHeader() {
|
||||
return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Vcs_GitLabApi', false) ):
|
||||
|
||||
class Puc_v4p9_Vcs_GitLabApi extends Puc_v4p9_Vcs_Api {
|
||||
/**
|
||||
* @var string GitLab username.
|
||||
*/
|
||||
protected $userName;
|
||||
|
||||
/**
|
||||
* @var string GitLab server host.
|
||||
*/
|
||||
protected $repositoryHost;
|
||||
|
||||
/**
|
||||
* @var string Protocol used by this GitLab server: "http" or "https".
|
||||
*/
|
||||
protected $repositoryProtocol = 'https';
|
||||
|
||||
/**
|
||||
* @var string GitLab repository name.
|
||||
*/
|
||||
protected $repositoryName;
|
||||
|
||||
/**
|
||||
* @var string GitLab authentication token. Optional.
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
public function __construct($repositoryUrl, $accessToken = null, $subgroup = null) {
|
||||
//Parse the repository host to support custom hosts.
|
||||
$port = parse_url($repositoryUrl, PHP_URL_PORT);
|
||||
if ( !empty($port) ) {
|
||||
$port = ':' . $port;
|
||||
}
|
||||
$this->repositoryHost = parse_url($repositoryUrl, PHP_URL_HOST) . $port;
|
||||
|
||||
if ( $this->repositoryHost !== 'gitlab.com' ) {
|
||||
$this->repositoryProtocol = parse_url($repositoryUrl, PHP_URL_SCHEME);
|
||||
}
|
||||
|
||||
//Find the repository information
|
||||
$path = parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
$this->userName = $matches['username'];
|
||||
$this->repositoryName = $matches['repository'];
|
||||
} elseif ( ($this->repositoryHost === 'gitlab.com') ) {
|
||||
//This is probably a repository in a subgroup, e.g. "/organization/category/repo".
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
if ( count($parts) < 3 ) {
|
||||
throw new InvalidArgumentException('Invalid GitLab.com repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
$lastPart = array_pop($parts);
|
||||
$this->userName = implode('/', $parts);
|
||||
$this->repositoryName = $lastPart;
|
||||
} else {
|
||||
//There could be subgroups in the URL: gitlab.domain.com/group/subgroup/subgroup2/repository
|
||||
if ( $subgroup !== null ) {
|
||||
$path = str_replace(trailingslashit($subgroup), '', $path);
|
||||
}
|
||||
|
||||
//This is not a traditional url, it could be gitlab is in a deeper subdirectory.
|
||||
//Get the path segments.
|
||||
$segments = explode('/', untrailingslashit(ltrim($path, '/')));
|
||||
|
||||
//We need at least /user-name/repository-name/
|
||||
if ( count($segments) < 2 ) {
|
||||
throw new InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
|
||||
//Get the username and repository name.
|
||||
$usernameRepo = array_splice($segments, -2, 2);
|
||||
$this->userName = $usernameRepo[0];
|
||||
$this->repositoryName = $usernameRepo[1];
|
||||
|
||||
//Append the remaining segments to the host if there are segments left.
|
||||
if ( count($segments) > 0 ) {
|
||||
$this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
|
||||
}
|
||||
|
||||
//Add subgroups to username.
|
||||
if ( $subgroup !== null ) {
|
||||
$this->userName = $usernameRepo[0] . '/' . untrailingslashit($subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest release from GitLab.
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getLatestRelease() {
|
||||
return $this->getLatestTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Puc_v4p9_Vcs_Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/:id/repository/tags');
|
||||
if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$versionTags = $this->sortTagsByVersion($tags);
|
||||
if ( empty($versionTags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tag = $versionTags[0];
|
||||
return new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
|
||||
'apiResponse' => $tag,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch by name.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/:id/repository/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = new Puc_v4p9_Vcs_Reference(array(
|
||||
'name' => $branch->name,
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
|
||||
'apiResponse' => $branch,
|
||||
));
|
||||
|
||||
if ( isset($branch->commit, $branch->commit->committed_date) ) {
|
||||
$reference->updated = $branch->commit->committed_date;
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLatestCommitTime($ref) {
|
||||
$commits = $this->api('/:id/repository/commits/', array('ref_name' => $ref));
|
||||
if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $commits[0]->committed_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a GitLab API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
protected function api($url, $queryParams = array()) {
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => 10);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
return json_decode($body);
|
||||
}
|
||||
|
||||
$error = new WP_Error(
|
||||
'puc-gitlab-http-error',
|
||||
sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully qualified URL for an API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return string
|
||||
*/
|
||||
protected function buildApiUrl($url, $queryParams) {
|
||||
$variables = array(
|
||||
'user' => $this->userName,
|
||||
'repo' => $this->repositoryName,
|
||||
'id' => $this->userName . '/' . $this->repositoryName,
|
||||
);
|
||||
|
||||
foreach ($variables as $name => $value) {
|
||||
$url = str_replace("/:{$name}", '/' . urlencode($value), $url);
|
||||
}
|
||||
|
||||
$url = substr($url, 1);
|
||||
$url = sprintf('%1$s://%2$s/api/v4/projects/%3$s', $this->repositoryProtocol, $this->repositoryHost, $url);
|
||||
|
||||
if ( !empty($this->accessToken) ) {
|
||||
$queryParams['private_token'] = $this->accessToken;
|
||||
}
|
||||
|
||||
if ( !empty($queryParams) ) {
|
||||
$url = add_query_arg($queryParams, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
|
||||
*/
|
||||
public function getRemoteFile($path, $ref = 'master') {
|
||||
$response = $this->api('/:id/repository/files/' . $path, array('ref' => $ref));
|
||||
if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64_decode($response->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to download a ZIP archive of the specified branch/tag/etc.
|
||||
*
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
public function buildArchiveDownloadUrl($ref = 'master') {
|
||||
$url = sprintf(
|
||||
'%1$s://%2$s/api/v4/projects/%3$s/repository/archive.zip',
|
||||
$this->repositoryProtocol,
|
||||
$this->repositoryHost,
|
||||
urlencode($this->userName . '/' . $this->repositoryName)
|
||||
);
|
||||
$url = add_query_arg('sha', urlencode($ref), $url);
|
||||
|
||||
if ( !empty($this->accessToken) ) {
|
||||
$url = add_query_arg('private_token', $this->accessToken, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return void
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out which reference (i.e tag or branch) contains the latest version.
|
||||
*
|
||||
* @param string $configBranch Start looking in this branch.
|
||||
* @return null|Puc_v4p9_Vcs_Reference
|
||||
*/
|
||||
public function chooseReference($configBranch) {
|
||||
$updateSource = null;
|
||||
|
||||
// GitLab doesn't handle releases the same as GitHub so just use the latest tag
|
||||
if ( $configBranch === 'master' ) {
|
||||
$updateSource = $this->getLatestTag();
|
||||
}
|
||||
|
||||
if ( empty($updateSource) ) {
|
||||
$updateSource = $this->getBranch($configBranch);
|
||||
}
|
||||
|
||||
return $updateSource;
|
||||
}
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
$this->accessToken = is_string($credentials) ? $credentials : null;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Vcs_PluginUpdateChecker') ):
|
||||
|
||||
class Puc_v4p9_Vcs_PluginUpdateChecker extends Puc_v4p9_Plugin_UpdateChecker implements Puc_v4p9_Vcs_BaseChecker {
|
||||
/**
|
||||
* @var string The branch where to look for updates. Defaults to "master".
|
||||
*/
|
||||
protected $branch = 'master';
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_Vcs_Api Repository API client.
|
||||
*/
|
||||
protected $api = null;
|
||||
|
||||
/**
|
||||
* Puc_v4p9_Vcs_PluginUpdateChecker constructor.
|
||||
*
|
||||
* @param Puc_v4p9_Vcs_Api $api
|
||||
* @param string $pluginFile
|
||||
* @param string $slug
|
||||
* @param int $checkPeriod
|
||||
* @param string $optionName
|
||||
* @param string $muPluginFile
|
||||
*/
|
||||
public function __construct($api, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
|
||||
$this->api = $api;
|
||||
$this->api->setHttpFilterName($this->getUniqueName('request_info_options'));
|
||||
|
||||
parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
|
||||
|
||||
$this->api->setSlug($this->slug);
|
||||
}
|
||||
|
||||
public function requestInfo($unusedParameter = null) {
|
||||
//We have to make several remote API requests to gather all the necessary info
|
||||
//which can take a while on slow networks.
|
||||
if ( function_exists('set_time_limit') ) {
|
||||
@set_time_limit(60);
|
||||
}
|
||||
|
||||
$api = $this->api;
|
||||
$api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
|
||||
|
||||
$info = new Puc_v4p9_Plugin_Info();
|
||||
$info->filename = $this->pluginFile;
|
||||
$info->slug = $this->slug;
|
||||
|
||||
$this->setInfoFromHeader($this->package->getPluginHeader(), $info);
|
||||
|
||||
//Pick a branch or tag.
|
||||
$updateSource = $api->chooseReference($this->branch);
|
||||
if ( $updateSource ) {
|
||||
$ref = $updateSource->name;
|
||||
$info->version = $updateSource->version;
|
||||
$info->last_updated = $updateSource->updated;
|
||||
$info->download_url = $updateSource->downloadUrl;
|
||||
|
||||
if ( !empty($updateSource->changelog) ) {
|
||||
$info->sections['changelog'] = $updateSource->changelog;
|
||||
}
|
||||
if ( isset($updateSource->downloadCount) ) {
|
||||
$info->downloaded = $updateSource->downloadCount;
|
||||
}
|
||||
} else {
|
||||
//There's probably a network problem or an authentication error.
|
||||
do_action(
|
||||
'puc_api_error',
|
||||
new WP_Error(
|
||||
'puc-no-update-source',
|
||||
'Could not retrieve version information from the repository. '
|
||||
. 'This usually means that the update checker either can\'t connect '
|
||||
. 'to the repository or it\'s configured incorrectly.'
|
||||
),
|
||||
null, null, $this->slug
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
//Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata
|
||||
//are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
|
||||
$mainPluginFile = basename($this->pluginFile);
|
||||
$remotePlugin = $api->getRemoteFile($mainPluginFile, $ref);
|
||||
if ( !empty($remotePlugin) ) {
|
||||
$remoteHeader = $this->package->getFileHeader($remotePlugin);
|
||||
$this->setInfoFromHeader($remoteHeader, $info);
|
||||
}
|
||||
|
||||
//Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain
|
||||
//a lot of useful information like the required/tested WP version, changelog, and so on.
|
||||
if ( $this->readmeTxtExistsLocally() ) {
|
||||
$this->setInfoFromRemoteReadme($ref, $info);
|
||||
}
|
||||
|
||||
//The changelog might be in a separate file.
|
||||
if ( empty($info->sections['changelog']) ) {
|
||||
$info->sections['changelog'] = $api->getRemoteChangelog($ref, $this->package->getAbsoluteDirectoryPath());
|
||||
if ( empty($info->sections['changelog']) ) {
|
||||
$info->sections['changelog'] = __('没有可用的修改日志', 'kratos');
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty($info->last_updated) ) {
|
||||
//Fetch the latest commit that changed the tag or branch and use it as the "last_updated" date.
|
||||
$latestCommitTime = $api->getLatestCommitTime($ref);
|
||||
if ( $latestCommitTime !== null ) {
|
||||
$info->last_updated = $latestCommitTime;
|
||||
}
|
||||
}
|
||||
|
||||
$info = apply_filters($this->getUniqueName('request_info_result'), $info, null);
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the currently installed version has a readme.txt file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function readmeTxtExistsLocally() {
|
||||
return $this->package->fileExists($this->api->getLocalReadmeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy plugin metadata from a file header to a Plugin Info object.
|
||||
*
|
||||
* @param array $fileHeader
|
||||
* @param Puc_v4p9_Plugin_Info $pluginInfo
|
||||
*/
|
||||
protected function setInfoFromHeader($fileHeader, $pluginInfo) {
|
||||
$headerToPropertyMap = array(
|
||||
'Version' => 'version',
|
||||
'Name' => 'name',
|
||||
'PluginURI' => 'homepage',
|
||||
'Author' => 'author',
|
||||
'AuthorName' => 'author',
|
||||
'AuthorURI' => 'author_homepage',
|
||||
|
||||
'Requires WP' => 'requires',
|
||||
'Tested WP' => 'tested',
|
||||
'Requires at least' => 'requires',
|
||||
'Tested up to' => 'tested',
|
||||
);
|
||||
foreach ($headerToPropertyMap as $headerName => $property) {
|
||||
if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) {
|
||||
$pluginInfo->$property = $fileHeader[$headerName];
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty($fileHeader['Description']) ) {
|
||||
$pluginInfo->sections['description'] = $fileHeader['Description'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy plugin metadata from the remote readme.txt file.
|
||||
*
|
||||
* @param string $ref GitHub tag or branch where to look for the readme.
|
||||
* @param Puc_v4p9_Plugin_Info $pluginInfo
|
||||
*/
|
||||
protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
|
||||
$readme = $this->api->getRemoteReadme($ref);
|
||||
if ( empty($readme) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset($readme['sections']) ) {
|
||||
$pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']);
|
||||
}
|
||||
if ( !empty($readme['tested_up_to']) ) {
|
||||
$pluginInfo->tested = $readme['tested_up_to'];
|
||||
}
|
||||
if ( !empty($readme['requires_at_least']) ) {
|
||||
$pluginInfo->requires = $readme['requires_at_least'];
|
||||
}
|
||||
|
||||
if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
|
||||
$pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
|
||||
}
|
||||
}
|
||||
|
||||
public function setBranch($branch) {
|
||||
$this->branch = $branch;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
$this->api->setAuthentication($credentials);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVcsApi() {
|
||||
return $this->api;
|
||||
}
|
||||
|
||||
public function getUpdate() {
|
||||
$update = parent::getUpdate();
|
||||
|
||||
if ( isset($update) && !empty($update->download_url) ) {
|
||||
$update->download_url = $this->api->signDownloadUrl($update->download_url);
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
public function onDisplayConfiguration($panel) {
|
||||
parent::onDisplayConfiguration($panel);
|
||||
$panel->row('Branch', $this->branch);
|
||||
$panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
|
||||
$panel->row('API client', get_class($this->api));
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
if ( !class_exists('Puc_v4p9_Vcs_Reference', false) ):
|
||||
|
||||
/**
|
||||
* This class represents a VCS branch or tag. It's intended as a read only, short-lived container
|
||||
* that only exists to provide a limited degree of type checking.
|
||||
*
|
||||
* @property string $name
|
||||
* @property string|null version
|
||||
* @property string $downloadUrl
|
||||
* @property string $updated
|
||||
*
|
||||
* @property string|null $changelog
|
||||
* @property int|null $downloadCount
|
||||
*/
|
||||
class Puc_v4p9_Vcs_Reference {
|
||||
private $properties = array();
|
||||
|
||||
public function __construct($properties = array()) {
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function __get($name) {
|
||||
return array_key_exists($name, $this->properties) ? $this->properties[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __set($name, $value) {
|
||||
$this->properties[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name) {
|
||||
return isset($this->properties[$name]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('Puc_v4p9_Vcs_ThemeUpdateChecker', false) ):
|
||||
|
||||
class Puc_v4p9_Vcs_ThemeUpdateChecker extends Puc_v4p9_Theme_UpdateChecker implements Puc_v4p9_Vcs_BaseChecker {
|
||||
/**
|
||||
* @var string The branch where to look for updates. Defaults to "master".
|
||||
*/
|
||||
protected $branch = 'master';
|
||||
|
||||
/**
|
||||
* @var Puc_v4p9_Vcs_Api Repository API client.
|
||||
*/
|
||||
protected $api = null;
|
||||
|
||||
/**
|
||||
* Puc_v4p9_Vcs_ThemeUpdateChecker constructor.
|
||||
*
|
||||
* @param Puc_v4p9_Vcs_Api $api
|
||||
* @param null $stylesheet
|
||||
* @param null $customSlug
|
||||
* @param int $checkPeriod
|
||||
* @param string $optionName
|
||||
*/
|
||||
public function __construct($api, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
|
||||
$this->api = $api;
|
||||
$this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
|
||||
|
||||
parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
|
||||
|
||||
$this->api->setSlug($this->slug);
|
||||
}
|
||||
|
||||
public function requestUpdate() {
|
||||
$api = $this->api;
|
||||
$api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
|
||||
|
||||
$update = new Puc_v4p9_Theme_Update();
|
||||
$update->slug = $this->slug;
|
||||
|
||||
//Figure out which reference (tag or branch) we'll use to get the latest version of the theme.
|
||||
$updateSource = $api->chooseReference($this->branch);
|
||||
if ( $updateSource ) {
|
||||
$ref = $updateSource->name;
|
||||
$update->download_url = $updateSource->downloadUrl;
|
||||
} else {
|
||||
do_action(
|
||||
'puc_api_error',
|
||||
new WP_Error(
|
||||
'puc-no-update-source',
|
||||
'Could not retrieve version information from the repository. '
|
||||
. 'This usually means that the update checker either can\'t connect '
|
||||
. 'to the repository or it\'s configured incorrectly.'
|
||||
),
|
||||
null, null, $this->slug
|
||||
);
|
||||
$ref = $this->branch;
|
||||
}
|
||||
|
||||
//Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata
|
||||
//are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
|
||||
$remoteHeader = $this->package->getFileHeader($api->getRemoteFile('style.css', $ref));
|
||||
$update->version = Puc_v4p9_Utils::findNotEmpty(array(
|
||||
$remoteHeader['Version'],
|
||||
Puc_v4p9_Utils::get($updateSource, 'version'),
|
||||
));
|
||||
|
||||
//The details URL defaults to the Theme URI header or the repository URL.
|
||||
$update->details_url = Puc_v4p9_Utils::findNotEmpty(array(
|
||||
$remoteHeader['ThemeURI'],
|
||||
$this->package->getHeaderValue('ThemeURI'),
|
||||
$this->metadataUrl,
|
||||
));
|
||||
|
||||
if ( empty($update->version) ) {
|
||||
//It looks like we didn't find a valid update after all.
|
||||
$update = null;
|
||||
}
|
||||
|
||||
$update = $this->filterUpdateResult($update);
|
||||
return $update;
|
||||
}
|
||||
|
||||
//FIXME: This is duplicated code. Both theme and plugin subclasses that use VCS share these methods.
|
||||
|
||||
public function setBranch($branch) {
|
||||
$this->branch = $branch;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
$this->api->setAuthentication($credentials);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVcsApi() {
|
||||
return $this->api;
|
||||
}
|
||||
|
||||
public function getUpdate() {
|
||||
$update = parent::getUpdate();
|
||||
|
||||
if ( isset($update) && !empty($update->download_url) ) {
|
||||
$update->download_url = $this->api->signDownloadUrl($update->download_url);
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
public function onDisplayConfiguration($panel) {
|
||||
parent::onDisplayConfiguration($panel);
|
||||
$panel->row('Branch', $this->branch);
|
||||
$panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
|
||||
$panel->row('API client', get_class($this->api));
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Update Checker Library 4.9
|
||||
* http://w-shadow.com/
|
||||
*
|
||||
* Copyright 2020 Janis Elsts
|
||||
* Released under the MIT license. See license.txt for details.
|
||||
*/
|
||||
|
||||
require dirname(__FILE__) . '/Puc/v4p9/Autoloader.php';
|
||||
new Puc_v4p9_Autoloader();
|
||||
|
||||
require dirname(__FILE__) . '/Puc/v4p9/Factory.php';
|
||||
require dirname(__FILE__) . '/Puc/v4/Factory.php';
|
||||
|
||||
//Register classes defined in this version with the factory.
|
||||
foreach (
|
||||
array(
|
||||
'Plugin_UpdateChecker' => 'Puc_v4p9_Plugin_UpdateChecker',
|
||||
'Theme_UpdateChecker' => 'Puc_v4p9_Theme_UpdateChecker',
|
||||
|
||||
'Vcs_PluginUpdateChecker' => 'Puc_v4p9_Vcs_PluginUpdateChecker',
|
||||
'Vcs_ThemeUpdateChecker' => 'Puc_v4p9_Vcs_ThemeUpdateChecker',
|
||||
|
||||
'GitHubApi' => 'Puc_v4p9_Vcs_GitHubApi',
|
||||
'BitBucketApi' => 'Puc_v4p9_Vcs_BitBucketApi',
|
||||
'GitLabApi' => 'Puc_v4p9_Vcs_GitLabApi',
|
||||
)
|
||||
as $pucGeneralClass => $pucVersionedClass
|
||||
) {
|
||||
Puc_v4_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.9');
|
||||
//Also add it to the minor-version factory in case the major-version factory
|
||||
//was already defined by another, older version of the update checker.
|
||||
Puc_v4p9_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.9');
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.puc-debug-bar-panel-v4 pre {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Style the debug data table to match "widefat" table style used by WordPress. */
|
||||
table.puc-debug-data {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
margin: 0;
|
||||
|
||||
border-spacing: 0;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
table.puc-debug-data * {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
table.puc-debug-data th {
|
||||
width: 11em;
|
||||
padding: 7px 7px 8px;
|
||||
text-align: left;
|
||||
|
||||
font-family: "Georgia", "Times New Roman", "Bitstream Charter", "Times", serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.3em;
|
||||
text-shadow: rgba(255, 255, 255, 0.804) 0 1px 0;
|
||||
}
|
||||
|
||||
table.puc-debug-data td, table.puc-debug-data th {
|
||||
border-width: 1px 0;
|
||||
border-style: solid;
|
||||
|
||||
border-top-color: #fff;
|
||||
border-bottom-color: #dfdfdf;
|
||||
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
table.puc-debug-data td {
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
padding: 4px 7px 2px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.puc-ajax-response {
|
||||
border: 1px solid #dfdfdf;
|
||||
border-radius: 3px;
|
||||
padding: 0.5em;
|
||||
margin: 5px 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.puc-ajax-nonce {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.puc-ajax-response dt {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.puc-ajax-response dd {
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
jQuery(function($) {
|
||||
|
||||
function runAjaxAction(button, action) {
|
||||
button = $(button);
|
||||
var panel = button.closest('.puc-debug-bar-panel-v4');
|
||||
var responseBox = button.closest('td').find('.puc-ajax-response');
|
||||
|
||||
responseBox.text('Processing...').show();
|
||||
$.post(
|
||||
ajaxurl,
|
||||
{
|
||||
action : action,
|
||||
uid : panel.data('uid'),
|
||||
_wpnonce: panel.data('nonce')
|
||||
},
|
||||
function(data) {
|
||||
responseBox.html(data);
|
||||
},
|
||||
'html'
|
||||
);
|
||||
}
|
||||
|
||||
$('.puc-debug-bar-panel-v4 input[name="puc-check-now-button"]').click(function() {
|
||||
runAjaxAction(this, 'puc_v4_debug_check_now');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.puc-debug-bar-panel-v4 input[name="puc-request-info-button"]').click(function() {
|
||||
runAjaxAction(this, 'puc_v4_debug_request_info');
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// Debug Bar uses the panel class name as part of its link and container IDs. This means we can
|
||||
// end up with multiple identical IDs if more than one plugin uses the update checker library.
|
||||
// Fix it by replacing the class name with the plugin slug.
|
||||
var panels = $('#debug-menu-targets').find('.puc-debug-bar-panel-v4');
|
||||
panels.each(function() {
|
||||
var panel = $(this);
|
||||
var uid = panel.data('uid');
|
||||
var target = panel.closest('.debug-menu-target');
|
||||
|
||||
//Change the panel wrapper ID.
|
||||
target.attr('id', 'debug-menu-target-puc-' + uid);
|
||||
|
||||
//Change the menu link ID as well and point it at the new target ID.
|
||||
$('#debug-bar-menu').find('.puc-debug-menu-link-' + uid)
|
||||
.closest('.debug-menu-link')
|
||||
.attr('id', 'debug-menu-link-puc-' + uid)
|
||||
.attr('href', '#' + target.attr('id'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "3.1.7",
|
||||
"details_url": "https://github.com/vtrois/kratos/releases/tag/v3.1.7",
|
||||
"download_url": "https://mirrors.vtrois.com/kratos/v3.1.7.zip"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
if ( !class_exists('Parsedown', false) ) {
|
||||
//Load the Parsedown version that's compatible with the current PHP version.
|
||||
if ( version_compare(PHP_VERSION, '5.3.0', '>=') ) {
|
||||
require __DIR__ . '/ParsedownModern.php';
|
||||
} else {
|
||||
require __DIR__ . '/ParsedownLegacy.php';
|
||||
}
|
||||
}
|
||||
+1535
File diff suppressed because it is too large
Load Diff
+1538
File diff suppressed because it is too large
Load Diff
+341
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
if ( !class_exists('PucReadmeParser', false) ):
|
||||
|
||||
/**
|
||||
* This is a slightly modified version of github.com/markjaquith/WordPress-Plugin-Readme-Parser
|
||||
* It uses Parsedown instead of the "Markdown Extra" parser.
|
||||
*/
|
||||
|
||||
class PucReadmeParser {
|
||||
|
||||
function __construct() {
|
||||
// This space intentionally blank
|
||||
}
|
||||
|
||||
function parse_readme( $file ) {
|
||||
$file_contents = @implode('', @file($file));
|
||||
return $this->parse_readme_contents( $file_contents );
|
||||
}
|
||||
|
||||
function parse_readme_contents( $file_contents ) {
|
||||
$file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
|
||||
$file_contents = trim($file_contents);
|
||||
if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) )
|
||||
$file_contents = substr( $file_contents, 3 );
|
||||
|
||||
// Markdown transformations
|
||||
$file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n", $file_contents );
|
||||
$file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im", '==$1=='."\n", $file_contents );
|
||||
$file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im", '===$1==='."\n", $file_contents );
|
||||
|
||||
// === Plugin Name ===
|
||||
// Must be the very first thing.
|
||||
if ( !preg_match('|^===(.*)===|', $file_contents, $_name) )
|
||||
return array(); // require a name
|
||||
$name = trim($_name[1], '=');
|
||||
$name = $this->sanitize_text( $name );
|
||||
|
||||
$file_contents = $this->chop_string( $file_contents, $_name[0] );
|
||||
|
||||
|
||||
// Requires at least: 1.5
|
||||
if ( preg_match('|Requires at least:(.*)|i', $file_contents, $_requires_at_least) )
|
||||
$requires_at_least = $this->sanitize_text($_requires_at_least[1]);
|
||||
else
|
||||
$requires_at_least = NULL;
|
||||
|
||||
|
||||
// Tested up to: 2.1
|
||||
if ( preg_match('|Tested up to:(.*)|i', $file_contents, $_tested_up_to) )
|
||||
$tested_up_to = $this->sanitize_text( $_tested_up_to[1] );
|
||||
else
|
||||
$tested_up_to = NULL;
|
||||
|
||||
|
||||
// Stable tag: 10.4-ride-the-fire-eagle-danger-day
|
||||
if ( preg_match('|Stable tag:(.*)|i', $file_contents, $_stable_tag) )
|
||||
$stable_tag = $this->sanitize_text( $_stable_tag[1] );
|
||||
else
|
||||
$stable_tag = NULL; // we assume trunk, but don't set it here to tell the difference between specified trunk and default trunk
|
||||
|
||||
|
||||
// Tags: some tag, another tag, we like tags
|
||||
if ( preg_match('|Tags:(.*)|i', $file_contents, $_tags) ) {
|
||||
$tags = preg_split('|,[\s]*?|', trim($_tags[1]));
|
||||
foreach ( array_keys($tags) as $t )
|
||||
$tags[$t] = $this->sanitize_text( $tags[$t] );
|
||||
} else {
|
||||
$tags = array();
|
||||
}
|
||||
|
||||
|
||||
// Contributors: markjaquith, mdawaffe, zefrank
|
||||
$contributors = array();
|
||||
if ( preg_match('|Contributors:(.*)|i', $file_contents, $_contributors) ) {
|
||||
$temp_contributors = preg_split('|,[\s]*|', trim($_contributors[1]));
|
||||
foreach ( array_keys($temp_contributors) as $c ) {
|
||||
$tmp_sanitized = $this->user_sanitize( $temp_contributors[$c] );
|
||||
if ( strlen(trim($tmp_sanitized)) > 0 )
|
||||
$contributors[$c] = $tmp_sanitized;
|
||||
unset($tmp_sanitized);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Donate Link: URL
|
||||
if ( preg_match('|Donate link:(.*)|i', $file_contents, $_donate_link) )
|
||||
$donate_link = esc_url( $_donate_link[1] );
|
||||
else
|
||||
$donate_link = NULL;
|
||||
|
||||
|
||||
// togs, conts, etc are optional and order shouldn't matter. So we chop them only after we've grabbed their values.
|
||||
foreach ( array('tags', 'contributors', 'requires_at_least', 'tested_up_to', 'stable_tag', 'donate_link') as $chop ) {
|
||||
if ( $$chop ) {
|
||||
$_chop = '_' . $chop;
|
||||
$file_contents = $this->chop_string( $file_contents, ${$_chop}[0] );
|
||||
}
|
||||
}
|
||||
|
||||
$file_contents = trim($file_contents);
|
||||
|
||||
|
||||
// short-description fu
|
||||
if ( !preg_match('/(^(.*?))^[\s]*=+?[\s]*.+?[\s]*=+?/ms', $file_contents, $_short_description) )
|
||||
$_short_description = array( 1 => &$file_contents, 2 => &$file_contents );
|
||||
$short_desc_filtered = $this->sanitize_text( $_short_description[2] );
|
||||
$short_desc_length = strlen($short_desc_filtered);
|
||||
$short_description = substr($short_desc_filtered, 0, 150);
|
||||
if ( $short_desc_length > strlen($short_description) )
|
||||
$truncated = true;
|
||||
else
|
||||
$truncated = false;
|
||||
if ( $_short_description[1] )
|
||||
$file_contents = $this->chop_string( $file_contents, $_short_description[1] ); // yes, the [1] is intentional
|
||||
|
||||
// == Section ==
|
||||
// Break into sections
|
||||
// $_sections[0] will be the title of the first section, $_sections[1] will be the content of the first section
|
||||
// the array alternates from there: title2, content2, title3, content3... and so forth
|
||||
$_sections = preg_split('/^[\s]*==[\s]*(.+?)[\s]*==/m', $file_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$sections = array();
|
||||
for ( $i=0; $i < count($_sections); $i +=2 ) {
|
||||
$title = $this->sanitize_text( $_sections[$i] );
|
||||
if ( isset($_sections[$i+1]) ) {
|
||||
$content = preg_replace('/(^[\s]*)=[\s]+(.+?)[\s]+=/m', '$1<h4>$2</h4>', $_sections[$i+1]);
|
||||
$content = $this->filter_text( $content, true );
|
||||
} else {
|
||||
$content = '';
|
||||
}
|
||||
$sections[str_replace(' ', '_', strtolower($title))] = array('title' => $title, 'content' => $content);
|
||||
}
|
||||
|
||||
|
||||
// Special sections
|
||||
// This is where we nab our special sections, so we can enforce their order and treat them differently, if needed
|
||||
// upgrade_notice is not a section, but parse it like it is for now
|
||||
$final_sections = array();
|
||||
foreach ( array('description', 'installation', 'frequently_asked_questions', 'screenshots', 'changelog', 'change_log', 'upgrade_notice') as $special_section ) {
|
||||
if ( isset($sections[$special_section]) ) {
|
||||
$final_sections[$special_section] = $sections[$special_section]['content'];
|
||||
unset($sections[$special_section]);
|
||||
}
|
||||
}
|
||||
if ( isset($final_sections['change_log']) && empty($final_sections['changelog']) )
|
||||
$final_sections['changelog'] = $final_sections['change_log'];
|
||||
|
||||
|
||||
$final_screenshots = array();
|
||||
if ( isset($final_sections['screenshots']) ) {
|
||||
preg_match_all('|<li>(.*?)</li>|s', $final_sections['screenshots'], $screenshots, PREG_SET_ORDER);
|
||||
if ( $screenshots ) {
|
||||
foreach ( (array) $screenshots as $ss )
|
||||
$final_screenshots[] = $ss[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the upgrade_notice section specially:
|
||||
// 1.0 => blah, 1.1 => fnord
|
||||
$upgrade_notice = array();
|
||||
if ( isset($final_sections['upgrade_notice']) ) {
|
||||
$split = preg_split( '#<h4>(.*?)</h4>#', $final_sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
|
||||
if ( count($split) >= 2 ) {
|
||||
for ( $i = 0; $i < count( $split ); $i += 2 ) {
|
||||
$upgrade_notice[$this->sanitize_text( $split[$i] )] = substr( $this->sanitize_text( $split[$i + 1] ), 0, 300 );
|
||||
}
|
||||
}
|
||||
unset( $final_sections['upgrade_notice'] );
|
||||
}
|
||||
|
||||
// No description?
|
||||
// No problem... we'll just fall back to the old style of description
|
||||
// We'll even let you use markup this time!
|
||||
$excerpt = false;
|
||||
if ( !isset($final_sections['description']) ) {
|
||||
$final_sections = array_merge(array('description' => $this->filter_text( $_short_description[2], true )), $final_sections);
|
||||
$excerpt = true;
|
||||
}
|
||||
|
||||
|
||||
// dump the non-special sections into $remaining_content
|
||||
// their order will be determined by their original order in the readme.txt
|
||||
$remaining_content = '';
|
||||
foreach ( $sections as $s_name => $s_data ) {
|
||||
$remaining_content .= "\n<h3>{$s_data['title']}</h3>\n{$s_data['content']}";
|
||||
}
|
||||
$remaining_content = trim($remaining_content);
|
||||
|
||||
|
||||
// All done!
|
||||
// $r['tags'] and $r['contributors'] are simple arrays
|
||||
// $r['sections'] is an array with named elements
|
||||
$r = array(
|
||||
'name' => $name,
|
||||
'tags' => $tags,
|
||||
'requires_at_least' => $requires_at_least,
|
||||
'tested_up_to' => $tested_up_to,
|
||||
'stable_tag' => $stable_tag,
|
||||
'contributors' => $contributors,
|
||||
'donate_link' => $donate_link,
|
||||
'short_description' => $short_description,
|
||||
'screenshots' => $final_screenshots,
|
||||
'is_excerpt' => $excerpt,
|
||||
'is_truncated' => $truncated,
|
||||
'sections' => $final_sections,
|
||||
'remaining_content' => $remaining_content,
|
||||
'upgrade_notice' => $upgrade_notice
|
||||
);
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
function chop_string( $string, $chop ) { // chop a "prefix" from a string: Agressive! uses strstr not 0 === strpos
|
||||
if ( $_string = strstr($string, $chop) ) {
|
||||
$_string = substr($_string, strlen($chop));
|
||||
return trim($_string);
|
||||
} else {
|
||||
return trim($string);
|
||||
}
|
||||
}
|
||||
|
||||
function user_sanitize( $text, $strict = false ) { // whitelisted chars
|
||||
if ( function_exists('user_sanitize') ) // bbPress native
|
||||
return user_sanitize( $text, $strict );
|
||||
|
||||
if ( $strict ) {
|
||||
$text = preg_replace('/[^a-z0-9-]/i', '', $text);
|
||||
$text = preg_replace('|-+|', '-', $text);
|
||||
} else {
|
||||
$text = preg_replace('/[^a-z0-9_-]/i', '', $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
function sanitize_text( $text ) { // not fancy
|
||||
$text = strip_tags($text);
|
||||
$text = esc_html($text);
|
||||
$text = trim($text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
function filter_text( $text, $markdown = false ) { // fancy, Markdown
|
||||
$text = trim($text);
|
||||
|
||||
$text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE
|
||||
|
||||
if ( $markdown ) { // Parse markdown.
|
||||
if ( !class_exists('Parsedown', false) ) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
|
||||
}
|
||||
$instance = Parsedown::instance();
|
||||
$text = $instance->text($text);
|
||||
}
|
||||
|
||||
$allowed = array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'title' => array(),
|
||||
'rel' => array()),
|
||||
'blockquote' => array('cite' => array()),
|
||||
'br' => array(),
|
||||
'p' => array(),
|
||||
'code' => array(),
|
||||
'pre' => array(),
|
||||
'em' => array(),
|
||||
'strong' => array(),
|
||||
'ul' => array(),
|
||||
'ol' => array(),
|
||||
'li' => array(),
|
||||
'h3' => array(),
|
||||
'h4' => array()
|
||||
);
|
||||
|
||||
$text = balanceTags($text);
|
||||
|
||||
$text = wp_kses( $text, $allowed );
|
||||
$text = trim($text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown
|
||||
// If doing markdown, first take any user formatted code blocks and turn them into backticks so that
|
||||
// markdown will preserve things like underscores in code blocks
|
||||
if ( $markdown )
|
||||
$text = preg_replace_callback("!(<pre><code>|<code>)(.*?)(</code></pre>|</code>)!s", array( __CLASS__,'decodeit'), $text);
|
||||
|
||||
$text = str_replace(array("\r\n", "\r"), "\n", $text);
|
||||
if ( !$markdown ) {
|
||||
// This gets the "inline" code blocks, but can't be used with Markdown.
|
||||
$text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text);
|
||||
// This gets the "block level" code blocks and converts them to PRE CODE
|
||||
$text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text);
|
||||
} else {
|
||||
// Markdown can do inline code, we convert bbPress style block level code to Markdown style
|
||||
$text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
function indent( $matches ) {
|
||||
$text = $matches[3];
|
||||
$text = preg_replace('|^|m', $matches[2] . ' ', $text);
|
||||
return $matches[1] . $text;
|
||||
}
|
||||
|
||||
function encodeit( $matches ) {
|
||||
if ( function_exists('encodeit') ) // bbPress native
|
||||
return encodeit( $matches );
|
||||
|
||||
$text = trim($matches[2]);
|
||||
$text = htmlspecialchars($text, ENT_QUOTES);
|
||||
$text = str_replace(array("\r\n", "\r"), "\n", $text);
|
||||
$text = preg_replace("|\n\n\n+|", "\n\n", $text);
|
||||
$text = str_replace('&lt;', '<', $text);
|
||||
$text = str_replace('&gt;', '>', $text);
|
||||
$text = "<code>$text</code>";
|
||||
if ( "`" != $matches[1] )
|
||||
$text = "<pre>$text</pre>";
|
||||
return $text;
|
||||
}
|
||||
|
||||
function decodeit( $matches ) {
|
||||
if ( function_exists('decodeit') ) // bbPress native
|
||||
return decodeit( $matches );
|
||||
|
||||
$text = $matches[2];
|
||||
$trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
|
||||
$text = strtr($text, $trans_table);
|
||||
$text = str_replace('<br />', '', $text);
|
||||
$text = str_replace('&', '&', $text);
|
||||
$text = str_replace(''', "'", $text);
|
||||
if ( '<pre><code>' == $matches[1] )
|
||||
$text = "\n$text\n";
|
||||
return "`$text`";
|
||||
}
|
||||
|
||||
} // end class
|
||||
|
||||
endif;
|
||||
Reference in New Issue
Block a user