├── .gitignore
├── App
├── PlayBooksApp
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── markdown.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20200328_1431.py
│ │ ├── 0003_auto_20200328_1450.py
│ │ ├── 0004_auto_20200329_1900.py
│ │ ├── 0005_auto_20200401_1746.py
│ │ ├── 0006_remove_playpage_offline_copy.py
│ │ ├── 0007_auto_20200406_1814.py
│ │ ├── 0008_remove_playpage_included_folder_path.py
│ │ ├── 0009_auto_20200410_1315.py
│ │ ├── 0010_auto_20200411_1101.py
│ │ ├── 0011_playbook_creator.py
│ │ ├── 0012_playbooksection_creator.py
│ │ └── __init__.py
│ ├── models.py
│ ├── request.py
│ ├── source_resolver.py
│ ├── static
│ │ ├── css
│ │ │ ├── md.css
│ │ │ └── style.css
│ │ └── js
│ │ │ └── navigation.js
│ ├── templates
│ │ ├── _edit_page.html
│ │ ├── _page_content.html
│ │ ├── _search_results.html
│ │ ├── _select_server_file.html
│ │ ├── base.html
│ │ ├── index.html
│ │ └── playbook.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── PlayBooksWeb
│ ├── __init__.py
│ ├── apps.py
│ ├── asgi.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20200404_1311.py
│ │ └── __init__.py
│ ├── models.py
│ ├── settings.py
│ ├── static
│ │ ├── css
│ │ │ └── login.css
│ │ ├── external
│ │ │ ├── css
│ │ │ │ ├── bootstrap-select.css.map
│ │ │ │ ├── bootstrap-select.min.css
│ │ │ │ ├── bootstrap-toggle.min.css
│ │ │ │ ├── bootstrap.min.css
│ │ │ │ ├── bootstrap.min.css.map
│ │ │ │ ├── fontawesome_all.min.css
│ │ │ │ ├── jquery-confirm.min.css
│ │ │ │ └── jquery-ui.min.css
│ │ │ ├── js
│ │ │ │ ├── bootstrap-select.js.map
│ │ │ │ ├── bootstrap-select.min.js
│ │ │ │ ├── bootstrap-select.min.js.map
│ │ │ │ ├── bootstrap-toggle.min.js
│ │ │ │ ├── bootstrap-toggle.min.js.map
│ │ │ │ ├── bootstrap.min.js
│ │ │ │ ├── bootstrap.min.js.map
│ │ │ │ ├── jquery-confirm.min.js
│ │ │ │ ├── jquery-ui.min.js
│ │ │ │ ├── jquery.min.js
│ │ │ │ └── popper.min.js
│ │ │ ├── mdb
│ │ │ │ ├── css
│ │ │ │ │ ├── mdb.min.css
│ │ │ │ │ └── mdb.min.css.map
│ │ │ │ └── js
│ │ │ │ │ ├── mdb.min.js
│ │ │ │ │ └── mdb.min.js.map
│ │ │ └── webfonts
│ │ │ │ ├── fa-brands-400.eot
│ │ │ │ ├── fa-brands-400.svg
│ │ │ │ ├── fa-brands-400.ttf
│ │ │ │ ├── fa-brands-400.woff
│ │ │ │ ├── fa-brands-400.woff2
│ │ │ │ ├── fa-regular-400.eot
│ │ │ │ ├── fa-regular-400.svg
│ │ │ │ ├── fa-regular-400.ttf
│ │ │ │ ├── fa-regular-400.woff
│ │ │ │ ├── fa-regular-400.woff2
│ │ │ │ ├── fa-solid-900.eot
│ │ │ │ ├── fa-solid-900.svg
│ │ │ │ ├── fa-solid-900.ttf
│ │ │ │ ├── fa-solid-900.woff
│ │ │ │ └── fa-solid-900.woff2
│ │ ├── img
│ │ │ ├── Favicon.xml
│ │ │ ├── Logo.svg
│ │ │ ├── Logo.xml
│ │ │ └── favicon.ico
│ │ └── js
│ │ │ └── includeFolder.js
│ ├── templates
│ │ ├── _folder_index.html
│ │ ├── admin
│ │ │ ├── _base.html
│ │ │ └── base_site.html
│ │ ├── folder_change.html
│ │ └── registration
│ │ │ ├── logged_out.html
│ │ │ ├── login.html
│ │ │ └── registration_base.html
│ ├── urls.py
│ ├── validators.py
│ ├── views.py
│ └── wsgi.py
├── manage.py
└── settings.json
├── Docker
├── Dockerfile
└── README.md
├── README.md
├── SampleImages
├── Dashboard_Overview.png
├── Edit_Article_Example.png
├── HTTP_Upload.png
├── HTTP_Upload_Example.png
├── Server_Upload_Example.png
├── Text_Input_Example.png
└── Update_Feature.png
└── requirements.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite3
2 | *.pyc
3 |
4 | venv/*
5 | .vscode/*
--------------------------------------------------------------------------------
/App/PlayBooksApp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksApp/__init__.py
--------------------------------------------------------------------------------
/App/PlayBooksApp/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class PlayBooksAppConfig(AppConfig):
5 | name = 'PlayBooksApp'
6 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.forms import ModelForm, ChoiceField
3 | from django.core.validators import URLValidator
4 | from django.core.exceptions import ValidationError
5 | from django.utils.translation import gettext_lazy as _
6 | from django.urls import reverse
7 |
8 | from .models import Playpage, PlaybookSection, Playbook
9 | from .request import URLRequester
10 | from .markdown import MarkdownParser
11 | from .source_resolver import SourceResolver
12 |
13 | from PlayBooksWeb.models import IncludedFolder
14 |
15 |
16 | class ChoiceFieldNoValidation(ChoiceField):
17 | DEFAULT_CHOICE = ( ('-', (('', '-- Choose Folder --'),)),)
18 |
19 | def validate(self, value):
20 | pass
21 |
22 | class PlaypageForm(ModelForm):
23 | INIT_SOURCE_TYPE = None
24 |
25 | class Meta:
26 | model = Playpage
27 | fields = ['title','source_type', 'check_updates']
28 | help_texts = {
29 | 'title': _("Title of your Page. Keep this short but significant"),
30 | }
31 | widgets = {
32 | 'title': forms.TextInput(attrs={'required': '', 'class': 'form-control'}),
33 | 'check_updates': forms.CheckboxInput( attrs={'data-toggle': 'toggle', 'class': 'update-toggle'} )
34 | }
35 |
36 | def __init__(self, user, *args, **kwargs):
37 | super(PlaypageForm, self).__init__(*args, **kwargs)
38 | if( not self.instance.pk ):
39 | self.instance.creator = user
40 |
41 | if( self.INIT_SOURCE_TYPE ):
42 | self.instance.source_type = self.INIT_SOURCE_TYPE
43 | self['source_type'].initial = self.INIT_SOURCE_TYPE
44 |
45 | ## HTTP UPLOAD FIELD
46 | http_initial = self.instance.source if ( self.instance and self.instance.source_type == Playpage.SOURCE_HTTP ) else ''
47 | http_source = forms.CharField(required=False, label="HTTP Resource", initial=http_initial )
48 | http_source.help_text = _("Enter a HTTP URL to fetch Markdown Text from, e.g. Raw Github Pages.")
49 | http_source.widget = forms.TextInput(attrs={
50 | "data-event": 'urlPrefetch',
51 | "data-url-prefetch": reverse('apiPrefetchSource'),
52 | 'class': 'full-width',
53 | "placeholder": "https://raw.githubusercontent.com/redcanaryco/atomic-red-team/master/atomics/T1170/T1170.md"
54 | })
55 | self.fields['http_source'] = http_source
56 | ## FILE UPLOAD FIELD
57 | file_initial = self.instance.source if ( self.instance and self.instance.source_type == Playpage.SOURCE_UPLOAD ) else ''
58 | file_source = forms.FileField(required=False, label="File Upload", initial=file_initial)
59 | file_source.help_text = _("Upload a Markdown file")
60 | file_source.widget = forms.FileInput(attrs={
61 | "data-event": 'urlPrefetch',
62 | "data-url-prefetch": reverse('apiPrefetchSource'),
63 | 'class': 'form-control'
64 | })
65 | self.fields['file_source'] = file_source
66 | ## DISK UPLOAD FIELD
67 | ### DISK FILES
68 | if(
69 | self.instance.source_type == Playpage.SOURCE_DISK and
70 | self.instance and
71 | self.instance.included_folder and
72 | self.instance.included_folder.id and
73 | self.instance.source
74 | ):
75 | allowedExtensions = MarkdownParser.ALLOWED_FILE_EXTENSIONS
76 | included_folder_ID = self.instance.included_folder.id
77 | included_folder_path_choices = IncludedFolder.list_allowed_files(included_folder_ID, allowedExtensions)
78 | included_folder_path_initial = self.instance.source
79 | else:
80 | included_folder_path_choices = ChoiceFieldNoValidation.DEFAULT_CHOICE
81 | included_folder_path_initial = ''
82 | included_folder_path = ChoiceFieldNoValidation(required=False, label='File', initial=included_folder_path_initial)
83 | included_folder_path.help_text = _("Chose a File From Disk As Source")
84 | included_folder_path.widget = forms.Select(choices=included_folder_path_choices, attrs={
85 | 'class': 'form-control',
86 | 'data-event': 'urlPrefetch',
87 | 'data-url-prefetch': reverse('apiPrefetchSource'),
88 | 'data-live-search': 'true'
89 | })
90 | self.fields['included_folder_path'] = included_folder_path
91 | ### DISK Folders
92 | disk_folder_choices = [('', '-- Choose --')] + [ (folder.id, folder.name) for folder in IncludedFolder.objects.all() ] #
93 | included_folder_initial = self.instance.included_folder.id if ( self.instance.source_type == Playpage.SOURCE_DISK and self.instance and self.instance.included_folder ) else ''
94 | included_folder = forms.ChoiceField(required=False, label='Folder', initial=included_folder_initial)
95 | included_folder.choices = disk_folder_choices
96 | included_folder.help_text = _("Choose a Folder From Disk To Select a Markdown File")
97 | included_folder.widget = forms.Select(choices=disk_folder_choices, attrs={
98 | 'data-event': 'diskFolder',
99 | 'data-update-target': self['included_folder_path'].auto_id,
100 | 'data-url-fileoptions': reverse('apiServerFileOptions'),
101 | 'class': 'form-control'
102 | })
103 | self.fields['included_folder'] = included_folder
104 | ## PASTE UPLOAD
105 | text_source_initial = self.instance.offline_store if ( self.instance ) else ''
106 | text_source = forms.CharField(required=False, label=False, initial=text_source_initial )
107 | text_source.help_text = _("Write Markdown Yourself")
108 | text_source.widget = forms.Textarea(attrs={
109 | "data-event": 'urlPrefetch',
110 | "data-url-prefetch": reverse('apiPrefetchSource'),
111 | "placeholder": "Start Typing...",
112 | "class": "hidden"
113 | })
114 | self.fields['text_source'] = text_source
115 |
116 | ## FIELD Order
117 | self.field_order = ['title', 'source_type', 'http_source', 'file_source', 'included_folder', 'included_folder_path', 'text_source', 'check_updates']
118 | self.order_fields(self.field_order)
119 |
120 | self.source_options = [
121 | {'name': 'HTTP', 'source_type': self.instance.SOURCE_HTTP, 'source_field': self['source_type'].auto_id,
122 | 'allowed_fields': [
123 | self['title'].auto_id, self['check_updates'].auto_id, self['http_source'].auto_id
124 | ]
125 | },
126 | {'name': 'FILE UPLOAD', 'source_type': self.instance.SOURCE_UPLOAD, 'source_field': self['source_type'].auto_id,
127 | 'allowed_fields': [
128 | self['title'].auto_id, self['file_source'].auto_id
129 | ]
130 | }
131 | ,
132 | {'name': 'FROM SERVER', 'source_type': self.instance.SOURCE_DISK , 'source_field': self['source_type'].auto_id,
133 | 'allowed_fields': [
134 | self['title'].auto_id, self['check_updates'].auto_id, self['included_folder'].auto_id, self['included_folder_path'].auto_id
135 | ]
136 | },
137 | {'name': 'TEXT INPUT', 'source_type': self.instance.SOURCE_TEXT , 'source_field': self['source_type'].auto_id,
138 | 'allowed_fields': [
139 | self['title'].auto_id, self['text_source'].auto_id, "page_update_modal_editablediv"
140 | ]
141 | }
142 |
143 | ]
144 |
145 |
146 |
147 | def validate_file_source(self):
148 | file_source = self['file_source'].data
149 | ## check existing
150 | if( not file_source ):
151 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ):
152 | emsg = "File Source is required"
153 | self.add_error('file_source', emsg)
154 | return False
155 | ## check file upload content type
156 | if( hasattr(file_source, 'content_type') and not (file_source.content_type in MarkdownParser.ALLOWED_MIME_TYPES) ):
157 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ):
158 | emsg = "File Source MimeType is not allowed. Content Type is %s" %file_source.content_type
159 | self.add_error('file_source', emsg)
160 | return False
161 | ## check mime type based on content
162 | try:
163 | import magic
164 | import copy
165 | ## make a deep copy
166 | buffer_copy = copy.deepcopy(file_source)
167 | ## reset the read cursor
168 | buffer_copy.seek(0)
169 | prober = magic.Magic(mime=True)
170 | buffer = buffer_copy.read().decode('utf-8', errors='ignore')
171 | probed_content_type = prober.from_buffer(buffer)
172 | if( not (probed_content_type in MarkdownParser.ALLOWED_MIME_TYPES) ):
173 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ):
174 | emsg = "File Source MimeType is not allowed. Mime Type is %s" %probed_content_type
175 | self.add_error('file_source', emsg)
176 | return False
177 | except:
178 | ## pass along
179 | pass
180 |
181 | return True
182 |
183 | def validate_http_source(self):
184 | url = self['http_source'].data
185 | ## Check if existing
186 | if( not url ):
187 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ):
188 | emsg = "HTTP Resource is required"
189 | self.add_error('http_source', emsg)
190 | return False
191 | else:
192 | ## Check if valid URL
193 | urlvalidator = URLValidator(schemes=['http', 'https'])
194 | try:
195 | urlvalidator(url)
196 | except ValidationError as e:
197 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ):
198 | emsg = "Invalid URL scheme. Only http:// https:// is allowed"
199 | self.add_error('http_source', emsg)
200 | return False
201 | else:
202 | requester = URLRequester(url)
203 | requester.request()
204 | valid_response = requester.is_valid_response()
205 | if( not valid_response ):
206 | ## Add error if not already added
207 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ):
208 | status_code = requester.get_status_code()
209 | emsg = "Received Invalid Server Response. Status Code was: '%s'" %(status_code)
210 | self.add_error('http_source', emsg)
211 | return False
212 | return True
213 |
214 | def validate_included_folder(self):
215 | ## Validate folder
216 | include_fodlerID = self['included_folder'].data
217 | if( not include_fodlerID ):
218 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder') ):
219 | emsg = "Not a valid choice."
220 | self.add_error('included_folder', emsg)
221 | return False
222 | objs = IncludedFolder.objects.filter(pk=include_fodlerID)
223 | if objs.count() != 1:
224 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder') ):
225 | emsg = "Not a valid choice."
226 | self.add_error('included_folder', emsg)
227 | return False
228 | return True
229 |
230 | def validate_included_folder_path(self):
231 | ## Validate path
232 | include_folder_path = self['included_folder_path'].data
233 | if( not include_folder_path ):
234 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ):
235 | emsg = "Not a valid choice."
236 | self.add_error('included_folder_path', emsg)
237 | return False
238 | if( ("../" in include_folder_path) or ("..\\" in include_folder_path) ):
239 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ):
240 | emsg = "Illegal characters contained in choice."
241 | self.add_error('included_folder_path', emsg)
242 | return False
243 |
244 | ## Check included Folder existing
245 | included_folder = self['included_folder'].data
246 | if( not included_folder ):
247 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ):
248 | emsg = "No Folder has been selected"
249 | self.add_error('included_folder_path', emsg)
250 | return False
251 |
252 | ## Check file path
253 | included_folder_obj = IncludedFolder.objects.get( pk=included_folder )
254 | folder_path = included_folder_obj.path
255 | folder_path += '' if folder_path.endswith('/') else '/'
256 | file_path = "%s%s" %(folder_path, include_folder_path)
257 | if( not IncludedFolder.valid_file_path(file_path) ):
258 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ):
259 | emsg = "File not found on Server"
260 | self.add_error('included_folder_path', emsg)
261 | return False
262 | return True
263 |
264 | def validate_text_source(self):
265 | text_source = self['text_source'].data
266 | if( not text_source ):
267 | if( hasattr(self._errors, 'get') and not self._errors.get('text_source') ):
268 | emsg = "Required Field"
269 | self.add_error('text_source', emsg)
270 | return False
271 | return True
272 |
273 | def is_valid(self):
274 | ### Custom Validations
275 | valid_super = super(ModelForm, self).is_valid()
276 | source_type = self['source_type'].data
277 | valid_source = self.validate_source_type(source_type)
278 |
279 | return (valid_super and valid_source)
280 |
281 | def validate_source_type(self, source_type):
282 | if( source_type == Playpage.SOURCE_UPLOAD):
283 | return self.validate_file_source()
284 | elif( source_type == Playpage.SOURCE_HTTP ):
285 | return self.validate_http_source()
286 | elif( source_type == Playpage.SOURCE_DISK ):
287 | return ( self.validate_included_folder() and self.validate_included_folder_path() )
288 | elif( source_type == Playpage.SOURCE_TEXT ):
289 | return self.validate_text_source()
290 |
291 | def get_included_folder_path_options(self):
292 | return self.fields['included_folder_path'].widget.choices
293 |
294 | def clean(self):
295 | ## Parent Clean
296 | super().clean()
297 | ## Custom Cleaning
298 | source_type = self.cleaned_data['source_type']
299 | ## Setting Data
300 | if( source_type == Playpage.SOURCE_UPLOAD):
301 | valid = self.validate_file_source()
302 | if( valid ):
303 | ## can't be updated
304 | self.instance.check_updates = False
305 | self.cleaned_data['check_updates'] = False
306 | ## can't have include folder
307 | self.instance.included_folder = None
308 | self.cleaned_data['included_folder'] = None
309 | ## content
310 | self.instance.source = self.cleaned_data['file_source']
311 | self.instance.offline_store = self.cleaned_data['file_source'].read().decode('utf-8', errors='ignore')
312 |
313 | elif( source_type == Playpage.SOURCE_HTTP ):
314 | valid = self.validate_http_source()
315 | if( valid ):
316 | ## can't have include folder
317 | self.instance.included_folder = None
318 | self.cleaned_data['included_folder'] = None
319 | ## content
320 | url = self.cleaned_data.get('http_source', '')
321 | resolver = SourceResolver(self.instance)
322 | response = resolver.resolve_from_http(url)
323 | self.instance.source = self.cleaned_data['http_source']
324 | if( response ):
325 | self.instance.offline_store = response
326 |
327 | elif( source_type == Playpage.SOURCE_DISK ):
328 | valid_included_folder = self.validate_included_folder()
329 | if( valid_included_folder ):
330 | selected_included_folder = self.cleaned_data['included_folder']
331 | if( selected_included_folder ):
332 | allowedExtensions = MarkdownParser.ALLOWED_FILE_EXTENSIONS
333 | newChoices = IncludedFolder.list_allowed_files(selected_included_folder, allowedExtensions)
334 | self.fields['included_folder_path'].widget.choices = newChoices
335 | self.fields['included_folder_path'].choices = newChoices
336 | self.instance.included_folder = IncludedFolder.objects.get( pk=selected_included_folder )
337 | else:
338 | ## Reset to default choice
339 | newChoices = ChoiceFieldNoValidation.DEFAULT_CHOICE
340 | self.fields['included_folder_path'].widget.choices = newChoices
341 | self.fields['included_folder_path'].choices = newChoices
342 | valid_included_folder_path = self.validate_included_folder_path()
343 | if( valid_included_folder and valid_included_folder_path ):
344 | selected_included_folder = self.cleaned_data['included_folder']
345 | selected_included_folder_path = self.cleaned_data['included_folder_path']
346 | if( selected_included_folder_path ):
347 | self.instance.source = selected_included_folder_path
348 | resolver = SourceResolver(self.instance)
349 | content = resolver.resolve_from_disk(selected_included_folder, selected_included_folder_path)
350 | if( content ):
351 | self.instance.offline_store = content
352 |
353 | elif( source_type == Playpage.SOURCE_TEXT ):
354 | valid_text_source = self.validate_text_source()
355 | if( valid_text_source ):
356 | ## Keep original source type if exiting
357 | if( self.instance.pk ):
358 | self.cleaned_data['source_type'] = self.instance.source_type
359 | ## offline store
360 | self.instance.offline_store = self.cleaned_data['text_source']
361 |
362 | return self.cleaned_data
363 |
364 |
365 | class TEXTPLaypageForm(PlaypageForm):
366 | INIT_SOURCE_TYPE = Playpage.SOURCE_TEXT
367 |
368 |
369 | class PlaybookSectionForm(ModelForm):
370 |
371 | class Meta:
372 | model = PlaybookSection
373 | fields = ['name']
374 | help_texts = {
375 | 'name': _("Enter a name for a new Section"),
376 | }
377 | widgets = {'name': forms.TextInput(
378 | attrs = {
379 | 'autocomplete': 'off',
380 | 'placeholder': 'New Section Name',
381 | 'class': 'form-control'
382 | }
383 | )}
384 |
385 | def __init__(self, user, *args, **kwargs):
386 | super(PlaybookSectionForm, self).__init__(*args, **kwargs)
387 | if( not self.instance.pk ):
388 | self.instance.creator = user
389 |
390 | class PlaybookAddPageForm(forms.Form):
391 | page = forms.ChoiceField(
392 | label = "Add Existing Page",
393 | help_text = "Add existing Page",
394 | widget = forms.Select(
395 | attrs = {
396 | 'class': 'form-control selectpicker',
397 | 'data-live-search': 'true'
398 | }
399 | )
400 | )
401 |
402 | def __init__(self, user, *args, **kwargs):
403 | super(PlaybookAddPageForm, self).__init__(*args, **kwargs)
404 | accessible_pages = Playpage.accessible_pages(user)
405 | choices = [ (page.id, page.title) for page in accessible_pages ]
406 | self.fields['page'].choices = choices
407 | self.fields['page'].widget.choices = choices
408 |
409 | class PlaybookForm(ModelForm):
410 | class Meta:
411 | model = Playbook
412 | fields = ['name']
413 | help_texts = {
414 | 'name': _("Enter a name for a new PlayBook"),
415 | }
416 | widgets = {'name': forms.TextInput(
417 | attrs = {
418 | 'autocomplete': 'off',
419 | 'placeholder': 'New Playbook Name',
420 | 'class': 'form-control'
421 | }
422 | )}
423 |
424 |
425 | def __init__(self, user, *args, **kwargs):
426 | super(PlaybookForm, self).__init__(*args, **kwargs)
427 | if( not self.instance.pk ):
428 | self.instance.creator = user
--------------------------------------------------------------------------------
/App/PlayBooksApp/markdown.py:
--------------------------------------------------------------------------------
1 | import markdown2
2 | import codecs
3 | import re
4 |
5 | class MarkdownParser():
6 | EXTRAS = [
7 | "fenced-code-blocks",
8 | "cuddled-lists",
9 | "code-friendly",
10 | "numbering",
11 | "smarty-pants",
12 | "tables",
13 | "task_list"
14 | ]
15 | ALLOWED_MIME_TYPES = [
16 | 'text/plain',
17 | 'text/markdown'
18 | ]
19 | ALLOWED_FILE_EXTENSIONS = [
20 | 'md',
21 | 'txt'
22 | ]
23 | IMG_REPLACE_PATCH = 'data-action="replace-image" '
24 | IMG_REPLACE_CLASS = "action image-replace "
25 |
26 | def __init__(self, extras=None):
27 | self.extras = extras or self.EXTRAS
28 |
29 | def parseMD(self, mdText):
30 | markdowner = markdown2.Markdown(self.extras)
31 | html = markdowner.convert(mdText)
32 |
33 | return html
34 |
35 | def replaceImages(self, content, path_validation_callback, path_processing_callback, **kwargs):
36 | ## try to contained images
37 | pImgTag = re.compile(r"\!\[.+\]\(.+\)")
38 | pImgLoc = re.compile(r"\(.+\)$")
39 | for match in pImgTag.finditer(content):
40 | ## get image tag 
41 | mdImageTag = match.group()
42 | ## grab out image location (.../...)
43 | mdImgLocTag = pImgLoc.search(mdImageTag).group()
44 | ## strip out '(' and ')'
45 | imgSubPath = mdImgLocTag.lstrip('(').rstrip(')')
46 | ## path validation
47 | try:
48 | path_valid = path_validation_callback( imgSubPath, **kwargs)
49 | if( path_valid ):
50 | base64Img = path_processing_callback( imgSubPath, **kwargs )
51 | if( base64Img ):
52 | imgTag = "" %(base64Img, imgSubPath)
53 | content = content.replace(mdImageTag, imgTag)
54 | except Exception as e:
55 | pass
56 |
57 | return content
58 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-27 15:30
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Playbook',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(max_length=500)),
19 | ],
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0002_auto_20200328_1431.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-28 13:31
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('PlayBooksApp', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='PlaybookSection',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(max_length=100)),
19 | ],
20 | ),
21 | migrations.CreateModel(
22 | name='Playpage',
23 | fields=[
24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25 | ('title', models.CharField(max_length=500)),
26 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)),
27 | ('last_modified', models.DateTimeField(auto_now=True, null=True)),
28 | ],
29 | ),
30 | migrations.AddField(
31 | model_name='playbook',
32 | name='cover_img',
33 | field=models.TextField(default=''),
34 | ),
35 | migrations.AddField(
36 | model_name='playbook',
37 | name='created_at',
38 | field=models.DateTimeField(auto_now_add=True, null=True),
39 | ),
40 | migrations.AddField(
41 | model_name='playbook',
42 | name='last_modified',
43 | field=models.DateTimeField(auto_now=True, null=True),
44 | ),
45 | migrations.CreateModel(
46 | name='SectionContent',
47 | fields=[
48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49 | ('position', models.PositiveIntegerField()),
50 | ('playpage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='PlayBooksApp.Playpage')),
51 | ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='PlayBooksApp.PlaybookSection')),
52 | ],
53 | ),
54 | migrations.AddField(
55 | model_name='playbooksection',
56 | name='pages',
57 | field=models.ManyToManyField(related_name='sections', related_query_name='section', through='PlayBooksApp.SectionContent', to='PlayBooksApp.Playpage'),
58 | ),
59 | migrations.AddField(
60 | model_name='playbooksection',
61 | name='playbook',
62 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sections', related_query_name='section', to='PlayBooksApp.Playbook'),
63 | ),
64 | ]
65 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0003_auto_20200328_1450.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-28 13:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0002_auto_20200328_1431'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='sectioncontent',
15 | name='position',
16 | ),
17 | migrations.AddField(
18 | model_name='playbooksection',
19 | name='section_position',
20 | field=models.PositiveIntegerField(blank=True, null=True),
21 | ),
22 | migrations.AddField(
23 | model_name='sectioncontent',
24 | name='page_position',
25 | field=models.PositiveIntegerField(blank=True, null=True),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0004_auto_20200329_1900.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-29 17:00
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0003_auto_20200328_1450'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='playpage',
15 | name='check_updates',
16 | field=models.BooleanField(default=True),
17 | ),
18 | migrations.AddField(
19 | model_name='playpage',
20 | name='http_source',
21 | field=models.TextField(blank=True, null=True),
22 | ),
23 | migrations.AddField(
24 | model_name='playpage',
25 | name='offline_copy',
26 | field=models.BooleanField(default=False),
27 | ),
28 | migrations.AlterField(
29 | model_name='playbook',
30 | name='cover_img',
31 | field=models.TextField(blank=True, null=True),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0005_auto_20200401_1746.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-01 15:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0004_auto_20200329_1900'),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name='playpage',
15 | old_name='http_source',
16 | new_name='source',
17 | ),
18 | migrations.AddField(
19 | model_name='playpage',
20 | name='offline_store',
21 | field=models.TextField(blank=True, null=True),
22 | ),
23 | migrations.AddField(
24 | model_name='playpage',
25 | name='source_type',
26 | field=models.CharField(default='HTTP', max_length=500),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0006_remove_playpage_offline_copy.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-04 11:12
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0005_auto_20200401_1746'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='playpage',
15 | name='offline_copy',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0007_auto_20200406_1814.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-06 16:14
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('PlayBooksWeb', '0002_auto_20200404_1311'),
11 | ('PlayBooksApp', '0006_remove_playpage_offline_copy'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='playpage',
17 | name='included_folder',
18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='PlayBooksWeb.IncludedFolder'),
19 | ),
20 | migrations.AddField(
21 | model_name='playpage',
22 | name='included_folder_path',
23 | field=models.TextField(blank=True, null=True),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0008_remove_playpage_included_folder_path.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-06 18:17
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0007_auto_20200406_1814'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='playpage',
15 | name='included_folder_path',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0009_auto_20200410_1315.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-10 11:15
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('PlayBooksApp', '0008_remove_playpage_included_folder_path'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='playpage',
15 | name='check_updates',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0010_auto_20200411_1101.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-11 09:01
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('PlayBooksApp', '0009_auto_20200410_1315'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterModelOptions(
17 | name='playbooksection',
18 | options={'ordering': ['section_position']},
19 | ),
20 | migrations.AlterModelOptions(
21 | name='sectioncontent',
22 | options={'ordering': ['page_position']},
23 | ),
24 | migrations.AddField(
25 | model_name='playpage',
26 | name='creator',
27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
28 | ),
29 | migrations.AlterField(
30 | model_name='sectioncontent',
31 | name='page_position',
32 | field=models.PositiveIntegerField(default=0),
33 | ),
34 | migrations.AlterField(
35 | model_name='sectioncontent',
36 | name='playpage',
37 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='section_contents', related_query_name='section_contents', to='PlayBooksApp.Playpage'),
38 | ),
39 | migrations.AlterField(
40 | model_name='sectioncontent',
41 | name='section',
42 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='section_contents', related_query_name='section_contents', to='PlayBooksApp.PlaybookSection'),
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0011_playbook_creator.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-11 09:29
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('PlayBooksApp', '0010_auto_20200411_1101'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='playbook',
18 | name='creator',
19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/0012_playbooksection_creator.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-04-11 12:18
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('PlayBooksApp', '0011_playbook_creator'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='playbooksection',
18 | name='creator',
19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksApp/migrations/__init__.py
--------------------------------------------------------------------------------
/App/PlayBooksApp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 |
4 |
5 | from PlayBooksWeb.models import IncludedFolder
6 |
7 | class Playbook(models.Model):
8 | name = models.CharField(max_length=500)
9 | cover_img = models.TextField(null=True, blank=True)
10 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
11 | created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
12 | last_modified = models.DateTimeField(auto_now=True, null=True, blank=True)
13 |
14 | @classmethod
15 | def user_all(self, user):
16 | return self.objects.filter(creator=user)
17 |
18 | def user_sections(self, user):
19 | return self.sections.filter(creator=user)
20 |
21 | ## Only creator can delete Playbook
22 | def can_delete(self, user):
23 | return (user == self.creator)
24 |
25 | def __str__(self):
26 | return "name='%s', created='%s', last_modified='%s'" %(self.name, self.created_at, self.last_modified)
27 |
28 |
29 | class Playpage(models.Model):
30 | SOURCE_HTTP = "HTTP"
31 | SOURCE_UPLOAD = "UPLOAD"
32 | SOURCE_DISK = "DISK"
33 | SOURCE_TEXT = "TEXT"
34 |
35 | title = models.CharField(max_length=500)
36 | source = models.TextField(null=True, blank=True)
37 | source_type = models.CharField(max_length=500, default=SOURCE_HTTP)
38 | offline_store = models.TextField(null=True, blank=True)
39 | included_folder = models.ForeignKey(IncludedFolder, on_delete=models.SET_NULL, null=True, blank=True)
40 | check_updates = models.BooleanField(default=False)
41 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
42 | created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
43 | last_modified = models.DateTimeField(auto_now=True, null=True, blank=True)
44 |
45 | ## Access = Read + Write
46 | def can_access(self, user):
47 | return ( user == self.creator )
48 |
49 | ## Change Position
50 | def can_change_position(self, user):
51 | return self.can_access(user)
52 |
53 | ## Only creator can delete Pages
54 | def can_delete(self, user):
55 | return (user == self.creator)
56 |
57 | def can_be_updated(self):
58 | return self.source_type in [self.SOURCE_DISK, self.SOURCE_HTTP]
59 |
60 | def playbooks(self):
61 | return Playbook.objects.filter(section__pages__id=self.id)
62 |
63 | @classmethod
64 | def accessible_pages(self, user):
65 | return [ page for page in self.objects.all() if page.can_access(user) ]
66 |
67 | def save_model(self, request, obj, form, change):
68 | if( request.user and not obj.pk ):
69 | # Only set added_by during the first save.
70 | obj.creator = request.user
71 | super().save_model(request, obj, form, change)
72 |
73 |
74 | def set_position(self, position, sectionID):
75 | try:
76 | section_content = self.section_contents.get(section=sectionID)
77 | section_content.page_position = position
78 | section_content.save()
79 | except Exception as e:
80 | return e
81 | else:
82 | return True
83 |
84 | def __str__(self):
85 | return "title='%s', created='%s', last_modified='%s'" %(self.title, self.created_at, self.last_modified)
86 |
87 |
88 | class PlaybookSection(models.Model):
89 | class Meta:
90 | ordering = ['section_position']
91 |
92 | name = models.CharField(max_length=100)
93 | section_position = models.PositiveIntegerField(null=True, blank=True)
94 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
95 | playbook = models.ForeignKey(
96 | Playbook,
97 | on_delete=models.CASCADE,
98 | related_name='sections',
99 | related_query_name="section",)
100 | pages = models.ManyToManyField(
101 | Playpage,
102 | through='SectionContent', through_fields=('section', 'playpage'),
103 | related_name='sections',
104 | related_query_name="section")
105 |
106 | ## Only creator can delete Section
107 | def can_delete(self, user):
108 | return (user == self.creator)
109 |
110 | def pages_sorted(self):
111 | return self.pages.all().order_by('section_contents__page_position')
112 |
113 | def last_page_position(self):
114 | position = 0
115 | try:
116 | position = self.section_contents.order_by('page_position').last().page_position
117 | except:
118 | ## if error, pass and go with 0
119 | pass
120 | return position
121 |
122 | def append_page(self, page):
123 | position = self.last_page_position() or 0
124 | return self.pages.add( page, through_defaults={ 'page_position': position })
125 |
126 | def __str__(self):
127 | return "name='%s' position='%s'" %(self.name, self.section_position)
128 |
129 |
130 | class SectionContent(models.Model):
131 | class Meta:
132 | ordering = ['page_position']
133 |
134 | section = models.ForeignKey(
135 | PlaybookSection,
136 | on_delete=models.CASCADE,
137 | related_name='section_contents',
138 | related_query_name="section_contents"
139 | )
140 | playpage = models.ForeignKey(
141 | Playpage,
142 | on_delete=models.CASCADE,
143 | related_name='section_contents',
144 | related_query_name="section_contents"
145 | )
146 | page_position = models.PositiveIntegerField(default=0)
147 |
148 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/request.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from urllib.parse import urlparse
3 | import base64
4 |
5 | from .markdown import MarkdownParser
6 |
7 | class URLRequester():
8 | HEADERS = {
9 | 'User-Agent': 'Mozilla/5.0 (X11; Linux; rv:31.0) Gecko/20100101 Firefox'
10 | }
11 |
12 | ALLOWED_IMAGE_CONTENT_TYPES = [
13 | 'image/jpeg',
14 | 'image/png'
15 | ]
16 |
17 | def __init__(self, url):
18 | self.url = url
19 | self.verify = False ## Do not verify per default
20 | self.response = None
21 | self.status_code = None
22 |
23 | def request(self, url=None):
24 | response = self.request_url(url=url)
25 | self.status_code = response.status_code
26 | self.response = response.text
27 | return response
28 |
29 | def is_valid_response(self):
30 | if( self.get_status_code() == 200 ):
31 | return True
32 | else:
33 | return False
34 |
35 | def request_url(self, url=None):
36 | request_url = url or self.url
37 | response = requests.get(url=request_url, headers=self.HEADERS, verify=self.verify)
38 | return response
39 |
40 | def resolve_images(self):
41 | md_parser = MarkdownParser()
42 | content = md_parser.replaceImages(self.response, self.path_validation, self.path_processing)
43 | self.response = content
44 |
45 | def request_subpath(self, path):
46 | parsedUrl = urlparse(self.url)
47 | subPaths = [ subPath for subPath in urlparse(self.url).path.split('/') if subPath ]
48 | for subPath in subPaths:
49 | subPath += '/' if subPath.endswith('') else ''
50 | subPath += path
51 | rootPath = parsedUrl.netloc
52 | rootPath += '/' if rootPath.endswith('') else ''
53 | full_url = "%s://%s%s" %(parsedUrl.scheme, rootPath, subPath)
54 | response = self.request_url(url=full_url)
55 | if( response.status_code == 200 ):
56 | return response
57 |
58 | def path_validation(self, path):
59 | response = self.request_subpath(path)
60 | if( response.headers['Content-Type'] in self.ALLOWED_IMAGE_CONTENT_TYPES ):
61 | return True
62 | else:
63 | return False
64 |
65 |
66 | def path_processing(self, path):
67 | response = self.request_subpath(path)
68 | if( response.content ):
69 | return base64.b64encode(response.content).decode('ascii')
70 | else:
71 | return False
72 |
73 |
74 | def get_response(self):
75 | return self.response
76 |
77 | def get_status_code(self):
78 | return self.status_code
--------------------------------------------------------------------------------
/App/PlayBooksApp/source_resolver.py:
--------------------------------------------------------------------------------
1 |
2 | from .models import Playpage
3 | from PlayBooksWeb.models import IncludedFolder
4 |
5 | from .request import URLRequester
6 | from .markdown import MarkdownParser
7 |
8 | class SourceResolver():
9 |
10 | def __init__(self, page):
11 | if( page and isinstance(page, Playpage) ):
12 | self.playpage = page
13 | else:
14 | raise Exception("Invalid Page Given To Source Resolver")
15 |
16 | def resolve_type(self):
17 | source_type = self.playpage.source_type
18 | if( source_type == Playpage.SOURCE_HTTP ):
19 | return self.resolve_from_http(url=self.playpage.source)
20 | elif( source_type == Playpage.SOURCE_DISK ):
21 | return self.resolve_from_disk(self.playpage.included_folder.id, self.playpage.source)
22 |
23 | def resolve_from_http(self, url=None):
24 | if( url ):
25 | requester = URLRequester(url)
26 | requester.request()
27 | if( requester.is_valid_response() ):
28 | requester.resolve_images()
29 | response = requester.get_response()
30 | return response
31 | else:
32 | return False
33 | else:
34 | return False
35 |
36 | def resolve_from_disk(self, selected_included_folder, selected_included_folder_path):
37 | md_parser = MarkdownParser()
38 | content = IncludedFolder.get_file_content(selected_included_folder, selected_included_folder_path)
39 | content = md_parser.replaceImages(content, IncludedFolder.path_validation, IncludedFolder.path_processing, folderID=selected_included_folder, subpath=selected_included_folder_path)
40 | if( content ):
41 | return content
42 | else:
43 | return False
44 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/static/css/md.css:
--------------------------------------------------------------------------------
1 | .codehilite .hll { background-color: #ffffcc }
2 | .codehilite { background: #f8f8f8; }
3 | .codehilite .c { color: #408080; font-style: italic } /* Comment */
4 | .codehilite .err { border: 1px solid #FF0000 } /* Error */
5 | .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
6 | .codehilite .o { color: #666666 } /* Operator */
7 | .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
8 | .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
9 | .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
10 | .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
11 | .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
12 | .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
13 | .codehilite .gd { color: #A00000 } /* Generic.Deleted */
14 | .codehilite .ge { font-style: italic } /* Generic.Emph */
15 | .codehilite .gr { color: #FF0000 } /* Generic.Error */
16 | .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
17 | .codehilite .gi { color: #00A000 } /* Generic.Inserted */
18 | .codehilite .go { color: #888888 } /* Generic.Output */
19 | .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
20 | .codehilite .gs { font-weight: bold } /* Generic.Strong */
21 | .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
22 | .codehilite .gt { color: #0044DD } /* Generic.Traceback */
23 | .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
24 | .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
25 | .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
26 | .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
27 | .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
28 | .codehilite .kt { color: #B00040 } /* Keyword.Type */
29 | .codehilite .m { color: #666666 } /* Literal.Number */
30 | .codehilite .s { color: #BA2121 } /* Literal.String */
31 | .codehilite .na { color: #7D9029 } /* Name.Attribute */
32 | .codehilite .nb { color: #008000 } /* Name.Builtin */
33 | .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
34 | .codehilite .no { color: #880000 } /* Name.Constant */
35 | .codehilite .nd { color: #AA22FF } /* Name.Decorator */
36 | .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
37 | .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
38 | .codehilite .nf { color: #0000FF } /* Name.Function */
39 | .codehilite .nl { color: #A0A000 } /* Name.Label */
40 | .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
41 | .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
42 | .codehilite .nv { color: #19177C } /* Name.Variable */
43 | .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
44 | .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
45 | .codehilite .mb { color: #666666 } /* Literal.Number.Bin */
46 | .codehilite .mf { color: #666666 } /* Literal.Number.Float */
47 | .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
48 | .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
49 | .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
50 | .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
51 | .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
52 | .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
53 | .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
54 | .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
55 | .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
56 | .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
57 | .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
58 | .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
59 | .codehilite .sx { color: #008000 } /* Literal.String.Other */
60 | .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
61 | .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
62 | .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
63 | .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
64 | .codehilite .fm { color: #0000FF } /* Name.Function.Magic */
65 | .codehilite .vc { color: #19177C } /* Name.Variable.Class */
66 | .codehilite .vg { color: #19177C } /* Name.Variable.Global */
67 | .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
68 | .codehilite .vm { color: #19177C } /* Name.Variable.Magic */
69 | .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
70 |
--------------------------------------------------------------------------------
/App/PlayBooksApp/static/css/style.css:
--------------------------------------------------------------------------------
1 | html, body, .container-fluid {
2 | height: 100%;
3 | }
4 |
5 | body .overflow {
6 | overflow: auto;
7 | height: 100%;
8 | }
9 |
10 | body .overflow-overlay {
11 | overflow: overlay;
12 | }
13 |
14 | body .hidden {
15 | display: none;
16 | }
17 |
18 | body .full-width {
19 | width: 100%;
20 | }
21 |
22 | body .warning {
23 | color: #f50404;
24 | font-weight: bold;
25 | }
26 |
27 | body .contenteditable {
28 | width: 100%;
29 | background: #eae9e9;
30 | min-height: 80px;
31 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125);
32 | padding: 10px;
33 | }
34 |
35 | .btn.btn-add {
36 | color: #fff;
37 | background-color: #18692a;
38 | border-color: #041d04;
39 | }
40 | .btn.btn-add:hover {
41 | color: #fff;
42 | background-color: #114a1d;
43 | }
44 |
45 | .btn.btn-delete {
46 | color: #fff;
47 | background-color: #5f1010;
48 | border-color: #250609;
49 | }
50 | .btn.btn-delete:hover {
51 | color: #fff;
52 | background-color: #520c13;
53 | }
54 |
55 | body img {
56 | max-width: 100%;
57 | display: block;
58 | }
59 |
60 | /* width */
61 | ::-webkit-scrollbar {
62 | width: 0.5em;
63 | height: 0.8em;
64 | }
65 |
66 | /* Track */
67 | ::-webkit-scrollbar-track {
68 | /*box-shadow: inset 0 0 5px grey;*/
69 | border-radius: 10px;
70 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
71 | }
72 |
73 | /* Handle */
74 | ::-webkit-scrollbar-thumb {
75 | /*background: red; */
76 | border-radius: 10px;
77 | background-color: darkgrey;
78 | outline: 1px solid slategrey;
79 | }
80 |
81 | /* Handle on hover */
82 | ::-webkit-scrollbar-thumb:hover {
83 | background: #1c1d22;
84 | }
85 |
86 | body #PlayBooks {
87 | height: 100%;
88 | font-family: 'Avenir Next', Avenir, 'Helvetica Neue', 'Lato', 'Segoe UI', Helvetica, Arial, sans-serif;
89 | overflow: hidden;
90 | margin: 0;
91 | color: #cecece;
92 | background: #2a2b30;
93 | -webkit-font-smoothing: antialiased;
94 | }
95 |
96 | body #main-content {
97 | margin-left: 330px;
98 | margin-right: 50px;
99 | height: 100%;
100 | }
101 |
102 | body #breadcrumb-nav {
103 | padding: 1em 0em;
104 | }
105 |
106 | body #breadcrumb-nav .breadcrumb-container{
107 | display: inline-flex;
108 | width: 100%;
109 | background-color: #1c1d22;
110 | border-radius: 5px;
111 | }
112 |
113 | body #breadcrumb-nav .breadcrumb-container ol.breadcrumb {
114 | background-color: #1c1d22;
115 | margin: 0;
116 | }
117 | body #breadcrumb-nav .breadcrumb-container ol.breadcrumb .breadcrumb-item {
118 | padding-top: 10px;
119 | }
120 |
121 | body #breadcrumb-nav .breadcrumb-container .search-form-container {
122 | padding: 15px;
123 | }
124 |
125 | body #breadcrumb-nav .breadcrumb-container .search-form-container .search-form-dropdown {
126 | top: 0px !important;
127 | border-radius: 5px;
128 | left: 3px !important;
129 | min-width: 225px;
130 | }
131 |
132 | body #sidenav .nav-item {
133 | display: inline-block;
134 | width: 105%;
135 | padding: 5px 20px;
136 | background: #58595d;
137 | margin: 10px 0px;
138 | border-radius: 5px 0px 0px 10px;
139 | position: relative;
140 | word-break: break-word;
141 | }
142 |
143 | body #sidenav .nav-item:after {
144 | content: '';
145 | position: absolute;
146 | bottom: -10px;
147 | right: 4px;
148 | transform: rotate(45deg);
149 | border: 10px solid transparent;
150 | border-right-color: #58595d;
151 | }
152 |
153 | #PlayBooks .clickable:hover {
154 | cursor: pointer;
155 | }
156 |
157 |
158 |
159 | #main-container .pb-tile {
160 | display: block;
161 | width: 200px;
162 | height: 200px;
163 | margin: 10px;
164 | border-radius: 5px;
165 | background: #1c1d22;
166 | float: left;
167 | position: relative;
168 | }
169 |
170 | #main-container .tile-main {
171 | height: calc(200px - 25px);
172 | display: table-cell;
173 | vertical-align: middle;
174 | width: inherit;
175 | text-align: center;
176 | word-break: break-word;
177 | }
178 |
179 | #main-container .tile-main .input-group {
180 | display: inline-block;
181 | }
182 |
183 | #main-container .tile-main .input-group input {
184 | width: 100%;
185 | border-radius: 0px 0px 5px 5px;
186 | }
187 |
188 | #main-container .tile-main .input-group .help-text {
189 | text-align: left;
190 | }
191 |
192 | form .help-text {
193 | padding: 0px 10px;
194 | }
195 |
196 | #main-container .tile-footer {
197 | position: absolute;
198 | bottom: 0;
199 | width: 100%;
200 | padding: 10px;
201 | display: inline-block;
202 | height: 50px;
203 | }
204 |
205 | #main-container .tile-footer input {
206 | padding: 2px 10px;
207 | }
208 |
209 |
210 |
211 | .modal-container .modal {
212 | z-index: 99999;
213 | }
214 |
215 |
216 |
217 | body .main-container{
218 | height: 80%;
219 | overflow: auto;
220 | }
221 |
222 | body .tile {
223 | display: inline-block;
224 | width: 200px;
225 | height: 200px;
226 | margin: 10px;
227 | border-radius: 5px;
228 | background: #1c1d22;
229 | float: left;
230 | }
231 |
232 | body .codehilite {
233 | background: #f8f8f8;
234 | padding: 5px 5px 5px 5px;
235 | margin-bottom: 5px;
236 | border: 1px solid darkgrey;
237 | background-color: lightgray;
238 | }
239 |
240 | .codehilite pre {
241 | margin: 0;
242 | background-color: lightgray;
243 | }
244 |
245 | #pb-main pre{
246 | background-color: lightgray;
247 | }
248 |
249 | #edit-page-modal .modal-content {
250 | display: inline-flex;
251 | }
252 |
253 | /**
254 | -- Side Navigation
255 | --
256 | --
257 | **/
258 |
259 | body #side-content {
260 | position: fixed;
261 | top: 0;
262 | left: 0;
263 | width: 300px;
264 | background: #1c1d22;
265 | height: 100%;
266 | }
267 |
268 | body #side-content #sidenav {
269 | overflow-x: overlay;
270 | height: 80%; /* Height for scrolling */
271 | width: 110%; /* For scroll not to overlap with content*/
272 | }
273 |
274 | #side-content .nav-menu {
275 | width: 300px; /* re-enforce the width of the side content*/
276 | }
277 |
278 | body #side-content #header {
279 | min-height: 100px;
280 | }
281 |
282 | body #side-content #header #header-logo{
283 | padding: 1em 0 0 0;
284 | text-align: center;
285 | color: #3b3d4a;
286 | background: #1c1d22;
287 | }
288 |
289 | body #side-content #header #logo {
290 | width: 60%;
291 | margin: auto;
292 | margin-bottom: 10px;
293 | }
294 |
295 |
296 | body #header #user .username {
297 | color: #cecece;
298 | }
299 | body #header #user .logout {
300 | font-size: 85%;
301 | margin-left: 15px;
302 | }
303 |
304 | body #sidenav .nav-item:hover {
305 | background: #303131;
306 | }
307 |
308 | #sidenav .section-item {
309 | border-radius: 0px;
310 | display: inline-block;
311 | width: 100%;
312 | padding: 5px 20px;
313 | background: #58595d;
314 | margin: 10px 0px;
315 | position: relative;
316 | }
317 |
318 | #sidenav .btn {
319 | padding: 0px 10px;
320 | float: right;
321 | margin-left: 5px;
322 | }
323 |
324 | #sidenav .new-section {
325 | display: inline-block;
326 | width: 100%;
327 | border-bottom: 3px solid black;
328 | padding: 15px 0px;
329 | border-top: 3px solid black;
330 | }
331 |
332 | #sidenav .new-section .btn {
333 | margin-right: 10px;
334 | }
335 |
336 | .nav-icon.icon-draggable {
337 | margin-left: -10px;
338 | margin-right: 10px;
339 | }
340 |
341 | /** -- Form Fields -- **/
342 |
343 | /**
344 | -- Form Fields
345 | --
346 | --
347 | **/
348 |
349 | .edit-modal.modal-dialog {
350 | min-width: 50%;
351 | }
352 |
353 | .edit-modal .modal-content .page-add-module.add-existing {
354 | border-bottom: 3px solid #151618;
355 | }
356 |
357 | .modal-container .page-add-module {
358 | display: inline-flex;
359 | width: 100%;
360 | }
361 |
362 | .input-group-fieldset {
363 | width: 100%;
364 | display: inline-flex;
365 | }
366 |
367 | .error-message {
368 | color: #a90909;
369 | }
370 |
371 | .input-group-fieldset .errorlist li {
372 | display: block;
373 | }
374 |
375 | .btn-page-source {
376 | border: 1px solid; */
377 | background: #e9ecef;
378 | padding: 5px 25px;
379 | /* box-shadow: 0 2px 6px rgba(0,0,0,0.25), 0 2px 2px rgba(0,0,0,0.22); */
380 | border-radius: 3px;
381 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px;
382 | background: rgb(35, 49, 66);
383 | color: #ccc;
384 | border: 1px solid rgb(35, 49, 66);
385 | /* background: transparent; */
386 | border: 1px solid #ced4da;
387 | box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset;
388 | background: linear-gradient(rgb(230, 230, 230) 5%, rgb(233, 236, 239) 100%) rgb(237, 237, 237);
389 | border-radius: 3px;
390 | border: 1px solid rgb(220, 220, 220);
391 | /* display: inline-block; */
392 | cursor: pointer;
393 | color: rgb(119, 119, 119);
394 | /* font-family: Arial; */
395 | font-size: 15px;
396 | font-weight: 550;
397 | padding: 6px 24px;
398 | text-decoration: none;
399 | text-shadow: rgb(255, 255, 255) 0px 1px 0px;
400 | background: linear-gradient(rgb(233, 236, 239) 5%, rgb(233, 236, 239) 100%) rgb(237, 237, 237);
401 | display: block;
402 | }
403 |
404 | .btn-page-source.active {
405 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125);
406 | color: black;
407 | }
408 |
409 | .btn-page-source:hover {
410 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125);
411 | }
412 |
413 | /** -- Form Fields -- **/
414 |
415 | /**
416 | -- Page Content Edit
417 | --
418 | --
419 | **/
420 |
421 | #page-content-edit {
422 | width: calc(100% - 420px);
423 | }
424 |
425 | #page-content-edit .label {
426 | font-weight: bold;
427 | margin-right: 5px;
428 | }
429 |
430 | #page-content-edit .page-update-status {
431 | display: inline-block;
432 | }
433 |
434 | #page-content-edit .page-update-status .notifications {
435 | margin-right: 10px;
436 | font-weight: normal;
437 | }
438 |
439 | #page-content-edit .page-update-status .notifications .status-update {
440 | color: #ecec85;
441 | }
442 |
443 | #page-content-edit .page-update-status .notifications .status-notmodified {
444 | color: #0bca0b;
445 | }
446 |
447 | #page-content-edit .page-update-status .notifications .status-unable {
448 | color: #9e9e9e;
449 | }
450 |
451 | #page-content-edit .page-update-status .btn {
452 | line-height: 1.0;
453 | }
454 |
455 | #page-content-edit .page-edit-controls .btn-edit {
456 | background-color: #49525a;
457 | }
458 |
459 | #page-content-edit .page-edit-controls .btn-edit:hover {
460 | background-color: #72808c;
461 | }
462 |
463 | #page-content-edit .page-edit-controls, #page-content-edit .page-update-controls {
464 | display: inline-block;
465 | margin: 5px 0px;
466 | width: 100%;
467 | }
468 |
469 | #page-content-edit #pb-controls {
470 | width: 100%;
471 | display: inline-block;
472 | }
473 |
474 | #page-content-edit .page-edit-controls button, #page-content-edit .page-update-controls button {
475 | margin-right: 10px;
476 | }
477 |
478 | #page-content-edit .page-content.content-control-fields {
479 | display: inline-flex;
480 | }
481 |
482 | #page-content-edit .page-content.content-control-fields .input-group {
483 | width: fit-content;
484 | float: left;
485 | margin-right: 10px;
486 | }
487 |
488 | #page-content-edit .contenteditable {
489 | color: #212529;
490 | }
491 |
492 | .toggle.btn {
493 | height: 100% !important;
494 | }
495 | /** -- Page Content Edit -- **/
496 |
497 |
498 | /**
499 | -- Playbook Overview
500 | --
501 | --
502 | **/
503 |
504 | .playbook-overview .attribute {
505 | margin-right: 10px;
506 | }
507 |
508 | .playbook-overview li {
509 | display: inline-block;
510 | }
511 |
512 | /** -- Playbook Overview -- **/
513 |
514 |
515 | /**
516 | -- Prefetch Container
517 | --
518 | --
519 | **/
520 |
521 | body #prefetch-container {
522 | position: absolute;
523 | height: 90%;
524 | width: 410px;
525 | top: 30px;
526 | right: 0px;
527 | overflow: auto;
528 | font-size: 70%;
529 | border: 2px solid #cecece;
530 | padding: 15px 15px;
531 | background-color: #131111;
532 | border-right: none;
533 | z-index: 9999;
534 | }
535 |
536 | #prefetch-container .close-container {
537 | color: white;
538 | }
539 |
540 | #prefetch-container code {
541 | color: #cecece;
542 | }
543 | #prefetch-container .codehilite {
544 | background-color: #a29f9f;
545 | }
546 | #prefetch-container .codehilite pre {
547 | background-color: #a29f9f;
548 | }
549 | #prefetch-container h1{ font-size: 2em; }
550 | #prefetch-container h2{ font-size: 1.5em; }
551 | #prefetch-container h3{ font-size: 1em; }
552 | #prefetch-container h4{ font-size: 0.8em; }
553 |
554 | /** -- Prefetch Container -- **/
555 |
556 | /**
557 | -- Bootstrap Additions
558 | --
559 | --
560 | **/
561 |
562 | .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open>.dropdown-toggle.btn-default {
563 | color: #333;
564 | background-color: #e6e6e6;
565 | border-color: #adadad;
566 | box-shadow: inset 0 3px 5px rgba(0,0,0,.125)
567 | }
568 | .toggle-handle.btn-default {
569 | color: #333;
570 | background-color: #e9ecef;
571 | border-color: #ccc;
572 | border: 0.5px solid lightgray;
573 | }
574 | /** -- Bootstrap Additions -- **/
--------------------------------------------------------------------------------
/App/PlayBooksApp/templates/_edit_page.html:
--------------------------------------------------------------------------------
1 |