Files
Projects-Portfolio/projects-portfolio.php
T
Keith Solomon 9b6aa8a6e8
Release / Build & publish plugin zip (push) Successful in 7s
🔵 other: Bump version
2026-08-12 19:20:32 -05:00

395 lines
13 KiB
PHP

<?php
/**
* The plugin bootstrap file
*
* @link https://keithsolomon.net
* @since 1.0.0
* @package Projects_Portfolio
*
* @wordpress-plugin
*
* Plugin Name: Projects Portfolio
* Description: Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.
* Plugin URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
* Version: 1.2.0
* Author: Keith Solomon
* Author URI: https://keithsolomon.net
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: projects-wp
* Domain Path: /languages
* Update URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
*/
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
}
require 'includes/plugin-update-checker/plugin-update-checker.php';
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
$myUpdateChecker = PucFactory::buildUpdateChecker(
'https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio',
__FILE__,
'projects-portfolio'
);
// Note: PUC's setBranch() is only available when the host is recognized as a
// VCS provider (GitHub, GitLab, Bitbucket). The Gitea URL here falls back to
// PUC's plain JSON metadata mode, so we skip the call rather than throw.
// Current plugin version.
define( 'PROJECTS_PORTFOLIO_VERSION', '1.2.0' );
// Add the required files.
require 'admin/admin-settings.php';
require 'admin/cpt-taxonomy.php';
require 'admin/metabox.php';
require 'includes/helper-functions.php';
require 'includes/providers/interface-repository-provider.php';
require 'includes/providers/class-github-provider.php';
require 'includes/providers/class-gitea-provider.php';
require 'includes/providers/class-provider-factory.php';
/**
* Flush rewrite rules on activation.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_flush_rewrite_rules() {
projects_portfolio_register_cpt_and_taxonomy();
projects_portfolio_add_rewrite_rules();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'projects_portfolio_flush_rewrite_rules' );
/**
* Add rewrite rules for download endpoint.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_add_rewrite_rules() {
add_rewrite_rule(
'^download/([0-9]+)/?$',
'index.php?project_download_id=$matches[1]',
'top'
);
}
add_action( 'init', 'projects_portfolio_add_rewrite_rules' );
/**
* WP_Query Vars
*
* @param array $vars Query variables array.
*
* @since 1.0.0
* @return array
*/
function projects_portfolio_query_vars( $vars ) {
$vars[] = 'project_download_id';
return $vars;
}
add_filter( 'query_vars', 'projects_portfolio_query_vars' );
/**
* Handle Download Redirect and Increment Download Count
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_handle_download_redirect() {
$project_id = get_query_var( 'project_download_id' );
if ( $project_id ) {
$download_url = projects_portfolio_get_release_url( (int) $project_id );
if ( $download_url ) {
// Increment download count.
$download_count = (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true );
++$download_count;
update_post_meta( $project_id, '_projects_portfolio_download_count', $download_count );
// Redirect to the release ZIP on the project's host.
// Use wp_redirect() (not wp_safe_redirect()) because the destination
// is an external host the user has explicitly configured — wp_safe_redirect
// would reject it and fall back to admin_url().
wp_redirect( esc_url_raw( $download_url ), 302 ); // phpcs:ignore
exit;
} else {
wp_die(
esc_html__( 'Invalid download URL. Please check the repository URL.', 'projects-wp' ),
esc_html__( 'Download Error', 'projects-wp' ),
array( 'response' => 404 )
);
}
}
}
add_action( 'template_redirect', 'projects_portfolio_handle_download_redirect' );
/**
* Add Download Count Column to Projects Admin Table
*
* @since 1.0.0
* @param array $columns The columns array.
* @return mixed
*/
function projects_portfolio_add_download_count_column( $columns ) {
$columns['download_count'] = esc_html__( 'Download Count', 'projects-wp' );
return $columns;
}
add_filter( 'manage_projects_posts_columns', 'projects_portfolio_add_download_count_column' );
/**
* Display Download Count in Admin Column
*
* @since 1.0.0
* @param string $column The column name.
* @param int $post_id The post ID.
* @return void
*/
function projects_portfolio_display_download_count_column( $column, $post_id ) {
if ( 'download_count' === $column ) {
$download_count = (int) get_post_meta( $post_id, '_projects_portfolio_download_count', true );
echo $download_count ? esc_html( $download_count ) : esc_html__( '0', 'projects-wp' );
}
}
add_action( 'manage_projects_posts_custom_column', 'projects_portfolio_display_download_count_column', 10, 2 );
/**
* Make the Download Count Column Sortable
*
* @since 1.0.0
* @param array $columns The columns array.
* @return mixed
*/
function projects_portfolio_sortable_columns( $columns ) {
$columns['download_count'] = 'download_count';
return $columns;
}
add_filter( 'manage_edit-projects_sortable_columns', 'projects_portfolio_sortable_columns' );
/**
* Handle Sorting for Download Count
*
* @since 1.0.0
* @param WP_Query $query The query object.
* @return void
*/
function projects_portfolio_sort_download_count_column( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( 'download_count' === $query->get( 'orderby' ) ) {
$query->set( 'meta_key', '_projects_portfolio_download_count' );
$query->set( 'orderby', 'meta_value_num' );
}
}
add_action( 'pre_get_posts', 'projects_portfolio_sort_download_count_column' );
/**
* Register custom REST API endpoint for projects.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_register_custom_endpoint() {
register_rest_route(
'projects/v1',
'/projects',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'projects_portfolio_get_projects_data',
'permission_callback' => '__return_true',
)
);
}
add_action( 'rest_api_init', 'projects_portfolio_register_custom_endpoint' );
/**
* Load custom templates for single and archive project views.
*
* @since 1.0.0
* @param string $template The template file path.
* @return mixed
*/
function projects_portfolio_template_loader( $template ) {
if ( is_singular( 'projects' ) ) {
// Check for a single-projects.php template in the theme
$theme_template = locate_template( 'single-projects.php' );
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/single-projects.php';
}
if ( is_post_type_archive( 'projects' ) ) {
// Check for an archive-projects.php template in the theme
$theme_template = locate_template( 'archive-projects.php' );
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/archive-projects.php';
}
if ( is_tax( 'project-type' ) ) {
// Check for a taxonomy-project-type.php template in the theme
$theme_template = locate_template( 'archive-project-type.php' );
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/taxonomy-project-type.php';
}
return $template;
}
add_filter( 'template_include', 'projects_portfolio_template_loader' );
/**
* Enqueue styles for project single and archive templates.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_enqueue_project_styles() {
if ( is_singular( 'projects' ) || is_post_type_archive( 'projects' ) || is_tax( 'project-type' ) ) {
$is_fse_theme = wp_theme_has_theme_json() && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
// Register stylesheet for both theme types
wp_register_style(
'projects-wp-styles',
plugin_dir_url( __FILE__ ) . 'assets/css/style.css',
array(),
PROJECTS_PORTFOLIO_VERSION
);
if ( $is_fse_theme ) {
// Inline block style enqueue (needed for FSE themes)
add_action(
'wp_footer',
function () {
echo '<style id="projects-wp-styles">';
echo file_get_contents( plugin_dir_path( __FILE__ ) . 'assets/css/style.css' ); // phpcs:ignore
echo '</style>';
}
);
} else {
// Classic theme enqueue
wp_enqueue_style( 'projects-wp-styles' );
}
}
}
add_action( 'wp_enqueue_scripts', 'projects_portfolio_enqueue_project_styles' );
/**
* Enqueue admin styles.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_admin_styles() {
wp_enqueue_style( 'projects-wp-admin-css', plugin_dir_url( __FILE__ ) . 'assets/css/admin-styles.css' );
}
add_action( 'admin_enqueue_scripts', 'projects_portfolio_admin_styles' );
/**
* Modify the number of posts per page for the 'projects' post type archive.
*
* Ensures only the 'projects' archive page is affected without changing global settings.
*
* @param WP_Query $query The WordPress query object.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_custom_posts_per_page( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
$projects_per_page = apply_filters( 'projects_portfolio_archives_projects_per_page', 12 );
if ( is_post_type_archive( 'projects' ) ) {
$query->set( 'posts_per_page', $projects_per_page );
}
}
add_action( 'pre_get_posts', 'projects_portfolio_custom_posts_per_page', 1 );
/**
* Modify the posts per page for the 'projects' post type archive in Full Site Editing (FSE) themes.
*
* This function ensures that the 'projects' post type archive displays 12 posts per page,
* even when using an FSE theme with a Query Loop block.
*
* @param array $query The existing query variables for the Query Loop block.
* @param object $block The current block object (not used but available if needed).
*
* @since 1.0.0
* @return array Modified query variables with 'posts_per_page' set for the projects archive.
*/
function modify_projects_posts_per_page_fse( $query, $block ) { // phpcs:ignore
// Ensure we are modifying the archive query for the 'projects' post type.
if ( isset( $query['post_type'] ) && $query['post_type'] === 'projects' ) {
$query['posts_per_page'] = 12;
}
return $query;
}
add_filter( 'query_loop_block_query_vars', 'modify_projects_posts_per_page_fse', 10, 2 );
/**
* Add social sharing buttons with Tabler icons.
*
* @param int $project_id The project post ID.
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_social_sharing_buttons( $project_id ) {
$url = rawurlencode( get_permalink( $project_id ) );
$title = rawurlencode( get_the_title( $project_id ) );
$html = '<div class="project-social-sharing">';
$svg_icon = function ( string $name, string $file ) use ( &$html ) {
// Render each social icon as an <img> referencing a real .svg file
// shipped under assets/icons/. This bypasses any kses data: URI stripping
// and ensures the icon loads as a normal image via standard img rules.
$src = plugins_url( 'assets/icons/' . $file, __FILE__ );
$html .= '<img src="' . esc_url( $src ) . '" alt="' . esc_attr( $name ) . '" width="20" height="20" style="width:20px;height:20px;display:inline-block;vertical-align:middle;" />';
};
// Facebook.
$html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Facebook', 'facebook.svg' );
$html .= '</a>';
// X (Twitter).
$html .= '<a href="https://twitter.com/intent/tweet?text=' . $title . '&url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'X', 'x.svg' );
$html .= '</a>';
// LinkedIn.
$html .= '<a href="https://www.linkedin.com/sharing/share-offsite/?url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'LinkedIn', 'linkedin.svg' );
$html .= '</a>';
// Reddit.
$html .= '<a href="https://www.reddit.com/submit?url=' . $url . '&title=' . $title . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Reddit', 'reddit.svg' );
$html .= '</a>';
// WhatsApp.
$html .= '<a href="https://www.whatsapp.com/send?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'WhatsApp', 'whatsapp.svg' );
$html .= '</a>';
// Pinterest.
$html .= '<a href="https://pinterest.com/pin/create/button/?url=' . $url . '&description=' . $title . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Pinterest', 'pinterest.svg' );
$html .= '</a>';
// Email.
$html .= '<a href="mailto:?subject=' . $title . '&body=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Email', 'mail.svg' );
$html .= '</a>';
$html .= '</div>';
echo wp_kses_post( $html );
}
add_action( 'projects_after_download_button', 'projects_portfolio_social_sharing_buttons', 999 );