1import os
2import posixpath
3
4import click
5from flask import Blueprint
6from flask import current_app
7from flask import g
8from flask import jsonify
9from flask import request
10
11from lektor._compat import iteritems
12from lektor._compat import itervalues
13from lektor.admin.utils import eventstream
14from lektor.environment import PRIMARY_ALT
15from lektor.publisher import publish
16from lektor.publisher import PublishError
17from lektor.utils import is_valid_id
18
19
20bp = Blueprint("api", __name__, url_prefix="/admin/api")
21
22
23def get_record_and_parent(path):
24    pad = g.admin_context.pad
25    record = pad.get(path)
26    if record is None:
27        parent = pad.get(posixpath.dirname(path))
28    else:
29        parent = record.parent
30    return record, parent
31
32
33@bp.route("/pathinfo")
34def get_path_info():
35    """Returns the path segment information for a record."""
36    tree_item = g.admin_context.tree.get(request.args["path"])
37    segments = []
38
39    while tree_item is not None:
40        segments.append(
41            {
42                "id": tree_item.id,
43                "path": tree_item.path,
44                "label_i18n": tree_item.label_i18n,
45                "exists": tree_item.exists,
46                "can_have_children": tree_item.can_have_children,
47            }
48        )
49        tree_item = tree_item.get_parent()
50
51    segments.reverse()
52    return jsonify(segments=segments)
53
54
55@bp.route("/recordinfo")
56def get_record_info():
57    pad = g.admin_context.pad
58    request_path = request.args["path"]
59    tree_item = g.admin_context.tree.get(request_path)
60    children = []
61    attachments = []
62    alts = []
63
64    for child in tree_item.iter_children():
65        if child.is_attachment:
66            attachments.append(child)
67        else:
68            children.append(child)
69
70    primary_alt = pad.db.config.primary_alternative
71    if primary_alt is not None:
72        for alt in itervalues(tree_item.alts):
73            alt_cfg = pad.db.config.get_alternative(alt.id)
74            alts.append(
75                {
76                    "alt": alt.id,
77                    "is_primary": alt.id == PRIMARY_ALT,
78                    "primary_overlay": alt.id == primary_alt,
79                    "name_i18n": alt_cfg["name"],
80                    "exists": alt.exists,
81                }
82            )
83
84    child_order_by = pad.query(request_path).get_order_by() or []
85
86    return jsonify(
87        id=tree_item.id,
88        path=tree_item.path,
89        label_i18n=tree_item.label_i18n,
90        exists=tree_item.exists,
91        is_attachment=tree_item.is_attachment,
92        attachments=[
93            {
94                "id": x.id,
95                "path": x.path,
96                "type": x.attachment_type,
97            }
98            for x in attachments
99        ],
100        children=[
101            {
102                "id": x.id,
103                "path": x.path,
104                "label": x.id,
105                "label_i18n": x.label_i18n,
106                "visible": x.is_visible,
107            }
108            for x in sorted(
109                children, key=lambda c: pad.get(c.path).get_sort_key(child_order_by)
110            )
111        ],
112        alts=alts,
113        can_have_children=tree_item.can_have_children,
114        can_have_attachments=tree_item.can_have_attachments,
115        can_be_deleted=tree_item.can_be_deleted,
116    )
117
118
119@bp.route("/previewinfo")
120def get_preview_info():
121    alt = request.args.get("alt") or PRIMARY_ALT
122    record = g.admin_context.pad.get(request.args["path"], alt=alt)
123    if record is None:
124        return jsonify(exists=False, url=None, is_hidden=True)
125    return jsonify(exists=True, url=record.url_path, is_hidden=record.is_hidden)
126
127
128@bp.route("/find", methods=["POST"])
129def find():
130    alt = request.values.get("alt") or PRIMARY_ALT
131    lang = request.values.get("lang") or g.admin_context.info.ui_lang
132    q = request.values.get("q")
133    builder = current_app.lektor_info.get_builder()
134    return jsonify(results=builder.find_files(q, alt=alt, lang=lang))
135
136
137@bp.route("/browsefs", methods=["POST"])
138def browsefs():
139    alt = request.values.get("alt") or PRIMARY_ALT
140    record = g.admin_context.pad.get(request.values["path"], alt=alt)
141    okay = False
142    if record is not None:
143        if record.is_attachment:
144            fn = record.attachment_filename
145        else:
146            fn = record.source_filename
147        if os.path.exists(fn):
148            click.launch(fn, locate=True)
149            okay = True
150    return jsonify(okay=okay)
151
152
153@bp.route("/matchurl")
154def match_url():
155    record = g.admin_context.pad.resolve_url_path(
156        request.args["url_path"], alt_fallback=False
157    )
158    if record is None:
159        return jsonify(exists=False, path=None, alt=None)
160    return jsonify(exists=True, path=record["_path"], alt=record["_alt"])
161
162
163@bp.route("/rawrecord")
164def get_raw_record():
165    alt = request.args.get("alt") or PRIMARY_ALT
166    ts = g.admin_context.tree.edit(request.args["path"], alt=alt)
167    return jsonify(ts.to_json())
168
169
170@bp.route("/newrecord")
171def get_new_record_info():
172    # XXX: convert to tree usage
173    pad = g.admin_context.pad
174    alt = request.args.get("alt") or PRIMARY_ALT
175    ts = g.admin_context.tree.edit(request.args["path"], alt=alt)
176    if ts.is_attachment:
177        can_have_children = False
178    elif ts.datamodel.child_config.replaced_with is not None:
179        can_have_children = False
180    else:
181        can_have_children = True
182    implied = ts.datamodel.child_config.model
183
184    def describe_model(model):
185        primary_field = None
186        if model.primary_field is not None:
187            f = model.field_map.get(model.primary_field)
188            if f is not None:
189                primary_field = f.to_json(pad)
190        return {
191            "id": model.id,
192            "name": model.name,
193            "name_i18n": model.name_i18n,
194            "primary_field": primary_field,
195        }
196
197    return jsonify(
198        {
199            "label": ts.record and ts.record.record_label or ts.id,
200            "can_have_children": can_have_children,
201            "implied_model": implied,
202            "available_models": dict(
203                (k, describe_model(v))
204                for k, v in iteritems(pad.db.datamodels)
205                if not v.hidden or k == implied
206            ),
207        }
208    )
209
210
211@bp.route("/newattachment")
212def get_new_attachment_info():
213    ts = g.admin_context.tree.edit(request.args["path"])
214    return jsonify(
215        {
216            "can_upload": ts.exists and not ts.is_attachment,
217            "label": ts.record and ts.record.record_label or ts.id,
218        }
219    )
220
221
222@bp.route("/newattachment", methods=["POST"])
223def upload_new_attachments():
224    alt = request.values.get("alt") or PRIMARY_ALT
225    ts = g.admin_context.tree.edit(request.values["path"], alt=alt)
226    if not ts.exists or ts.is_attachment:
227        return jsonify({"bad_upload": True})
228
229    buckets = []
230
231    for file in request.files.getlist("file"):
232        buckets.append(
233            {
234                "original_filename": file.filename,
235                "stored_filename": ts.add_attachment(file.filename, file),
236            }
237        )
238
239    return jsonify(
240        {
241            "bad_upload": False,
242            "path": request.form["path"],
243            "buckets": buckets,
244        }
245    )
246
247
248@bp.route("/newrecord", methods=["POST"])
249def add_new_record():
250    values = request.get_json()
251    alt = values.get("alt") or PRIMARY_ALT
252    exists = False
253
254    if not is_valid_id(values["id"]):
255        return jsonify(valid_id=False, exists=False, path=None)
256
257    path = posixpath.join(values["path"], values["id"])
258
259    ts = g.admin_context.tree.edit(path, datamodel=values.get("model"), alt=alt)
260    with ts:
261        if ts.exists:
262            exists = True
263        else:
264            ts.update(values.get("data") or {})
265
266    return jsonify({"valid_id": True, "exists": exists, "path": path})
267
268
269@bp.route("/deleterecord", methods=["POST"])
270def delete_record():
271    alt = request.values.get("alt") or PRIMARY_ALT
272    delete_master = request.values.get("delete_master") == "1"
273    if request.values["path"] != "/":
274        ts = g.admin_context.tree.edit(request.values["path"], alt=alt)
275        with ts:
276            ts.delete(delete_master=delete_master)
277    return jsonify(okay=True)
278
279
280@bp.route("/rawrecord", methods=["PUT"])
281def update_raw_record():
282    values = request.get_json()
283    data = values["data"]
284    alt = values.get("alt") or PRIMARY_ALT
285    ts = g.admin_context.tree.edit(values["path"], alt=alt)
286    with ts:
287        ts.update(data)
288    return jsonify(path=ts.path)
289
290
291@bp.route("/servers")
292def get_servers():
293    db = g.admin_context.pad.db
294    config = db.env.load_config()
295    servers = config.get_servers(public=True)
296    return jsonify(
297        servers=sorted(
298            [x.to_json() for x in servers.values()], key=lambda x: x["name"].lower()
299        )
300    )
301
302
303@bp.route("/build", methods=["POST"])
304def trigger_build():
305    builder = current_app.lektor_info.get_builder()
306    builder.build_all()
307    builder.prune()
308    return jsonify(okay=True)
309
310
311@bp.route("/clean", methods=["POST"])
312def trigger_clean():
313    builder = current_app.lektor_info.get_builder()
314    builder.prune(all=True)
315    builder.touch_site_config()
316    return jsonify(okay=True)
317
318
319@bp.route("/publish")
320def publish_build():
321    db = g.admin_context.pad.db
322    server = request.values["server"]
323    config = db.env.load_config()
324    server_info = config.get_server(server)
325    info = current_app.lektor_info
326
327    @eventstream
328    def generator():
329        try:
330            event_iter = (
331                publish(
332                    info.env,
333                    server_info.target,
334                    info.output_path,
335                    server_info=server_info,
336                )
337                or ()
338            )
339            for event in event_iter:
340                yield {"msg": event}
341        except PublishError as e:
342            yield {"msg": "Error: %s" % e}
343
344    return generator()
345
346
347@bp.route("/ping")
348def ping():
349    return jsonify(project_id=current_app.lektor_info.env.project.id, okay=True)
350