Skip to content
Open
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
30 changes: 30 additions & 0 deletions apiki-favorites.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Plugin Name: Apiki Favorites
* Description: Favorite and unfavorite posts for authenticated users via the WordPress REST API.
* Version: 1.0.0
* Requires PHP: 5.6
* Author: Apiki Challenge
*/

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

define( 'APIKI_FAVORITES_VERSION', '1.0.0' );
define( 'APIKI_FAVORITES_PLUGIN_FILE', __FILE__ );
define( 'APIKI_FAVORITES_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );

require_once APIKI_FAVORITES_PLUGIN_PATH . 'includes/class-activator.php';
require_once APIKI_FAVORITES_PLUGIN_PATH . 'includes/class-favorites-repository.php';
require_once APIKI_FAVORITES_PLUGIN_PATH . 'includes/class-favorites-rest-controller.php';
require_once APIKI_FAVORITES_PLUGIN_PATH . 'includes/class-plugin.php';

register_activation_hook( __FILE__, array( 'Apiki_Favorites_Activator', 'activate' ) );

function apiki_favorites_run_plugin() {
$plugin = new Apiki_Favorites_Plugin();
$plugin->run();
}

apiki_favorites_run_plugin();
26 changes: 26 additions & 0 deletions includes/class-activator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

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

class Apiki_Favorites_Activator {

public static function activate() {
global $wpdb;

$table_name = $wpdb->prefix . 'apiki_favorites';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table_name} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL,
post_id bigint(20) unsigned NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY user_post (user_id, post_id)
) {$charset_collate};";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';

dbDelta( $sql );
}
}
59 changes: 59 additions & 0 deletions includes/class-favorites-repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

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

class Apiki_Favorites_Repository {

protected $wpdb;

protected $table_name;

public function __construct( $database = null ) {
global $wpdb;

$this->wpdb = $database ? $database : $wpdb;
$this->table_name = $this->wpdb->prefix . 'apiki_favorites';
}

public function exists( $user_id, $post_id ) {
$favorite_id = $this->wpdb->get_var(
$this->wpdb->prepare(
"SELECT id FROM {$this->table_name} WHERE user_id = %d AND post_id = %d LIMIT 1",
$user_id,
$post_id
)
);

return ! empty( $favorite_id );
}

public function insert( $user_id, $post_id ) {
return $this->wpdb->insert(
$this->table_name,
array(
'user_id' => $user_id,
'post_id' => $post_id,
),
array(
'%d',
'%d',
)
);
}

public function delete( $user_id, $post_id ) {
return $this->wpdb->delete(
$this->table_name,
array(
'user_id' => $user_id,
'post_id' => $post_id,
),
array(
'%d',
'%d',
)
);
}
}
149 changes: 149 additions & 0 deletions includes/class-favorites-rest-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

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

class Apiki_Favorites_REST_Controller {

protected $namespace = 'apiki/v1';

protected $repository;

public function __construct( Apiki_Favorites_Repository $repository ) {
$this->repository = $repository;
}

public function register_routes() {
register_rest_route(
$this->namespace,
'/posts/(?P<post_id>\d+)/favorite',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'favorite' ),
'permission_callback' => array( $this, 'permission_check' ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'unfavorite' ),
'permission_callback' => array( $this, 'permission_check' ),
),
)
);
}

public function permission_check( $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'apiki_favorites_forbidden',
'Authentication required.',
array( 'status' => 401 )
);
}

return true;
}

public function favorite( WP_REST_Request $request ) {
$post_id = (int) $request['post_id'];
$post = $this->get_valid_post( $post_id );
$user_id = get_current_user_id();

if ( is_wp_error( $post ) ) {
return $post;
}

if ( $this->repository->exists( $user_id, $post_id ) ) {
return new WP_Error(
'apiki_favorites_conflict',
'Post already favorited.',
array( 'status' => 409 )
);
}

$insert_result = $this->repository->insert( $user_id, $post_id );

if ( false === $insert_result ) {
if ( $this->repository->exists( $user_id, $post_id ) ) {
return new WP_Error(
'apiki_favorites_conflict',
'Post already favorited.',
array( 'status' => 409 )
);
}

return new WP_Error(
'apiki_favorites_insert_error',
'Could not favorite post.',
array( 'status' => 500 )
);
}

return new WP_REST_Response(
array(
'post_id' => $post_id,
'favorited' => true,
),
201
);
}

public function unfavorite( WP_REST_Request $request ) {
$post_id = (int) $request['post_id'];
$post = $this->get_valid_post( $post_id );
$user_id = get_current_user_id();

if ( is_wp_error( $post ) ) {
return $post;
}

if ( ! $this->repository->exists( $user_id, $post_id ) ) {
return new WP_Error(
'apiki_favorites_not_found',
'Favorite not found.',
array( 'status' => 404 )
);
}

$delete_result = $this->repository->delete( $user_id, $post_id );

if ( 0 === $delete_result ) {
return new WP_Error(
'apiki_favorites_not_found',
'Favorite not found.',
array( 'status' => 404 )
);
}

if ( false === $delete_result ) {
return new WP_Error(
'apiki_favorites_delete_error',
'Could not unfavorite post.',
array( 'status' => 500 )
);
}

return new WP_REST_Response(
array(
'post_id' => $post_id,
'favorited' => false,
),
200
);
}

protected function get_valid_post( $post_id ) {
$post = get_post( $post_id );

if ( ! $post || 'post' !== $post->post_type ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate post visibility before favorite operations

get_valid_post only checks that get_post returns a post type, but get_post can return private/draft posts even when the current user is not allowed to read them. In practice, any authenticated user can favorite/unfavorite non-public posts and infer their existence from the response code (404 vs 201/409/200). Add a permission/status check (for example current_user_can( 'read_post', $post_id ) or equivalent visibility logic) before treating the post as valid.

Useful? React with 👍 / 👎.

return new WP_Error(
'apiki_favorites_post_not_found',
'Post not found.',
array( 'status' => 404 )
);
}

return $post;
}
}
21 changes: 21 additions & 0 deletions includes/class-plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

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

class Apiki_Favorites_Plugin {

protected $repository;

protected $rest_controller;

public function __construct() {
$this->repository = new Apiki_Favorites_Repository();
$this->rest_controller = new Apiki_Favorites_REST_Controller( $this->repository );
}

public function run() {
add_action( 'rest_api_init', array( $this->rest_controller, 'register_routes' ) );
}
}