从gitee代码库中初始化项目。

This commit is contained in:
2022-01-07 11:15:42 +08:00
parent b46e3d988f
commit a826b26bee
184 changed files with 30423 additions and 2 deletions
+109
View File
@@ -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'));
});
});
+166
View File
@@ -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()
})
}
})
+441
View File
@@ -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("'", '&apos;', $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('&apos;', "'", $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' );
+202
View File
@@ -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');
}
+151
View File
@@ -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');
}
+593
View File
@@ -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;
}
}
}
+520
View File
@@ -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;
}
+129
View File
@@ -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;
}
+294
View File
@@ -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;
}
+130
View File
@@ -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');
+387
View File
@@ -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');
+6
View File
@@ -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;
+297
View File
@@ -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;
+132
View File
@@ -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;
+130
View File
@@ -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;
+277
View File
@@ -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;
+238
View File
@@ -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;
+207
View File
@@ -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;
+34
View File
@@ -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&#8230;',
'<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;
+69
View File
@@ -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;
+302
View File
@@ -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;
+35
View File
@@ -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');
}
+70
View File
@@ -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;
}
+52
View File
@@ -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'));
});
});
+5
View File
@@ -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
View File
@@ -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';
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+341
View File
@@ -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('&amp;lt;', '&lt;', $text);
$text = str_replace('&amp;gt;', '&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('&#38;', '&', $text);
$text = str_replace('&#39;', "'", $text);
if ( '<pre><code>' == $matches[1] )
$text = "\n$text\n";
return "`$text`";
}
} // end class
endif;