1/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 *     Copyright 2017 Couchbase, Inc
4 *
5 *   Licensed under the Apache License, Version 2.0 (the "License");
6 *   you may not use this file except in compliance with the License.
7 *   You may obtain a copy of the License at
8 *
9 *       http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *   Unless required by applicable law or agreed to in writing, software
12 *   distributed under the License is distributed on an "AS IS" BASIS,
13 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *   See the License for the specific language governing permissions and
15 *   limitations under the License.
16 */
17
18#include "collections/filter.h"
19#include "collections/collections_dockey.h"
20#include "collections/collections_types.h"
21
22#include <JSON_checker.h>
23#include <cJSON.h>
24#include <cJSON_utils.h>
25#include <memcached/engine_error.h>
26#include <platform/make_unique.h>
27
28#include <iostream>
29
30/**
31 * Construct a Collections::Filter, optionally using a JSON document with the
32 * following format specifying which collections are allowed DcpStreams created
33 * from a DcpProducer who owns this.
34 *
35 * Format:
36 *   {"collection":["collection1", "collection2"]}
37 *
38 * Each collection in the "collection" array must be found in the Manifest
39 *
40 * @param jsonFilter an optional std::string. If this is not initialised than
41 *        a legacy (non-collection) DcpProducer is being opened.
42 * @param manifest the Manifest (bucket manifest) to check the filter is valid
43 *        which can be null in the case when the bucket has not been told the
44 *        manifest.
45 * @throws invalid_argument for input errors (with detailed message)
46 */
47Collections::Filter::Filter(boost::optional<const std::string&> jsonFilter,
48                            const Manifest* manifest)
49    : defaultAllowed(false), passthrough(false), systemEventsAllowed(true) {
50    // If the jsonFilter is not initialised we are building a filter for a
51    // legacy DCP stream, one which could only ever support $default
52    if (!jsonFilter.is_initialized()) {
53        // 1. If there's no manifest, we'll allow the construction, $default may
54        //    or may not exist, streamRequest will re-check.
55        // 2. If manifest is specified then we can check for $default.
56        if (!manifest || manifest->doesDefaultCollectionExist()) {
57            defaultAllowed = true;
58
59            // This filter is for a 'legacy' producer which will not know about
60            // system events.
61            systemEventsAllowed = false;
62            return;
63        } else {
64            throw cb::engine_error(cb::engine_errc::unknown_collection,
65                                   "Filter::Filter no $default");
66        }
67    }
68
69    auto json = jsonFilter.get();
70
71    // The filter is the empty string. Create this Filter to be:
72    // 1. passthrough - all collections allowed
73    // 2. defaultAllowed -  all $default items allowed
74    // 3. systemEventsAllowed - the client will be informed of system events
75    //    this is already set by the initializer list.
76    if (json.empty()) {
77        passthrough = true;
78        defaultAllowed = true;
79        return;
80    }
81
82    if (!checkUTF8JSON(reinterpret_cast<const unsigned char*>(json.data()),
83                       json.size())) {
84        throw cb::engine_error(cb::engine_errc::invalid_arguments,
85                               "Filter::Filter input not valid jsonFilter:" +
86                                       jsonFilter.get());
87    }
88
89    unique_cJSON_ptr cjson(cJSON_Parse(json.c_str()));
90    if (!cjson) {
91        throw cb::engine_error(cb::engine_errc::invalid_arguments,
92                               "Filter::Filter cJSON cannot parse jsonFilter:" +
93                                       jsonFilter.get());
94    }
95
96    // Now before processing the JSON we must have a manifest. We cannot create
97    // a filtered producer without a manifest.
98    if (!manifest) {
99        throw cb::engine_error(cb::engine_errc::no_collections_manifest,
100                               "Filter::Filter no manifest");
101    }
102
103    // @todo null check manifest to go past this point. Will be done along with
104    // an appropriate error-code
105
106    auto jsonCollections = cJSON_GetObjectItem(cjson.get(), "collections");
107    bool nameFound = false, uidFound = false;
108    if (!jsonCollections || jsonCollections->type != cJSON_Array) {
109        throw cb::engine_error(
110                cb::engine_errc::invalid_arguments,
111                "Filter::Filter cannot find collections:" +
112                        (!jsonCollections
113                                 ? "nullptr"
114                                 : std::to_string(jsonCollections->type)) +
115                        ", jsonFilter:" + jsonFilter.get());
116    } else {
117        for (int ii = 0; ii < cJSON_GetArraySize(jsonCollections); ii++) {
118            auto collection = cJSON_GetArrayItem(jsonCollections, ii);
119            if (!(collection && (collection->type == cJSON_String ||
120                                 collection->type == cJSON_Object))) {
121                throw cb::engine_error(
122                        cb::engine_errc::invalid_arguments,
123                        "Filter::Filter cannot find "
124                        "valid collection for index:" +
125                                std::to_string(ii) + ", collection:" +
126                                (!collection
127                                         ? "nullptr"
128                                         : std::to_string(collection->type)) +
129                                ", jsonFilter:" + jsonFilter.get());
130            } else {
131                if (collection->type == cJSON_String) {
132                    // Can throw..
133                    addCollection(collection->valuestring, *manifest);
134                    nameFound = true;
135                } else {
136                    addCollection(collection, *manifest);
137                    uidFound = true;
138                }
139            }
140        }
141    }
142
143    // Validate that the input hasn't mixed and matched name/uid vs name
144    // require one or the other.
145    if (uidFound && nameFound) {
146        throw cb::engine_error(
147                cb::engine_errc::invalid_arguments,
148                "Filter::Filter mixed name/uid not allowed jsonFilter:" +
149                        jsonFilter.get());
150    }
151    type = uidFound ? Type::NameUid : Type::Name;
152}
153
154void Collections::Filter::addCollection(const char* collection,
155                                        const Manifest& manifest) {
156    // Is this the default collection?
157    if (DefaultCollectionIdentifier == collection) {
158        if (manifest.doesDefaultCollectionExist()) {
159            defaultAllowed = true;
160        } else {
161            throw cb::engine_error(cb::engine_errc::unknown_collection,
162                                   "Filter::addCollection no $default");
163        }
164    } else {
165        auto itr = manifest.find(collection);
166        if (itr != manifest.end()) {
167            filter.push_back({collection, {}});
168        } else {
169            throw cb::engine_error(cb::engine_errc::unknown_collection,
170                                   "Filter::addCollection unknown collection:" +
171                                           std::string(collection));
172        }
173    }
174}
175
176void Collections::Filter::addCollection(cJSON* object,
177                                        const Manifest& manifest) {
178    auto jsonName = cJSON_GetObjectItem(object, "name");
179    auto jsonUID = cJSON_GetObjectItem(object, "uid");
180
181    if (!jsonName || jsonName->type != cJSON_String) {
182        throw cb::engine_error(
183                cb::engine_errc::invalid_arguments,
184                "Filter::Filter invalid collection name:" +
185                        (!jsonName ? "nullptr"
186                                   : std::to_string(jsonName->type)));
187    }
188
189    if (!jsonUID || jsonUID->type != cJSON_String) {
190        throw cb::engine_error(
191                cb::engine_errc::invalid_arguments,
192                "Filter::Filter invalid collection uid:" +
193                        (!jsonUID ? "nullptr" : std::to_string(jsonUID->type)));
194    }
195
196    auto entry = manifest.find(
197            {{jsonName->valuestring}, makeUid(jsonUID->valuestring)});
198
199    if (entry == manifest.end()) {
200        throw cb::engine_error(
201                cb::engine_errc::unknown_collection,
202                "Filter::Filter: cannot add unknown collection:" +
203                        std::string(jsonName->valuestring) + ":" +
204                        std::string(jsonUID->valuestring));
205    } else {
206        filter.push_back({cb::to_string(entry->getName()), {entry->getUid()}});
207    }
208}
209
210void Collections::Filter::dump() const {
211    std::cerr << *this << std::endl;
212}
213
214std::ostream& Collections::operator<<(std::ostream& os,
215                                      const Collections::Filter& filter) {
216    os << "Collections::Filter"
217       << ": passthrough:" << filter.passthrough
218       << ", defaultAllowed:" << filter.defaultAllowed
219       << ", systemEventsAllowed:" << filter.systemEventsAllowed;
220    switch (filter.getType()) {
221    case Collections::Filter::Type::Name:
222        os << ", type:name";
223        break;
224    case Collections::Filter::Type::NameUid:
225        os << ", type:name-uid";
226        break;
227    }
228    os << ", filter.size:" << filter.filter.size() << std::endl;
229    for (const auto& entry : filter.filter) {
230        os << entry.first;
231        if (entry.second.is_initialized()) {
232            os << ":" << entry.second.get();
233        }
234        os << std::endl;
235    }
236    return os;
237}
238