Get list of unpublished items through API

When using /content/items/{model}, can one also grab unpublished entries? It seems to return published entries only. I’ve tried using filter {"_state":0}, but that seems to be ignored entirely.
If I use the same filter on /content/item/{model} (so singular), it works fine, but I’m looking to retrieve a list of unpublished entries.

It makes sense to not show unpublished entries, if the API is exposed on a public website that, for example, shows blog posts. I can imagine you don’t want unpublished blog posts to be retrievable. But there are other cases where you do want to be able to grab everything.

Am I missing something? It might be so simple, but I can’t see it at the moment :slight_smile:.

Thanks!


You need to comment in line 474
modules/Content/api.php
then you can use filter {"_state":0}

Please don’t modify core code :wink:

I would recommend to create a custom api endpoint, like described here: Authenticate API with Username and Password - #6 by artur

1 Like

Thanks for the hint. I’m looking into this, but don’t yet know the best approach. I’m more than happy to create an add-on for this, which could then also be shared with others. Wouldn’t it be possible to place all required files in the addon directory instead?

For example, in a folder all-items inside addons I copied api.php from /modules/Content/api.php and removed everything I don’t need. Then renamed the path to /content/allitems/{model}. This results in the path being displayed in the “api” area of the GUI - which is great, because then I can test the endpoint through the GUI.

image

When I try to test it, I get a 404, but perhaps that’s obvious. Is what I’m looking for possible?

the correct path config/api/allitems.php

the api url /api/allitems

you can copy api.php to allitems.php and modify you need

Thanks. I’ve moved the file and I can call it (and also still see it in the GUI), but I can only call “api/allitems”, not “api/allitems/{model}” (where {model} could be any collection). How would one go about that?

Currently working on trying to return at least something. Hopefully I’ll figure that part out soon.

Create the following file: config/api/allitems/[...all].get.php

Within that file you have access to a variable called $API_ARGS

So if you do a GET request against eg /api/allitems/posts then $API_ARGS[0] == 'posts'

Thanks, Artur! I got it working now. The endpoint shows up in the GUI (not necessary, but I like it) and it returns all results I want. Thanks a lot for your help, this inspires me to learn more.

For anyone interested, in my case I made the following file: config/api/content/allitems/[...all].get.php

And this is the code I put in there (mind you, pretty much everything was copied from /modules/Content/api.php):

<?php

/**
 *
 * @OA\Tag(
 *   name="content",
 *   description="Content module",
 * )
 */
$this->on('restApi.config', function($restApi) {

	/**
	 * @OA\Get(
	 *     path="/content/allitems/{model}",
	 *     tags={"content"},
	 *     @OA\Parameter(
	 *         description="Model name",
	 *         in="path",
	 *         name="model",
	 *         required=true,
	 *         @OA\Schema(type="string")
	 *     ),
	 *    @OA\Parameter(
	 *         description="Return content for specified locale",
	 *         in="query",
	 *         name="locale",
	 *         required=false,
	 *         @OA\Schema(type="String")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Url encoded filter json",
	 *         in="query",
	 *         name="filter",
	 *         required=false,
	 *         @OA\Schema(type="json")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Url encoded sort json",
	 *         in="query",
	 *         name="sort",
	 *         required=false,
	 *         @OA\Schema(type="json")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Url encoded fields projection as json",
	 *         in="query",
	 *         name="fields",
	 *         required=false,
	 *         @OA\Schema(type="json")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Max amount of items to return",
	 *         in="query",
	 *         name="limit",
	 *         required=false,
	 *         @OA\Schema(type="int")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Amount of items to skip",
	 *         in="query",
	 *         name="skip",
	 *         required=false,
	 *         @OA\Schema(type="int")
	 *     ),
	 *     @OA\Parameter(
	 *         description="Populate items with linked content items.",
	 *         in="query",
	 *         name="populate",
	 *         required=false,
	 *         @OA\Schema(type="int")
	 *     ),
	 *     @OA\OpenApi(
	 *         security={
	 *             {"api_key": {}}
	 *         }
	 *     ),
	 *     @OA\Response(response="200", description="Get list of published model items"),
	 *     @OA\Response(response="401", description="Unauthorized"),
	 *     @OA\Response(response="404", description="Model not found")
	 * )
	 */
});


$app = $this;
$model = $API_ARGS[0];

// Check if model exists
if (!$app->module('content')->model($model)) {
	$app->response->status = 404;
	return ["error" => "Model <{$model}> not found"];
}

// Check if user has access to the model
if (!$app->helper('acl')->isAllowed("content/{$model}/read", $app->helper('auth')->getUser('role'))) {
	$app->response->status = 403;
	return ['error' => 'Permission denied'];
}

$options = [];
$process = ['locale' => $app->param('locale', 'default')];

$limit = $app->param('limit:int', null);
$skip = $app->param('skip:int', null);
$populate = $app->param('populate:int', null);
$filter = $app->param('filter:string', null);
$sort = $app->param('sort:string', null);
$fields = $app->param('fields:string', null);

if (!is_null($filter)) $options['filter'] = $filter;
if (!is_null($sort)) $options['sort'] = $sort;
if (!is_null($fields)) $options['fields'] = $fields;
if (!is_null($limit)) $options['limit'] = $limit;
if (!is_null($skip)) $options['skip'] = $skip;

foreach (['filter', 'fields', 'sort'] as $prop) {
	if (isset($options[$prop])) {
		try {
			$options[$prop] = json5_decode($options[$prop], true);
		} catch(\Throwable $e) {
			$app->response->status = 400;
			return ['error' => "<{$prop}> is not valid json"];
		}
	}
}

if ($populate) {
	$process['populate'] = $populate;
}

if (!isset($options['filter']) || !is_array($options['filter'])) {
	$options['filter'] = [];
}

// If the model doesn't allow to show unpublished (state=0) entries, force state=1
$modelProperties = $app->module('content')->model($model);
if ($modelProperties["meta"]["showUnpublished"] !== true) {    
	$options['filter']['_state'] = 1;
}

$items = $app->module('content')->items($model, $options, $process);

if (isset($options['skip'], $options['limit'])) {
	return [
		'data' => $items,
		'meta' => [
			'total' => $app->module('content')->count($model, $options['filter'] ?? [])
		]
	];
}

if (count($items)) {
	$app->trigger('content.api.items', [&$items, $model]);
}

return $items;

In most cases, you don’t want all collections to display unpublished entries, especially if your API is public. So, for the collections you want unpublished items to show up, make sure to put the following in ‘meta’:

{
  showUnpublished: true,
}

If this information is not present, it’ll act exactly the same as /api/content/items/{model}, where the filter “_state=1” (published entries only) is forced.

2 Likes