# Get list of unpublished items through API

**URL:** <https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609>\
**Category:** Cockpit v2\
**Created:** [December 13, 2022, 10:16pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609 "2022-12-13T22:16:47Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![witsec](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/witsec/32/818_2.png) [@witsec](https://discourse.getcockpit.com/u/witsec)\
**Post date:** [December 13, 2022, 10:16pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/1 "2022-12-13T22:16:47Z")

</div>

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 🙂.

Thanks!

---

<div class="post-metadata">

**Author:** ![creativeplusplus](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/creativeplusplus/32/844_2.png) [@creativeplusplus](https://discourse.getcockpit.com/u/creativeplusplus)\
**Post date:** [December 14, 2022, 10:05am UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/2 "2022-12-14T10:05:25Z")

</div>

![Screenshot 2022-12-14 at 17.02.57](https://canada1.discourse-cdn.com/flex030/uploads/getcockpit/original/2X/d/d555bc6cffecfff815e07379067f3c8b15400878.png)  
You need to comment in line 474  
modules/Content/api.php  
then you can use filter `{"_state":0}`

---

<div class="post-metadata">

**Author:** ![artur](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/artur/32/4_2.png) [@artur](https://discourse.getcockpit.com/u/artur)\
**Post date:** [December 14, 2022, 4:35pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/3 "2022-12-14T16:35:02Z")

</div>

Please don’t modify core code 😉

I would recommend to create a custom api endpoint, like described here: [Authenticate API with Username and Password - #6 by artur](https://discourse.getcockpit.com/t/authenticate-api-with-username-and-password/2472/6)

---

<div class="post-metadata">

**Author:** ![witsec](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/witsec/32/818_2.png) [@witsec](https://discourse.getcockpit.com/u/witsec)\
**Post date:** [December 14, 2022, 7:35pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/4 "2022-12-14T19:35:20Z")

</div>

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](https://canada1.discourse-cdn.com/flex030/uploads/getcockpit/original/2X/2/22ff511915c99a7187769dfa798aaa486c043a12.png)

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

---

<div class="post-metadata">

**Author:** ![creativeplusplus](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/creativeplusplus/32/844_2.png) [@creativeplusplus](https://discourse.getcockpit.com/u/creativeplusplus)\
**Post date:** [December 15, 2022, 7:24am UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/5 "2022-12-15T07:24:44Z")

</div>

the correct path `config/api/allitems.php`

the api url `/api/allitems`

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

---

<div class="post-metadata">

**Author:** ![witsec](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/witsec/32/818_2.png) [@witsec](https://discourse.getcockpit.com/u/witsec)\
**Post date:** [December 15, 2022, 7:53pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/6 "2022-12-15T19:53:16Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![artur](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/artur/32/4_2.png) [@artur](https://discourse.getcockpit.com/u/artur)\
**Post date:** [December 15, 2022, 10:05pm UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/7 "2022-12-15T22:05:59Z")

</div>

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'`

---

<div class="post-metadata">

**Author:** ![witsec](https://yyz2.discourse-cdn.com/flex030/user_avatar/discourse.getcockpit.com/witsec/32/818_2.png) [@witsec](https://discourse.getcockpit.com/u/witsec)\
**Post date:** [December 17, 2022, 9:52am UTC](https://discourse.getcockpit.com/t/get-list-of-unpublished-items-through-api/2609/8 "2022-12-17T09:52:12Z")

</div>

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`):

```auto
<?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’:

```auto
{
  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.
