Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

#! /usr/bin/env python3 

# -*- coding: utf-8 -*- 

 

#       Copyright 2013-2014, Marten de Vries 

# 

#       This file is part of OpenTeacher. 

# 

#       OpenTeacher is free software: you can redistribute it and/or modify 

#       it under the terms of the GNU General Public License as published by 

#       the Free Software Foundation, either version 3 of the License, or 

#       (at your option) any later version. 

# 

#       OpenTeacher is distributed in the hope that it will be useful, 

#       but WITHOUT ANY WARRANTY; without even the implied warranty of 

#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 

#       GNU General Public License for more details. 

# 

#       You should have received a copy of the GNU General Public License 

#       along with OpenTeacher.  If not, see <http://www.gnu.org/licenses/>. 

 

import flask 

import flask_recaptcha 

import feedparser 

import functools 

import collections 

import superjson 

import tempfile 

import os 

import json 

import contextlib 

import datetime 

import locale 

import hmac 

import hashlib 

import requests 

 

locale.setlocale(locale.LC_ALL, "C") 

 

#Imports handled by the module: 

# 

#import createWebDatabase 

#import gettextFunctions 

#import loaders 

#import savers 

#import metadata 

 

app = flask.Flask(__name__) 

recaptcha = flask_recaptcha.ReCaptcha() 

 

class DummyLesson: 

        def __init__(self, list, *args, **kwargs): 

                super().__init__(*args, **kwargs) 

 

                self.list = list 

                self.resources = {} 

 

#utils 

def get_couch(): 

        try: 

                return flask.g.couch 

        except AttributeError: 

                flask.g.couch = createWebDatabase( 

                        app.config["COUCHDB_HOST"], 

                        app.config["COUCHDB_ADMIN_USERNAME"], 

                        app.config["COUCHDB_ADMIN_PASSWORD"], 

                ) 

                return get_couch() 

 

feed = {} 

def get_feed(): 

        last_updated = feed.get("last_update", datetime.datetime.min) 

        if datetime.datetime.now() - last_updated > datetime.timedelta(minutes=30): 

                feed["data"] = feedparser.parse(metadata["newsFeedUrl"]) 

                feed["last_update"] = datetime.datetime.now() 

        return feed["data"] 

 

def json_default(item): 

        try: 

                return item.isoformat() 

        except AttributeError as e: 

                raise TypeError("Can't provide a default value") 

 

def jsonify(data): 

        dataJson = superjson.dumps(data, indent=4, default=json_default) 

        resp = flask.make_response(dataJson) 

        resp.headers["Content-Type"] = "application/json" 

        return resp 

 

def json_err(msg, code=400): 

        resp = jsonify({"error": msg}) 

        resp.status_code = code 

        return resp 

 

def auth_err(msg): 

        resp = json_err(msg, 401) 

        resp.headers["WWW-Authenticate"] = 'Basic realm="%s Web"' % metadata["name"] 

        return resp 

 

def requires_auth(f): 

        @functools.wraps(f) 

        def decorated(*args, **kwargs): 

                auth = flask.request.authorization 

                if not auth and flask.request.form.get("username"): 

                        auth = { 

                                "username": flask.request.form.get("username"), 

                                "password": flask.request.form.get("password", ""), 

                        } 

                if not auth: 

                        return auth_err("authentication_required") 

                if auth["username"] == "anonymous": 

                        return auth_err("The user 'anonymous' isn't allowed to use the services API.") 

                if not get_couch().check_auth(auth["username"], auth["password"]): 

                        return auth_err("wrong_username_or_password") 

                return f(*args, **kwargs) 

        return decorated 

 

#Thanks Armin Ronacher! See: http://flask.pocoo.org/snippets/56/ 

def crossdomain(origin=None, methods=None, headers=None, credentials="false", max_age=21600, attach_to_all=True, automatic_options=True): 

        if not origin: 

                return lambda f: f 

        if methods is not None: 

                methods = ', '.join(sorted(x.upper() for x in methods)) 

        if headers is not None and not isinstance(headers, str): 

                headers = ', '.join(x.upper() for x in headers) 

        if not isinstance(origin, str): 

                origin = ', '.join(origin) 

        if isinstance(max_age, datetime.timedelta): 

                max_age = max_age.total_seconds() 

 

        def get_methods(): 

                if methods is not None: 

                        return methods 

 

                options_resp = app.make_default_options_response() 

                return options_resp.headers['allow'] 

 

        def decorator(f): 

                def wrapped_function(*args, **kwargs): 

                        if automatic_options and flask.request.method == 'OPTIONS': 

                                resp = app.make_default_options_response() 

                        else: 

                                resp = flask.make_response(f(*args, **kwargs)) 

                        if not attach_to_all and flask.request.method != 'OPTIONS': 

                                return resp 

 

                        h = resp.headers 

 

                        h['Access-Control-Allow-Origin'] = origin 

                        h['Access-Control-Allow-Methods'] = get_methods() 

                        h['Access-Control-Max-Age'] = str(max_age) 

                        if headers is not None: 

                                h['Access-Control-Allow-Headers'] = headers 

                        if credentials: 

                                h['Access-Control-Allow-Credentials'] = "true" 

                        return resp 

 

                f.provide_automatic_options = False 

                return functools.update_wrapper(wrapped_function, f) 

        return decorator 

 

def allow_cross_domain_from_trusted_origins(f): 

        if app.config.get("TRUSTED_ORIGINS"): 

                return crossdomain(origin=app.config["TRUSTED_ORIGINS"], credentials=True, headers="authorization")(f) 

        else: 

                return f 

 

def load_exts(_cache=[]): 

        if not _cache: 

                for loader in loaders: 

                        for ext, dataTypes in loader.loads.items(): 

                                if "words" in dataTypes: 

                                        _cache.append(ext) 

        return _cache 

 

def save_exts(_cache=[]): 

        if not _cache: 

                for saver in savers: 

                        _cache.extend(saver.saves.get("words", [])) 

        return _cache 

 

def jsonp(f): 

        @functools.wraps(f) 

        def wrapper(*args, **kwargs): 

                resp = f(*args, **kwargs) 

                try: 

                        callback = flask.request.args["callback"] 

                except KeyError: 

                        return resp 

                else: 

                        content = str(callback) + "(" + superjson.dumps(resp) + ")" 

                        resp = flask.make_response(content) 

                        resp.headers["Content-Type"] = "application/javascript" 

                        return resp 

        return wrapper 

 

def initialize_endpoints(): 

        with app.app_context(): 

                if not get_couch().check_auth("anonymous", "anonymous"): 

                        get_couch().create_anonymous_user() 

 

        recaptcha.init_app(app) 

 

        #services 

        @app.route("/", methods=["OPTIONS", "GET"]) 

        @allow_cross_domain_from_trusted_origins 

        def services(): 

                data = collections.OrderedDict([ 

                        ("welcome", "%s Web services" % metadata["name"]), 

                        ("info", "All api entry points require HTTP Basic Authentication."), 

                        ("web_entry_points", [ 

                                flask.url_for("register"), 

                                flask.url_for("news"), 

                        ]), 

                        ("api_entry_points", [ 

                                flask.url_for("deregister"), 

                                flask.url_for("load"), 

                                flask.url_for("supported_load_extensions"), 

                                flask.url_for("save"), 

                                flask.url_for("supported_save_extensions"), 

                                flask.url_for("build_ci"), 

                        ]) 

                ]) 

                return jsonify(data) 

 

        @app.route("/news") 

        @jsonp 

        def news(): 

                def nice_time(struct): 

                        obj = datetime.datetime(*struct[:6]) 

                        return obj.strftime("%H:%M:%S at %B %d, %Y") 

                with open(app.config["NEWS_TEMPLATE_PATH"], encoding='UTF-8') as f: 

                        return flask.render_template_string(f.read(), feed=get_feed(), nice_time=nice_time) 

 

        @app.route("/register") 

        def register(): 

                try: 

                        redirect = flask.request.args["redirect"] 

                except KeyError: 

                        return "<h1>Required GET URL parameter: redirect.</h1>" 

                try: 

                        language = flask.request.args["language"] 

                except KeyError: 

                        return "<h1>Required GET URL parameter: language. ('C' will suffice if your app is available in English only.)</h1>" 

                screenshotOnly = flask.request.args.get("screenshotonly", "false") 

 

                _, ngettext = gettextFunctions(language) 

 

                error = { 

                        "invalid_captcha": _("Invalid captcha. Please try again."), 

                        "unsafe_password": _("Your password should at least be %s characters long, and contain special (non alphanumeric) characters. Please try again.") % 8, 

                        "username_taken": _("The username you requested is already taken. Please try again.") 

                }.get(flask.request.args.get("error"), u"") 

 

                data = {"screenshotOnly": screenshotOnly == 'true', "redirect": redirect, "error": error, "_": _, "ngettext": ngettext, "language": language} 

                with open(app.config["REGISTER_TEMPLATE_PATH"], encoding='UTF-8') as f: 

                        return flask.render_template_string(f.read(), **data) 

 

        @app.route("/register/send", methods=["OPTIONS", "POST"]) 

        def register_send(): 

                redirect_url = flask.request.form["redirect"] 

                language = flask.request.form["language"] 

                def error(e): 

                        return flask.redirect(flask.url_for("register") + "?error=" + e + "&redirect=" + redirect_url + "&language=" + language) 

 

                if not recaptcha.verify(): 

                        return error("invalid_captcha") 

 

                username = flask.request.form["username"] 

                password = flask.request.form["password"] 

                try: 

                        get_couch().new_user(username, password) 

                except ValueError as e: 

                        return error(str(e)) 

 

                return flask.redirect(redirect_url + "?status=ok") 

 

        @app.route("/deregister", methods=["OPTIONS", "POST"]) 

        @allow_cross_domain_from_trusted_origins 

        @requires_auth 

        def deregister(): 

                auth = flask.request.authorization 

                try: 

                        get_couch().delete_user(auth.username) 

                except ValueError as e: 

                        return json_err(str(e)) 

                return jsonify({"result": "ok"}) 

 

        @app.route("/load/supported_extensions", methods=["GET", "OPTIONS"]) 

        @allow_cross_domain_from_trusted_origins 

        @requires_auth 

        def supported_load_extensions(): 

                return jsonify({"result": load_exts()}) 

 

        @app.route("/load", methods=["OPTIONS", "POST"]) 

        @allow_cross_domain_from_trusted_origins 

        @requires_auth 

        def load(): 

                try: 

                        f = flask.request.files["file"] 

                except KeyError as e: 

                        return json_err("Please upload a file (name='file')") 

                ext = os.path.splitext(f.filename)[1] 

                #strip the . in .otwd 

                if not ext[1:] in load_exts(): 

                        return json_err("Invalid file type for the uploaded file.") 

 

                fd, path = tempfile.mkstemp(ext) 

                os.close(fd) 

                f.save(path) 

 

                resp = json_err("Couldn't load file") 

                for loader in loaders: 

                        if loader.getFileTypeOf(path) == "words": 

                                try: 

                                        result = loader.load(path) 

                                except Exception: 

                                        continue 

                                else: 

                                        resp = jsonify(result["list"]) 

                                        break 

 

                os.remove(path) 

                return resp 

 

        @app.route("/save/supported_extensions", methods=["GET", "OPTIONS"]) 

        @allow_cross_domain_from_trusted_origins 

        @requires_auth 

        def supported_save_extensions(): 

                return jsonify({"result": save_exts()}) 

 

        @app.route("/save", methods=["OPTIONS", "POST"]) 

        @allow_cross_domain_from_trusted_origins 

        @requires_auth 

        def save(): 

                try: 

                        list = json.loads(flask.request.form["list"]) 

                except KeyError: 

                        return json_err("Please specify the 'list' field in the body form data.") 

                except ValueError: 

                        return json_err("The posted list is invalid json.") 

 

                dateFormat = "%Y-%m-%dT%H:%M:%S.%fZ" 

                for item in list.get("items", []): 

                        with contextlib.suppress(KeyError): 

                                item["created"] = datetime.datetime.strptime(item["created"], dateFormat) 

                for test in list.get("tests", []): 

                        for result in test.get("results", []): 

                                with contextlib.suppress(KeyError): 

                                        result["active"]["start"] = datetime.datetime.strptime(result["active"]["start"], dateFormat) 

                                        result["active"]["end"] = datetime.datetime.strptime(result["active"]["end"], dateFormat) 

                lesson = DummyLesson(list) 

 

                try: 

                        filename = flask.request.form["filename"] 

                except KeyError: 

                        return json_err("Please specify the 'filename' field in the body form data.") 

 

                #[1:] tears of the . in .txt 

                ext = os.path.splitext(filename)[1][1:] 

                if not ext in save_exts(): 

                        return json_err("Unsupported filename extension. (%s)" % ext) 

 

                fd, path = tempfile.mkstemp("." + ext) 

                os.close(fd) 

                for saver in savers: 

                        if ext in saver.saves.get("words", []): 

                                saver.save("words", lesson, path) 

                                resp = flask.send_file(path, as_attachment=True, attachment_filename=filename) 

                                break 

 

                os.remove(path) 

                return resp 

 

        @app.route("/build-ci", methods=["POST"]) 

        def build_ci(): 

                signature = flask.request.headers.get("X-Hub-Signature", "") 

                try: 

                        contentLength = int(flask.request.headers.get("Content-Length")) 

                except ValueError: 

                        return json_err("Invalid content length") 

                if contentLength > 1024 * 1024: 

                        # 1 MB 

                        return json_err("Body too large") 

 

                body = flask.request.get_data() 

                key = app.config["SIGNATURE_SECRET"].encode('ascii') 

                hash = hmac.new(key, body, hashlib.sha1).hexdigest().encode('ascii') 

                expected = b"sha1=" + hash 

                if not hmac.compare_digest(signature.encode('ascii'), expected): 

                        return json_err("Invalid signature", 401) 

 

                travis = requests.post("https://api.travis-ci.org/repo/openteacher%2Fopenteacher-ci/requests", json={ 

                        "request": { 

                                "branch": "master", 

                                "message": "Build triggered by Launchpad branch update." 

                        }, 

                }, headers={ 

                        "User-Agent": "OpenTeacher web services server", 

                        "Content-Type": "application/json", 

                        "Accept": "application/json", 

                        "Travis-API-Version": "3", 

                        "Authorization": "token %s" % app.config["TRAVIS_TOKEN"], 

                }) 

                appveyor = requests.post("https://ci.appveyor.com/api/builds", json={ 

                        "accountName": "openteacher", 

                        "projectSlug": "openteacher-ci", 

                        "branch": "master", 

                }, headers={ 

                        "Authorization": "Bearer %s" % app.config["APPVEYOR_TOKEN"], 

                }) 

 

                return jsonify({ 

                        "travis": travis.json(), 

                        "appveyor": appveyor.json(), 

                })