395 lines
13 KiB
PHP
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 );
|