_pluginDir = dirname(__FILE__);
$this->_menuId = self::_SLUG . '-page';
$this->_themePageUrl = admin_url('themes.php?page='.$this->_menuId);
$this->_createChildFormId = self::_SLUG.'-create-child';
$this->_repairChildFormId = self::_SLUG.'-repair-child';
$this->_copyTemplateFormId = self::_SLUG.'-copy-template';
$this->_mshotSiteFormId = self::_SLUG.'-mshot-site';
// it has to be buried like this or you get an error:
// "You do not have sufficient permissions to access this page"
add_action( 'admin_menu', array($this,'createAdminMenu') );
// form handling code
add_action( 'admin_post_'.$this->_createChildFormId, array($this,'processCreateForm') );
add_action( 'admin_post_'.$this->_repairChildFormId, array($this,'processRepairChildForm') );
add_action( 'admin_post_'.$this->_copyTemplateFormId, array($this,'processCopyTemplateForm') );
add_action( 'admin_post_'.$this->_mshotSiteFormId, array($this,'processMShotSiteForm') );
// TODO: I could also use the $pagenow global, but is it still there?
if ( basename($_SERVER['PHP_SELF']) == 'themes.php' && !empty($_REQUEST['occt_error']) ) {
add_action( 'admin_notices', array($this,'showErrorNotice'));
}
}
/**
* Handle error and update notices for this theme
*
* There are now four types of notices: success (green), warning (orange), error (red),
* and info (blue).
*
* Put here because there is a redirect between all forms and error notifications and
* add_settings_error() only covers options API errors.
*/
public function showErrorNotice()
{
switch ($_GET['occt_error']) {
case 'child_created': //SUCCESS: child theme created
$type = 'updated'; //fade?
$msg = sprintf(
__('Theme switched! Click here to edit the child stylesheet.', self::_SLUG),
add_query_arg(
urlencode_deep(array(
'file' => 'style.css',
'theme' => get_stylesheet(),
)),
admin_url('theme-editor.php')
)
);
break;
case 'create_failed': //ERROR: create file failed (probably due to permissions)
$type = 'error';
$msg = sprintf(
__('Failed to create file: %s', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'edit_failed': //ERROR: edit file failed (probably do to permissions)
$type = 'error';
$msg = sprintf(
__('Failed to edit file: %s', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'repair_success': //SUCCESS: repaired child theme
$type = 'updated fade';
$msg = __('Repaired child theme.', self::_SLUG);
break;
case 'no_template': //ERROR: template file not specified
$type = 'error';
$msg = __('No template file specified.', self::_SLUG);
case 'missing_template': //ERROR: parent theme doesn't have template
$type = 'error';
$msg = sprintf(
__('Template file %s does not exist in parent theme!', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'already_template': //ERROR: child theme already has template
$type = 'error';
$msg = sprintf(
__('Template file %s already exists in child theme!', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'copy_failed': //ERROR: couldn't duplicate file for some reason
$type = 'error';
$msg = sprintf(
__('Failed to duplicate file %s!', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'copy_success': //SUCCESS: template file created
$type = 'updated'; //fade?
$msg = sprintf(
__('File %s created!', self::_SLUG),
add_query_arg(
urlencode_deep(array(
'file' => $_GET['filename'],
'theme' => get_stylesheet(),
)),
admin_url('theme-editor.php')
),
esc_html($_GET['filename'])
);
break;
case 'delete_failed': //ERROR: couldn't delete file for some reason
$type = 'error';
$msg = sprintf(
__('Failed to delete file %s!', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'mshot_404': //ERROR: couldn't find mshot
$type = 'error';
$msg = sprintf(
__('404 File not found at %s!', self::_SLUG),
esc_html($_GET['url'])
);
break;
case 'mshot_mime_wrong': //ERROR: couldn't find mshot
$type = 'error';
$msg = sprintf(
__('Unrecognized mimetype at %s!', self::_SLUG),
esc_html($_GET['url'])
);
break;
case 'mshot_nocreate': //ERROR: couldn't find mshot
$type = 'error';
$msg = sprintf(
__('Failed to create file %s!', self::_SLUG),
esc_html($_GET['filename'])
);
break;
case 'mshot_success': //SUCCESS: screenshot generated
$type = 'updated fade'; //fade?
$msg = __('Successfully changed screenshot.', self::_SLUG);
break;
default: //ERROR: it is a generic error message
$type = 'error';
$msg = esc_html($_GET['occt_error']);
}
printf(
'
',
$type,
$msg
);
}
/**
* Adds an admin menu for One Click Child Theme in Appearances
*/
public function createAdminMenu() {
add_theme_page(
__('Make a Child Theme', self::_SLUG), //page title
__('Child Theme', self::_SLUG), //menu title
'install_themes', //capability needed to view
$this->_menuId, //menu slug (and page query url)
array( $this, 'showThemePage' ) //callback function
);
}
//
// SHOW THEME PAGE
//
/**
* Show the theme page which has a form allowing you to child theme
* currently selected theme.
*
*/
public function showThemePage()
{
// Form is processed in the admin_post_* hooks
// Handle case where current theme is already a child
if ( is_child_theme() ) {
$this->_showFormAlreadyChild( $this->_child_theme_needs_repair() );
return;
}
// Default behavior: We are not a child theme, but interested in creating one.
// Grab default values from a form fail
$theme_name = ( !empty($_GET['theme_name']) ) ? $_GET['theme_name'] : '';
$description = ( !empty($_GET['description']) ) ? $_GET['description'] : '';
if ( !empty($_GET['author_name']) ) {
$author = $_GET['author_name'];
} else {
global $current_user;
get_currentuserinfo();
$author = $current_user->display_name;
}
// render default behaivor
require $this->_pluginDir.'/templates/create_child_form.php';
}
/**
* Show the "is child already" template.
* @param boolean $child_needs_repair whether or not child theme needs repair
* @todo handle grandchildren
*/
private function _showFormAlreadyChild($child_needs_repair) {
// set template parameters
$current_theme = wp_get_theme();
$child_theme_screenshot_url = ( $screenshot_filename = $this->_scanForScreenshot( get_stylesheet_directory() ) )
? get_stylesheet_directory_uri().'/'.$screenshot_filename
: '';
$mshot_url = $this->_mshotUrl();
// Search for template files.
// Note: since there can be files like {mimetype}.php, we must assume
// that any root level .php files in the template directory are
// templates.
$template_files = glob ( get_template_directory().'/*.php' );
foreach ( $template_files as $index=>$file ) {
$template_files[$index] = basename( $file );
}
// Filter out any files in child already created
$child_theme_dir = get_stylesheet_directory();
foreach ( $template_files as $index=>$filename ) {
if ( file_exists($child_theme_dir.'/'.$filename) ) {
unset($template_files[$index]);
}
}
require $this->_pluginDir.'/templates/is_child_already.php';
}
//
// FORM HANDLING
//
/**
* Handle the create child form.
*/
public function processCreateForm() {
check_admin_referer( $this->_createChildFormId . '-verify' );
$theme_name = $_POST['theme_name'];
$description = ( empty($_POST['description']) )
? ''
: $_POST['description'];
$author_name = ( empty($_POST['author_name']) )
? ''
: $_POST['author_name'];
$result = $this->_make_child_theme( $theme_name, $description, $author_name );
if ( is_wp_error( $result ) ) {
// should show create child form again
$this->_redirect(
$this->_themePageUrl,
$result->get_error_message(),
array(
'theme_name' => $theme_name,
'description' => $description,
'author_name' => $author_name,
)
);
return;
} else {
switch_theme( $result['parent_template'], $result['new_theme'] );
// Redirect to themes page on success
$this->_redirect(
admin_url('themes.php'),
'child_created'
);
}
}
/**
* Handle the repair_child_form form.
*/
public function processRepairChildForm()
{
check_admin_referer( $this->_repairChildFormId . '-verify' );
$child_theme_dir = get_stylesheet_directory();
$functions_file = $child_theme_dir.'/functions.php';
$style_file = $child_theme_dir.'/style.css';
// create functions.php if it doesn't exist yet
if ( !file_exists($functions_file) ) {
if ( !touch($functions_file) ) {
// fixing is hopeless if we can't create the file :-(
$this->_redirect(
$this->_themePageUrl,
'create_failed',
array( 'filename' => $functions_file )
);
return;
}
}
// read in style.css
$style_text = file_get_contents( $style_file );
// prune out old rules
$style_text = preg_replace(
'!@import\s+url\(\s?["\']\.\./.*/style.css["\']\s?\);!ims',
'',
$style_text
);
$style_text = preg_replace(
'!@import\s+url\(\s?["\']'.get_template_directory_uri().'/style.css["\']\s?\);!ims',
'',
$style_text
);
if ( file_put_contents( $style_file, $style_text) === false ) {
$this->_redirect(
$this->_themePageUrl,
'edit_failed',
array( 'filename' => $style_file )
);
return;
}
// modify functions.php to prepend new rules
$functions_text = file_get_contents( $this->_pluginDir.'/templates/functions.php' );
// ^^^ above file has no final carriage return and ending comment so it should
// "smash" the starting '_redirect(
$this->_themePageUrl,
'edit_failed',
array( 'filename' => $functions_file )
);
return;
}
$this->_redirect(
$this->_themePageUrl,
'repair_success'
);
}
/**
* Handle the Copy Template form.
*/
public function processCopyTemplateForm() {
check_admin_referer( $this->_copyTemplateFormId . '-verify' );
$filename = ( empty($_POST['filename']) )
? ''
: $_POST['filename'];
if ( !$filename ) {
$this->_redirect(
$this->_themePageUrl,
'no_template'
);
return;
}
$child_theme_dir = get_stylesheet_directory();
$template_dir = get_template_directory();
var_dump('bar');
if ( !file_exists($template_dir.'/'.$filename) ) {
$this->_redirect(
$this->_themePageUrl,
'missing_template',
array( 'filename' => $filename )
);
return;
}
if ( file_exists($child_theme_dir.'/'.$filename) ) {
$this->_redirect(
$this->_themePageUrl,
'already_template',
array( 'filename' => $filename )
);
return;
}
if ( !copy( $template_dir.'/'.$filename, $child_theme_dir.'/'.$filename ) ) {
$this->_redirect(
$this->_themePageUrl,
'copy_failed',
array( 'filename' => $filename )
);
}
$this->_redirect(
$this->_themePageUrl,
'copy_success',
array( 'filename' => $filename )
);
}
/**
* Handle the mshot Screenshot form
*/
public function processMShotSiteForm()
{
check_admin_referer( $this->_mshotSiteFormId . '-verify' );
// delete existing screenshot if it exists
$child_theme_dir = get_stylesheet_directory();
if ( $screenshot_filename = $this->_scanForScreenshot($child_theme_dir) ) {
$screenshot_path = $child_theme_dir.'/'.$screenshot_filename;
if ( !unlink($screenshot_path) ) {
// most likely a directory problem Fail with an error
$this->_redirect(
$this->_themePageUrl,
'delete_failed',
array( 'filename' => $screenshot_path )
);
return;
}
}
$mshot_url = $this->_mshotUrl();
// Get the mshot
$response = wp_remote_get($mshot_url);
if ( $response['code'] == 404 ) {
// The 404 image is gorgeous nowadays, but (if wp.com correctly handled error
// codes for image generation) we'd not let them use it as a theme screenshot.
$this->_redirect(
$this->_themePageUrl,
'mshot_404',
array( 'url' => $mshot_url )
);
}
// Should be 'image/jpeg', but let's hedge our bets
switch ($response['headers']['content-type']) {
case 'image/jpeg':
$screenshot_filename = 'screenshot.jpg';
break;
case 'image/png':
$screenshot_filename = 'screenshot.png';
break;
case 'image/gif':
$screenshot_filename = 'screenshot.gif';
break;
default:
$this->_redirect(
$this->_themePageUrl,
'mshot_mime_wrong',
array( 'url' => $mshot_url )
);
return;
}
$screenshot_path = $child_theme_dir.'/'.$screenshot_filename;
if ( file_put_contents($screenshot_path, $response['body']) === false ) {
$this->_redirect(
$this->_themePageUrl,
'mshot_nocreate',
array( 'filename' => $screenshot_path )
);
return;
}
$this->_redirect(
$this->_themePageUrl,
'mshot_success'
);
}
//
// PRIVATE METHOD
//
/**
* Does the work to make a child theme based on the current theme.
*
* This currently supports the following files:
*
* 1. style.css: Follows the rules outlined in {@link http://codex.wordpress.org/Child_Themes the Codex}
* 2. functions.php: Followed the updated rules outlined in the Codex. Note
* that since WordPress ?.? functions.php hierarchy is automatically
* included.
* 3. rtl.css: right to left language support, if not avaialble in parent, it
* uses TwentyFifteen's rtl
* 4. screenshot.png: screenshot if available in the parent
*
* @author terry chay
* @author Chris Robinson (for screenshot support).
* @return array|WP_Error If successful, it returns a hash contianing
* - new_theme: (directory) name of new theme
* - parent_template: (directory) name of parent template
* - parent_theme: (directory) name of parent theme
* - new_theme_path: full path to the directory cotnaining the new theme
* - new_theme_title: the name of the new theme
*/
private function _make_child_theme( $new_theme_title, $new_theme_description, $new_theme_author ) {
$parent_theme_title = get_current_theme();
$parent_theme_template = get_template(); //Doesn't play nice with the grandkids
$parent_theme_name = get_stylesheet();
$parent_theme_dir = get_stylesheet_directory();
// Turn a theme name into a directory name
$new_theme_name = sanitize_title( $new_theme_title );
$theme_root = get_theme_root();
// Validate theme name
$new_theme_path = $theme_root.'/'.$new_theme_name;
if ( file_exists( $new_theme_path ) ) {
return new WP_Error( 'exists', __( 'Theme directory already exists!', self::_SLUG ) );
}
mkdir( $new_theme_path );
// Make style.css
ob_start();
require $this->_pluginDir.'/templates/child-theme-css.php';
$css = ob_get_clean();
file_put_contents( $new_theme_path.'/style.css', $css );
// "Generate" functions.php
copy( $this->_pluginDir.'/templates/functions.php', $new_theme_path.'/functions.php' );
// RTL support
$rtl_theme = ( file_exists( $parent_theme_dir.'/rtl.css' ) )
? $parent_theme_name
: 'twentyfifteen'; //use the latest default theme rtl file
ob_start();
require $this->_pluginDir.'/templates/rtl-css.php';
$css = ob_get_clean();
file_put_contents( $new_theme_path.'/rtl.css', $css );
// Copy screenshot
if ( $screenshot_filename = $this->_scanForScreenshot( $parent_theme_dir ) ) {
copy(
$parent_theme_dir.'/'.$screenshot_filename,
$new_theme_path.'/'.$screenshot_filename
);
} // removed grandfather screenshot check (use mshot instead, rly)
// Make child theme an allowed theme (network enable theme)
$allowed_themes = get_site_option( 'allowedthemes' );
$allowed_themes[ $new_theme_name ] = true;
update_site_option( 'allowedthemes', $allowed_themes );
return array(
'parent_template' => $parent_theme_template,
'parent_theme' => $parent_theme_name,
'new_theme' => $new_theme_name,
'new_theme_path' => $new_theme_path,
'new_theme_title' => $new_theme_title,
);
}
//
// PRIVATE UTILITY FUNCTIONS
//
/**
* Detect if child theme needs repair.
*
* A child theme needs repair if it is missing a functions.php or the
* style.css still has a rule that points to the parent.
*/
private function _child_theme_needs_repair()
{
$child_theme_dir = get_stylesheet_directory();
if ( !file_exists($child_theme_dir.'/functions.php') ) {
return true;
}
$style_text = file_get_contents( $child_theme_dir.'/style.css' );
// look for relative match (dificult to extract parent theme directory
// so I'll assume any in this path is parent theme)
if ( preg_match(
'!@import\s+url\(\s?["\']\.\./.*/style.css["\']\s?\);!ims',
$style_text
) ) {
return true;
}
// look for absolute match
if ( preg_match(
'!@import\s+url\(\s?["\']'.get_template_directory_uri().'/style.css["\']\s?\);!ims',
$style_text
) ) {
return true;
}
return false;
}
/**
* Handle error redirects (for admin_notices generated by plugin)
*
* Note add_query_arg() is written like shit. Here are it's problems:
*
* 1. doesn't take advantage of built-in parse_url()
* 2. uses urlencode_deep() instead of an array_merge and built-in http_build_query()
* 3. doesn't urlencode() if $arg[0] is an array.
*
* The 3rd one is extremely non-intuitive, but fixing it, would break backward
* compatibility due to double-escaping. I'm hacking around that. :-(
*
* @param string $url The (base) url to redirect to, usually admin_url()
* @param string $error the error code to use
* @param string $args other arguments to add to the query string
* @return null
*/
private function _redirect($url, $error, $args = array()) {
$args['occt_error'] = $error;
$args = urlencode_deep($args);
wp_redirect( add_query_arg( $args, $url ) );
}
/**
* Searches directory for a theme screenshot
*
* @param string $directory directory to search (a theme directory)
* @return string|false 'screenshot.png' (or whatever) or false if there is no screenshot
*/
private function _scanForScreenshot($directory)
{
$screenshots = glob( $directory.'/screenshot.{png,jpg,jpeg,gif}', GLOB_BRACE );
return (empty($screenshots))
? false
: basename($screenshots[0]);
}
/**
* Generate mshot of wordpress homepage.
*
* Recommende image dimensions from https://codex.wordpress.org/Theme_Development#Screenshot
* @todo probably won't work correctly in multisite installs
* @todo remove debugging code
*/
private function _mshotUrl()
{
$scheme = (is_ssl()) ? 'https' : 'http';
return $scheme . '://s.wordpress.com/mshots/v1/'. urlencode(get_site_url()) . '?w=880&h=660';
}
}
new OneClickChildTheme();
// Start this plugin
//add_action( 'admin_init', array('OneClickChildTheme','init'), 12 );