1<?php
2##
3## Copyright 2013-2018 Opera Software AS
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
18if(isset($router->vars['objects'])) {
19	$api = new API;
20	switch($router->vars['objects']) {
21	case 'zones':
22		if(isset($router->vars['id'])) {
23			if(isset($router->vars['subobjects'])) {
24				switch($router->vars['subobjects']) {
25				case 'changes':
26					if(isset($router->vars['subid'])) {
27						$api->zone_change($router->vars['id'], $router->vars['subid']);
28					} else {
29						$api->zone_changes($router->vars['id']);
30					}
31				}
32			} else {
33				$api->zone($router->vars['id']);
34			}
35		} else {
36			$api->zones();
37		}
38	default:
39		header('HTTP/1.1 404 Not found');
40	}
41} else {
42	$content = new PageSection('apihelp');
43
44	$page = new PageSection('base');
45	$page->set('title', 'API documentation');
46	$page->set('content', $content);
47	$page->set('alerts', array());
48	echo $page->generate();
49}
50
51class API {
52	public function options($options, $params = array()) {
53		if($_SERVER['REQUEST_METHOD'] == 'OPTIONS' || !isset($options[$_SERVER['REQUEST_METHOD']])) {
54			if(!isset($options[$_SERVER['REQUEST_METHOD']])) {
55				header('HTTP/1.1 405 Method not allowed');
56			}
57			$optionlist = array_keys($options);
58			$optionlist[] = 'OPTIONS';
59			header('Allow: '.implode(', ', $optionlist));
60			exit;
61		} else {
62			try {
63				call_user_func_array(array($this, $options[$_SERVER['REQUEST_METHOD']]), $params);
64			} catch(AccessDenied $e) {
65				header('HTTP/1.1 403 Forbidden');
66				log_exception($e);
67				$this->output(['errors' => [['userMessage' => 'Access denied', 'code' => 3]]]);
68			} catch(InvalidJSON $e) {
69				header('HTTP/1.1 400 Bad request');
70				log_exception($e);
71				$this->output(['errors' => [['userMessage' => 'Invalid JSON', 'internalMessage' => $e->getMessage(), 'code' => 1]]]);
72			} catch(BadData $e) {
73				header('HTTP/1.1 400 Bad request');
74				log_exception($e);
75				$this->output(['errors' => [['userMessage' => 'Bad data', 'internalMessage' => $e->getMessage(), 'code' => 2]]]);
76			} catch(ResourceRecordInvalid $e) {
77				header('HTTP/1.1 400 Bad request');
78				log_exception($e);
79				$this->output(['errors' => [['userMessage' => 'Invalid resource record', 'internalMessage' => $e->getMessage(), 'code' => 4]]]);
80			} catch(Exception $e) {
81				header('HTTP/1.1 500 Internal server error');
82				log_exception($e);
83				$this->output(['errors' => [['userMessage' => 'Something went wrong (server fault)', 'code' => 0]]]);
84			}
85		}
86	}
87
88	public function zones() {
89		$this->options(array('GET' => 'list_zones'));
90	}
91
92	public function list_zones() {
93		global $active_user;
94		$zones = $active_user->list_accessible_zones();
95		$list = array();
96		foreach($zones as $zone) {
97			$item = new StdClass;
98			$item->name = $zone->name;
99			$item->serial = $zone->serial;
100			$list[] = $item;
101		}
102		$this->output($list);
103	}
104
105	public function zone($zone_name) {
106		$this->options(array('GET' => 'show_zone', 'PATCH' => 'update_zone_rrsets'), array($zone_name));
107	}
108
109	public function show_zone($zone_name) {
110		global $zone_dir, $active_user;
111		$zone = $zone_dir->get_zone_by_name($zone_name);
112		if(!$active_user->admin && !$active_user->access_to($zone)) throw new AccessDenied;
113		$data = new StdClass;
114		$data->name = $zone->name;
115		$data->serial = $zone->serial;
116		$data->rrsets = array();
117		foreach($zone->list_resource_record_sets() as $rrset) {
118			$rrs_data = new StdClass;
119			$rrs_data->name = DNSName::abbreviate($rrset->name, $zone->name);
120			$rrs_data->type = $rrset->type;
121			$rrs_data->ttl = DNSTime::abbreviate($rrset->ttl);
122			$rrs_data->records = array();
123			foreach($rrset->list_resource_records() as $rr) {
124				$rr_data = new StdClass;
125				$rr_data->content = DNSContent::decode($rr->content, $rrset->type, $zone_name);
126				$rr_data->enabled = !$rr->disabled;
127				$rrs_data->records[] = $rr_data;
128			}
129			$rrs_data->comments = array();
130			foreach($rrset->list_comments() as $comment) {
131				$comment_data = new StdClass;
132				$comment_data->content = $comment->content;
133				$comment_data->account = $comment->account;
134				$comment_data->modified_at = $comment->modified_at;
135				$rrs_data->comments[] = $comment_data;
136			}
137			$data->rrsets[] = $rrs_data;
138		}
139		$this->output($data);
140	}
141
142	public function update_zone_rrsets($zone_name) {
143		global $zone_dir, $active_user;
144		$zone = $zone_dir->get_zone_by_name($zone_name);
145		if(!$active_user->admin && !$active_user->access_to($zone)) throw new AccessDenied;
146		$json = file_get_contents('php://input');
147		$zone->process_bulk_json_rrset_update($json);
148		$this->output(null);
149	}
150
151	public function zone_changes($zone_name) {
152		$this->options(array('GET' => 'list_zone_changes'), array($zone_name));
153	}
154
155	public function list_zone_changes($zone_name) {
156		global $zone_dir, $active_user;
157		$zone = $zone_dir->get_zone_by_name($zone_name);
158		if(!$active_user->admin && !$active_user->access_to($zone)) throw new AccessDenied;
159		$changesets = $zone->list_changesets();
160		$list = array();
161		foreach($changesets as $changeset) {
162			$item = new StdClass;
163			$item->id = $changeset->id;
164			$item->author_uid = $changeset->author->uid;
165			$item->change_date = $changeset->change_date->format('c');
166			$item->comment = $changeset->comment;
167			$item->deleted = $changeset->deleted;
168			$item->added = $changeset->added;
169			$list[] = $item;
170		}
171		$this->output($list);
172	}
173
174	public function zone_change($zone_name, $changeset_id) {
175		$this->options(array('GET' => 'show_zone_change'), array($zone_name, $changeset_id));
176	}
177
178	public function show_zone_change($zone_name, $changeset_id) {
179		global $zone_dir, $active_user;
180		$zone = $zone_dir->get_zone_by_name($zone_name);
181		if(!$active_user->admin && !$active_user->access_to($zone)) throw new AccessDenied;
182		$changeset = $zone->get_changeset_by_id($changeset_id);
183		$data = new StdClass;
184		$data->id = $changeset->id;
185		$data->author_uid = $changeset->author->uid;
186		$data->change_date = $changeset->change_date->format('c');
187		$data->comment = $changeset->comment;
188		$data->deleted = $changeset->deleted;
189		$data->added = $changeset->added;
190		$data->changes = array();
191		foreach($changeset->list_changes() as $change) {
192			$c_data = new StdClass;
193			$states = array();
194			$states['before'] = unserialize($change->before);
195			$states['after'] = unserialize($change->after);
196			foreach($states as $state => $rrset) {
197				if($rrset) {
198					$c_data->{$state} = new StdClass;
199					$c_data->{$state}->name = $rrset->name;
200					$c_data->{$state}->type = $rrset->type;
201					$c_data->{$state}->ttl = DNSTime::abbreviate($rrset->ttl);
202					$c_data->{$state}->rrs = array();
203					$c_data->{$state}->comment = $rrset->merge_comment_text();
204					$rrs = $rrset->list_resource_records();
205					foreach($rrs as $rr) {
206						$rr_data = new StdClass;
207						$rr_data->content = DNSContent::decode($rr->content, $rr->type, $zone_name);
208						$rr_data->enabled = !$rr->disabled;
209						$c_data->{$state}->rrs[] = $rr_data;
210					}
211				}
212			}
213			$data->changes[] = $c_data;
214		}
215		$this->output($data);
216	}
217
218	public function created($url) {
219		header('HTTP/1.1 201 Created');
220		header('Location: /api/v2'.$url);
221		exit;
222	}
223
224	private function output($data) {
225		header('Content-type: application/json');
226		echo json_encode($data, JSON_PRETTY_PRINT);
227		exit;
228	}
229}
230