Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions environments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ npm run plugins:refresh
npx wp-env run cli wp <command>
```

### Theme Directory

A local instance of the WordPress.org Theme Directory with the theme directory plugin, theme, and supporting mu-plugins.

**Start:**

```bash
npm run themes:env start
```

**Re-seed themes** (clears import flag, then re-imports):

```bash
npm run themes:refresh
```

**Access:** `http://localhost:8888`

### Handbook (in-plugin)

The Handbook plugin has its own `.wp-env.json` in `wordpress.org/public_html/wp-content/plugins/handbook/`.
Expand Down
4 changes: 3 additions & 1 deletion environments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"scripts": {
"plugins:env": "wp-env --config plugin-directory/.wp-env.json",
"plugins:import": "npm run plugins:env -- run cli -- php wp-content/plugins/plugin-directory/bin/import-plugin.php --create --plugin",
"plugins:refresh": "npm run plugins:env -- run cli -- wp option delete wporg_env_imported && npm run plugins:env -- run cli wp eval-file wp-content/env-bin/import-plugins.php"
"plugins:refresh": "npm run plugins:env -- run cli -- wp option delete wporg_env_imported && npm run plugins:env -- run cli wp eval-file wp-content/env-bin/import-plugins.php",
"themes:env": "wp-env --config theme-directory/.wp-env.json",
"themes:refresh": "npm run themes:env -- run cli -- wp option delete wporg_themes_env_imported && npm run themes:env -- run cli wp eval-file wp-content/env-bin/import-themes.php"
},
"devDependencies": {
"@wordpress/env": "^11"
Expand Down
32 changes: 32 additions & 0 deletions environments/theme-directory/.wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"core": "WordPress/WordPress#master",
"phpVersion": "8.4",
"testsEnvironment": false,
"plugins": [
"../wordpress.org/public_html/wp-content/plugins/theme-directory",
"https://downloads.wordpress.org/plugin/jetpack.zip"
],
"themes": [
"WordPress/wporg-parent-2021#build"
],
"mappings": {
"wp-content/wporg-theme-directory": "WordPress/wporg-theme-directory#build",
"wp-content/mu-plugins/": "../wordpress.org/public_html/wp-content/mu-plugins/",
"wp-content/mu-plugins/wporg-mu-plugins": "WordPress/wporg-mu-plugins#build",
"wp-content/mu-plugins/mu-plugins-loader.php": "./mocks/mu-plugins-loader.php",
"wp-content/mu-plugins/wporg-query-filter.php": "./mocks/wporg-query-filter.php",
"wp-content/mu-plugins/wporg-dev-login.php": "./mocks/wporg-dev-login.php",
"wp-content/mu-plugins/wporg-ratings.php": "./theme-directory/mock-wporg-ratings.php",
"wp-content/mu-plugins/wporg-theme-directory-path.php": "./theme-directory/mu-plugin-theme-directory.php",
"wp-content/env-bin": "./theme-directory/bin"
},
"lifecycleScripts": {
"afterStart": "bash theme-directory/bin/after-start.sh"
},
"config": {
"WP_DEBUG": true,
"JETPACK_DEV_DEBUG": true,
"WP_TESTS_TITLE": "WordPress.org Themes",
"GLOTPRESS_LOCALES_PATH": "/var/www/html/wp-content/mu-plugins/pub/locales/locales.php"
}
}
40 changes: 40 additions & 0 deletions environments/theme-directory/bin/after-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
#
# Runs after wp-env start. Sets up permalinks, creates pages, and imports themes.
#

CONFIG="--config theme-directory/.wp-env.json"
WP="npx wp-env $CONFIG run cli --"
WPENV="npx wp-env $CONFIG run"

# Install CLI tools.
echo "Installing CLI tools..."
$WPENV wordpress sudo bash -c \
'command -v unzip > /dev/null || (apt-get -qy update && apt-get -qy install unzip zip) > /dev/null 2>&1'

$WPENV cli sudo sh -c \
'command -v unzip > /dev/null || apk add --no-cache -q unzip zip coreutils > /dev/null 2>&1'

# Set up permalinks and tag base.
$WP wp rewrite structure '/%postname%/' --hard
$WP wp rewrite flush --hard

# Activate the theme.
$WP wp theme activate wporg-themes-2024

# Create pages that exist on wordpress.org/themes.
echo "Creating pages..."
$WP wp post create --post_type=page --post_status=publish --post_title='Commercial' --post_name='commercial' --porcelain > /dev/null 2>&1 && echo " Created page: /commercial/" || true
$WP wp post create --post_type=page --post_status=publish --post_title='Getting Started' --post_name='getting-started' --porcelain > /dev/null 2>&1 && echo " Created page: /getting-started/" || true
$WP wp post create --post_type=page --post_status=publish --post_title='Upload' --post_name='upload' --post_content='[wporg-themes-upload]' --porcelain > /dev/null 2>&1 && echo " Created page: /upload/" || true

# Create stub database tables that exist outside WordPress on production.
$WP wp db import wp-content/env-bin/database-tables.sql

# Create business model terms.
echo "Creating taxonomy terms..."
$WP wp term create theme_business_model "Commercial" --slug=commercial > /dev/null 2>&1 && echo " Created term: commercial" || true
$WP wp term create theme_business_model "Community" --slug=community > /dev/null 2>&1 && echo " Created term: community" || true

# Import themes from wordpress.org.
$WP wp eval-file wp-content/env-bin/import-themes.php
22 changes: 22 additions & 0 deletions environments/theme-directory/bin/database-tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Stub tables for theme directory local development.
-- These tables exist outside WordPress on production.

CREATE TABLE IF NOT EXISTS `bb_themes_stats` (
`slug` varchar(255) NOT NULL,
`date` date NOT NULL,
`downloads` int(10) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`slug`, `date`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `ratings` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`object_type` varchar(20) NOT NULL DEFAULT '',
`object_slug` varchar(200) NOT NULL DEFAULT '',
`user_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`post_id` bigint(20) unsigned NOT NULL DEFAULT 0,
`rating` tinyint(3) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `object_type` (`object_type`, `object_slug`),
KEY `user_id` (`user_id`),
KEY `post_id` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
241 changes: 241 additions & 0 deletions environments/theme-directory/bin/import-themes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<?php
/**
* Import themes from WordPress.org for local development.
*
* Fetches theme data from the Themes API for popular, new, and updated
* browse views, and creates repopackage posts with the appropriate metadata.
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Skip if already imported.
if ( get_option( 'wporg_themes_env_imported' ) ) {
echo "Already imported, skipping.\n";
return;
}

update_option( 'wporg_themes_env_imported', time() );

$per_section = 15;
$browse_sections = array( 'popular', 'new', 'updated' );
$api_url = 'https://api.wordpress.org/themes/info/1.2/';

update_option( 'blogname', 'Theme Directory' );
update_option( 'blogdescription', 'Free WordPress Themes' );

/**
* Fetch themes for a given browse section from the Themes API.
*/
function fetch_themes( $api_url, $section, $count ) {
$url = add_query_arg(
array(
'action' => 'query_themes',
'request[browse]' => $section,
'request[per_page]' => $count,
'request[fields][description]' => 1,
'request[fields][tags]' => 1,
'request[fields][versions]' => 1,
'request[fields][active_installs]' => 1,
'request[fields][downloaded]' => 1,
'request[fields][extended_author]' => 1,
'request[fields][requires]' => 1,
'request[fields][requires_php]' => 1,
),
$api_url
);

$response = wp_remote_get( $url, array( 'timeout' => 60 ) );
if ( is_wp_error( $response ) ) {
return array();
}

$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $data['themes'] ) ) {
return array();
}

return $data['themes'];
}

/**
* Ensure a WordPress user exists.
*/
function ensure_user( $nicename, $display_name = '' ) {
if ( get_user_by( 'slug', $nicename ) ) {
return;
}

wp_insert_user( array(
'user_login' => $nicename,
'user_nicename' => $nicename,
'user_email' => $nicename . '@example.invalid',
'display_name' => $display_name ?: $nicename,
'user_pass' => wp_generate_password(),
'role' => 'subscriber',
) );
}

/**
* Import a single theme from API data.
*/
function save_theme( $theme ) {
// Ensure author exists.
$author_id = 0;
if ( ! empty( $theme['author']['user_nicename'] ) ) {
$nicename = $theme['author']['user_nicename'];
ensure_user( $nicename, $theme['author']['display_name'] ?? '' );
$author_user = get_user_by( 'slug', $nicename );
if ( $author_user ) {
$author_id = $author_user->ID;
}
}

// Check if already exists.
$existing = get_posts( array(
'post_type' => 'repopackage',
'name' => $theme['slug'],
'post_status' => 'any',
'numberposts' => 1,
) );

$post_args = array(
'post_type' => 'repopackage',
'post_title' => $theme['name'],
'post_name' => $theme['slug'],
'post_status' => 'publish',
'post_content' => $theme['description'] ?? '',
);

if ( $author_id ) {
$post_args['post_author'] = $author_id;
}

if ( $existing ) {
$post_args['ID'] = $existing[0]->ID;
wp_update_post( $post_args );
$post_id = $existing[0]->ID;
} else {
$post_id = wp_insert_post( $post_args );
}

if ( ! $post_id || is_wp_error( $post_id ) ) {
return null;
}

// Build the _status meta — only store the current version to match latest_version() expectations.
if ( ! empty( $theme['version'] ) ) {
$status = array( $theme['version'] => 'live' );
update_post_meta( $post_id, '_status', $status );
}

// Set the current live version.
if ( ! empty( $theme['version'] ) ) {
update_post_meta( $post_id, '_live_version', $theme['version'] );
}

// Store author meta per version.
if ( ! empty( $theme['version'] ) ) {
$version = $theme['version'];
if ( ! empty( $theme['author']['author'] ) ) {
update_post_meta( $post_id, "_author_{$version}", $theme['author']['author'] );
}
if ( ! empty( $theme['author']['author_url'] ) ) {
update_post_meta( $post_id, "_author_url_{$version}", $theme['author']['author_url'] );
}
}

// Screenshot — stored as a version-keyed array mapping version => filename.
if ( ! empty( $theme['version'] ) ) {
$screenshots = get_post_meta( $post_id, '_screenshot', true ) ?: array();
if ( ! is_array( $screenshots ) ) {
$screenshots = array();
}
$screenshots[ $theme['version'] ] = 'screenshot.png';
update_post_meta( $post_id, '_screenshot', $screenshots );
}

// Tags.
if ( ! empty( $theme['tags'] ) && is_array( $theme['tags'] ) ) {
wp_set_object_terms( $post_id, array_keys( $theme['tags'] ), 'post_tag' );
}

// Business model.
if ( ! empty( $theme['is_commercial'] ) ) {
wp_set_object_terms( $post_id, 'commercial', 'theme_business_model' );
} elseif ( ! empty( $theme['is_community'] ) ) {
wp_set_object_terms( $post_id, 'community', 'theme_business_model' );
}

// Active installs.
if ( isset( $theme['active_installs'] ) ) {
update_post_meta( $post_id, '_active_installs', (int) $theme['active_installs'] );
}

// Download count.
if ( isset( $theme['downloaded'] ) ) {
update_post_meta( $post_id, '_downloaded', (int) $theme['downloaded'] );
}

// Requirements.
if ( ! empty( $theme['requires'] ) ) {
update_post_meta( $post_id, '_requires', $theme['requires'] );
}
if ( ! empty( $theme['requires_php'] ) ) {
update_post_meta( $post_id, '_requires_php', $theme['requires_php'] );
}

return get_post( $post_id );
}

// Main loop.
$imported_slugs = array();

foreach ( $browse_sections as $section ) {
echo "Fetching themes in '{$section}' section...\n";

$themes = fetch_themes( $api_url, $section, $per_section );
echo " Found " . count( $themes ) . " themes.\n";

$imported = 0;
$tagged = 0;

foreach ( $themes as $theme_data ) {
$slug = $theme_data['slug'];

if ( in_array( $slug, $imported_slugs, true ) ) {
// Already imported in a previous section, just tag it.
$existing = get_posts( array(
'post_type' => 'repopackage',
'name' => $slug,
'post_status' => 'any',
'numberposts' => 1,
) );
if ( $existing ) {
echo " {$slug}... {$existing[0]->post_title} (tagged)\n";
$tagged++;
}
continue;
}

echo " {$slug}...";

$post = save_theme( $theme_data );
if ( ! $post ) {
echo " failed.\n";
continue;
}

$imported_slugs[] = $slug;
echo " {$post->post_title} (done)\n";
$imported++;
}

echo " {$section}: {$imported} new, {$tagged} tagged.\n\n";
}

// Flush rewrite rules.
flush_rewrite_rules();

echo "Done!\n";
Loading
Loading