Basic release
This commit is contained in:
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Pfadfinden\WordPress;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A theme repository.
|
||||
*
|
||||
* It’s a simple wrapper around a web service mimicking the wordpress.org theme repository.
|
||||
*
|
||||
* @author Philipp Cordes <philipp.cordes@pfadfinden.de>
|
||||
*/
|
||||
class ThemeRepository
|
||||
{
|
||||
const URL = 'http://lab.hanseaten-bremen.de/themes/api/';
|
||||
|
||||
|
||||
/**
|
||||
* Slugs of managed themes.
|
||||
*
|
||||
* FIXME: Move to a transient.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
private $known_themes = [ 'bdp-reloaded', 'bdp-test', 'buena' ];
|
||||
|
||||
|
||||
/**
|
||||
* @var ThemeUpdaterSettings
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
|
||||
public function __construct( ThemeUpdaterSettings $settings )
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether theme information is available.
|
||||
*
|
||||
* @param string $theme_slug
|
||||
* @return bool
|
||||
*/
|
||||
public function isKnownTheme( $theme_slug )
|
||||
{
|
||||
return in_array( $theme_slug, $this->known_themes, true );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper around HTTP calls, always returns an array of theme information.
|
||||
*
|
||||
* @param string $action One of the supported actions of the repository
|
||||
* @param array $params Parameters for the action
|
||||
* @param string $locale
|
||||
* @return array<object>|\WP_Error
|
||||
*/
|
||||
protected function doApiQuery( $action, array $params = [], $locale = '' )
|
||||
{
|
||||
$url_params = [
|
||||
'key' => $this->settings['key'],
|
||||
'action' => $action,
|
||||
];
|
||||
if ( $params ) {
|
||||
if ( function_exists( 'gzcompress' ) ) {
|
||||
$url_params['gzparams'] = gzcompress( json_encode( $params ), 9 );
|
||||
} else {
|
||||
$url_params['params'] = json_encode( $params );
|
||||
}
|
||||
}
|
||||
$url_params = array_map( 'rawurlencode', $url_params );
|
||||
|
||||
$url = add_query_arg( $url_params, self::URL );
|
||||
if ( strlen( $url ) > 2000 ) {
|
||||
// Lengths beyond 2000 seem unhealthy.
|
||||
return new \WP_Error(
|
||||
815,
|
||||
__( 'Your theme repository query is too long.', 'pfadfinden-theme-updater' )
|
||||
);
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
if ( ! strlen( $locale ) ) {
|
||||
$locale = get_locale();
|
||||
}
|
||||
if ( strlen( $locale ) ) {
|
||||
$locale = str_replace( '_', '-', $locale );
|
||||
$headers['Accept-Language'] = "$locale, en; q=0.6, *; q=0.1";
|
||||
}
|
||||
|
||||
// A GET request allows for caching
|
||||
$response = wp_remote_get( $url, [
|
||||
'headers' => $headers,
|
||||
] );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( isset( $body['type'] ) && 'success' === $body['type'] ) {
|
||||
return array_map( function ( array $theme ) {
|
||||
return (object) $theme;
|
||||
}, $body['themes'] );
|
||||
}
|
||||
|
||||
if ( WP_DEBUG ) {
|
||||
trigger_error( wp_remote_retrieve_body( $response ), E_USER_ERROR );
|
||||
}
|
||||
$error = new \WP_Error(
|
||||
wp_remote_retrieve_response_code( $response ),
|
||||
isset( $body['message'] ) ? $body['message'] : __( 'Unknown theme repository server error, no message attached.', 'pfadfinden-theme-updater' )
|
||||
);
|
||||
if ( isset( $body['exception'] ) ) {
|
||||
$error->add_data( $body['exception'] );
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<bool> $fields to explicitly include or exclude
|
||||
* @param string $locale
|
||||
* @return array<object {
|
||||
* @type string $name
|
||||
* @type string $slug lowercase, hyphenated
|
||||
* @type string $version
|
||||
* @type string $author
|
||||
* @type string $preview_url
|
||||
* @type string $screenshot_url
|
||||
* @type float $rating between 0 and 100
|
||||
* @type int $num_ratings
|
||||
* @type int $downloaded
|
||||
* @type string $last_updated Y-m-d
|
||||
* @type string $homepage
|
||||
* @type string $description
|
||||
* @type array $tags
|
||||
* }>
|
||||
*/
|
||||
public function queryFeaturedThemes( array $fields = [], $locale = '' )
|
||||
{
|
||||
return $this->doApiQuery( 'featured', [ 'fields' => $fields ], $locale );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query information about a specific theme.
|
||||
*
|
||||
* @param string|array $slugs theme slug(s)
|
||||
* @param array<bool> $fields to explicitly include or exclude
|
||||
* @param string $locale
|
||||
* @return object {
|
||||
* @type string $name
|
||||
* @type string $slug
|
||||
* @type string $version
|
||||
* @type string $author
|
||||
* @type string $preview_url
|
||||
* @type string $screenshot_url
|
||||
* @type float $rating between 0.0 and 100.0
|
||||
* @type int $num_ratings
|
||||
* @type int $downloaded
|
||||
* @type string $last_updated
|
||||
* @type string $homepage
|
||||
* @type array $sections {
|
||||
* @type string $description
|
||||
* }
|
||||
* @type string $description empty string when having sections
|
||||
* @type string $download_link
|
||||
* @type array<string> $tags keys are tag slugs, values also lowercase. strange.
|
||||
* }
|
||||
*/
|
||||
public function queryThemeInformation( $slugs, array $fields = [], $locale = '' )
|
||||
{
|
||||
$themes = $this->doApiQuery( 'information', [
|
||||
'slugs' => (array) $slugs,
|
||||
'fields' => $fields,
|
||||
], $locale );
|
||||
if ( is_wp_error( $themes ) ) {
|
||||
return $themes;
|
||||
}
|
||||
|
||||
if ( is_string( $slugs ) ) {
|
||||
if ( count( $themes ) !== 1 ) {
|
||||
return new \WP_Error( __( 'Ambiguous result for single theme information call.', 'pfadfinden-theme-updater' ) );
|
||||
}
|
||||
|
||||
return reset( $themes );
|
||||
}
|
||||
|
||||
return $themes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query information about updates for installed themes.
|
||||
*
|
||||
* @param array<bool> $fields to explicitly include or exclude
|
||||
* @param string $locale
|
||||
* @return array<object>
|
||||
*/
|
||||
public function queryUpdates( array $fields = [], $locale = '' )
|
||||
{
|
||||
// FIXME: Only include installed themes
|
||||
return $this->queryThemeInformation( $this->known_themes, $fields, $locale );
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Pfadfinden\WordPress;
|
||||
|
||||
use Shy\WordPress\Plugin;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A plugin that hooks the Pfadfinden theme repository into the Theme Updater.
|
||||
*
|
||||
* It knows about the way that WordPress handles and stores theme information.
|
||||
*
|
||||
* @author Philipp Cordes <philipp.cordes@pfadfinden.de>
|
||||
*/
|
||||
class ThemeUpdaterPlugin extends Plugin
|
||||
{
|
||||
const ACTION_QUERY_THEMES = 'query_themes';
|
||||
const ACTION_FEATURE_LIST = 'feature_list';
|
||||
const ACTION_THEME_INFORMATION = 'theme_information';
|
||||
|
||||
|
||||
/**
|
||||
* @var ThemeUpdaterSettings
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var ThemeRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->settings = new ThemeUpdaterSettings();
|
||||
|
||||
if ( ! $this->settings['key'] ) {
|
||||
// Bail out if there is no key.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->repository = new ThemeRepository( $this->settings );
|
||||
|
||||
|
||||
$this->addHookMethod( 'themes_api', 'filterApiCall' );
|
||||
$this->addHookMethod( 'themes_api_result', 'filterApiResult' );
|
||||
|
||||
$this->addHookMethod( 'themes_update_check_locales', 'filterThemeUpdateLocales' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace a Theme API call.
|
||||
*
|
||||
* Actually, only the call for theme information in special cases.
|
||||
*
|
||||
* @param \WP_Error|object|false $result
|
||||
* @param string $action 'theme_information', 'feature_list' or 'query_themes'
|
||||
* @param object $args
|
||||
* @return \WP_Error|object|array|false
|
||||
*/
|
||||
public function filterApiCall( $result, $action, $args )
|
||||
{
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( self::ACTION_THEME_INFORMATION === $action
|
||||
&& $this->repository->isKnownTheme( $args->slug )
|
||||
) {
|
||||
// Only handle our theme information calls
|
||||
return $this->repository->queryThemeInformation( $args->slug, $args->fields, $args->locale );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a Theme API result.
|
||||
*
|
||||
* Inject our themes at appropriate places.
|
||||
*
|
||||
* @param object|\WP_Error $result
|
||||
* @param string $action 'theme_information', 'feature_list', 'query_themes'
|
||||
* @param object|array $args An array after using built-in API, object otherwise.
|
||||
* @return object
|
||||
*/
|
||||
public function filterApiResult( $result, $action, $args )
|
||||
{
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// FIXME: Workaround to be removed on 2015-10-23
|
||||
if ( is_array( $args ) && isset( $args['body']['request'] ) ) {
|
||||
// See https://core.trac.wordpress.org/ticket/29079, fixed in 4.2
|
||||
$args = unserialize( $args['body']['request'] ); // Unpack original args
|
||||
}
|
||||
|
||||
if ( self::ACTION_QUERY_THEMES !== $action || ! isset( $args->browse ) || 'featured' !== $args->browse ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( ! $result || ! is_object( $result ) ) {
|
||||
// Construct empty result
|
||||
// FIXME: Maybe unneccessary
|
||||
$result = (object) [
|
||||
'info' => [
|
||||
'page' => 1,
|
||||
'pages' => 0,
|
||||
'results' => false,
|
||||
],
|
||||
'themes' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$themes = $this->repository->queryFeaturedThemes( $args->fields, $args->locale );
|
||||
if ( ! is_wp_error( $themes ) ) {
|
||||
$this->spliceThemes( $result, $themes );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice additional themes into an existing Theme API result.
|
||||
*
|
||||
* Put them in front.
|
||||
*
|
||||
* @param object $result {
|
||||
* @type object $info {
|
||||
* @type integer|false $results have browser count if false
|
||||
* @type integer|string $page
|
||||
* @type integer $pages may be 0
|
||||
* }
|
||||
* @type array $themes
|
||||
* }
|
||||
* @param array $themes
|
||||
* @return void
|
||||
*/
|
||||
public function spliceThemes( $result, array $themes )
|
||||
{
|
||||
$add = function ( $number, $increment ) {
|
||||
return is_integer( $number ) ? $number + $increment : $number;
|
||||
};
|
||||
|
||||
if ( is_array( $result->info ) ) {
|
||||
$result->info['results'] = $add( $result->info['results'], count( $themes ) );
|
||||
} elseif ( is_object( $result->info ) ) {
|
||||
// Seemed to be an object once…
|
||||
$result->info->results = $add( $result->info->results, count( $themes ) );
|
||||
}
|
||||
|
||||
array_splice( $result->themes, 0, 0, $themes );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter locales queried for a theme update.
|
||||
*
|
||||
* Just in time to wait for the theme updates HTTP request…
|
||||
*
|
||||
* @param array $locales
|
||||
* @return array
|
||||
*/
|
||||
public function filterThemeUpdateLocales( $locales )
|
||||
{
|
||||
$this->addHookMethod( 'http_response', 'filterThemeUpdateResponse' );
|
||||
|
||||
return $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
protected function isThemeUpdateUrl( $url )
|
||||
{
|
||||
return (bool) preg_match( '@^https?://api.wordpress.org/themes/update-check/1.1/$@', $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add our updates to the list.
|
||||
*
|
||||
* @param array $response
|
||||
* @param array $args Original args to request
|
||||
* @param string $url
|
||||
*/
|
||||
public function filterThemeUpdateResponse( array $response, array $args, $url )
|
||||
{
|
||||
if ( ! $this->isThemeUpdateUrl( $url ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeHookMethod( 'http_response', __FUNCTION__ );
|
||||
|
||||
$themes = $this->repository->queryUpdates( [
|
||||
// Eliminate worst offenders
|
||||
'author' => false,
|
||||
'description' => false,
|
||||
'preview_url' => false,
|
||||
'screenshot_url' => false,
|
||||
] );
|
||||
if ( is_wp_error( $themes ) ) {
|
||||
// Silently fail.
|
||||
return $response;
|
||||
}
|
||||
|
||||
$updates = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
foreach ( $themes as $theme ) {
|
||||
$installed_theme = wp_get_theme( $theme->slug );
|
||||
if ( ! $installed_theme->exists() || version_compare( $theme->version, $installed_theme->version, '<=' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Because that’s why: Rename all the fields.
|
||||
$updates['themes'][ $theme->slug ] = [
|
||||
'theme' => $theme->slug,
|
||||
'new_version' => $theme->version,
|
||||
'url' => $theme->homepage,
|
||||
'package' => $theme->download_link,
|
||||
];
|
||||
}
|
||||
|
||||
$response['body'] = json_encode( $updates );
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Pfadfinden\WordPress;
|
||||
|
||||
use Shy\WordPress\SettingsPage;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The code managing the plugin settings.
|
||||
*
|
||||
* @author Philipp Cordes <philipp.cordes@pfadfinden.de>
|
||||
*/
|
||||
class ThemeUpdaterSettings extends SettingsPage
|
||||
{
|
||||
/**
|
||||
* Full path of plugin main file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getPluginFilename()
|
||||
{
|
||||
return preg_replace( '/src\\/.*?$/', 'pfadfinden-theme-updater.php', __DIR__ );
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct( 'pfadfinden-theme-updater' );
|
||||
|
||||
$this->addHookMethod( 'plugin_action_links', 'filterPluginActions' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add our settings entry to the plugin actions.
|
||||
*
|
||||
* @param array<string> $actions
|
||||
* @param string $plugin_file
|
||||
* @param array $plugin_data
|
||||
* @param string $context
|
||||
* @return array<string>
|
||||
*/
|
||||
public function filterPluginActions( array $actions, $plugin_file, array $plugin_data, $context )
|
||||
{
|
||||
// Dereference possible symlink
|
||||
$plugin_file = realpath( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_file );
|
||||
if ( $this->getPluginFilename() !== $plugin_file ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
return array(
|
||||
'settings' => sprintf(
|
||||
'<a href="themes.php?page=%s">%s</a>',
|
||||
esc_attr( urlencode( $this->slug ) ),
|
||||
esc_html__( 'Settings' )
|
||||
),
|
||||
) + $actions;
|
||||
}
|
||||
|
||||
|
||||
protected function getParentSlug()
|
||||
{
|
||||
return 'themes.php';
|
||||
}
|
||||
|
||||
protected function getPageTitle()
|
||||
{
|
||||
return __( 'Pfadfinden Theme Updater Settings', 'pfadfinden-theme-updater' );
|
||||
}
|
||||
|
||||
protected function getMenuTitle()
|
||||
{
|
||||
return __( 'Pfadfinden Updater', 'pfadfinden-theme-updater' );
|
||||
}
|
||||
|
||||
|
||||
public function registerSettings()
|
||||
{
|
||||
$this->addSection( '', 'plugin' );
|
||||
|
||||
$this->addTextField(
|
||||
'key',
|
||||
__( 'API Key', 'pfadfinden-theme-updater' ),
|
||||
[ 'attr' => [
|
||||
'minlength' => '10',
|
||||
'maxlength' => '10',
|
||||
'pattern' => '^[A-Za-z0-9]{10}$',
|
||||
'title' => __( 'An API key consists of 10 alphanumeric characters.', 'pfadfinden-theme-updater' ),
|
||||
] ],
|
||||
[ $this, 'renderApiKeyField' ]
|
||||
);
|
||||
|
||||
$this->addCheckboxField(
|
||||
'keep-settings',
|
||||
__( 'Keep Settings', 'pfadfinden-theme-updater' ),
|
||||
__( 'Don’t delete settings when uninstalling the plugin.', 'pfadfinden-theme-updater' )
|
||||
);
|
||||
|
||||
parent::registerSettings();
|
||||
}
|
||||
|
||||
public function renderApiKeyField( array $args )
|
||||
{
|
||||
$this->renderTextField( $args );
|
||||
|
||||
echo '<p class="description">' . __( 'Just testing? Try APITESTKEY.', 'pfadfinden-theme-updater' ) . '</p>';
|
||||
}
|
||||
|
||||
public function sanitizeOptions( array $options )
|
||||
{
|
||||
if ( isset( $options['key'] ) ) {
|
||||
$key = preg_replace( '/[^A-Za-z0-9]+/', '', $options['key'] );
|
||||
$keylen = strlen( $key );
|
||||
if ( 0 !== $keylen && 10 !== $keylen ) {
|
||||
$this->addError( 'key', __( 'An API key consists of 10 alphanumeric characters.', 'pfadfinden-theme-updater' ) );
|
||||
}
|
||||
$options['key'] = $key;
|
||||
}
|
||||
|
||||
return $options + $this->getDefaults();
|
||||
}
|
||||
|
||||
public function getDefaults()
|
||||
{
|
||||
return [
|
||||
'key' => '',
|
||||
'keep-settings' => false,
|
||||
];
|
||||
}
|
||||
}
|
19
plugins/pfadfinden-theme-updater/src/autoloader.php
Normal file
19
plugins/pfadfinden-theme-updater/src/autoloader.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Try to load a Pfadfinden WordPress class.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
function pfadfinden_wordpress_autoloader( $name )
|
||||
{
|
||||
if ( substr( $name, 0, 21 ) !== 'Pfadfinden\\WordPress\\' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = __DIR__ . '/' . str_replace( '\\', DIRECTORY_SEPARATOR, $name ) . '.php';
|
||||
return is_file( $name ) && include( $name );
|
||||
}
|
||||
|
||||
spl_autoload_register( 'pfadfinden_wordpress_autoloader' );
|
Reference in New Issue
Block a user