diff --git a/.gitignore b/.gitignore index 29ca522..d4ba5a3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,9 @@ test-results/ Rainy/obj/ Rainy-tests/obj/ *.zip - +docs/reference +# nuget packages +packages/ # release folders rainy-?.?? diff --git a/.gitmodules b/.gitmodules index 16780b9..f1def98 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,7 @@ [submodule "JsonConfig"] path = JsonConfig url = https://github.com/Dynalon/JsonConfig.git +[submodule "tomboy-library-websync"] + path = tomboy-library-websync + url = https://github.com/Dynalon/tomboy-library-websync.git + diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..9e8f588 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,1781 @@ +# Doxyfile 1.7.6.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or sequence of words) that should +# identify the project. Note that if you do not use Doxywizard you need +# to put quotes around the project name if it contains spaces. + +PROJECT_NAME = "Rainy" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer +# a quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is +# included in the documentation. The maximum height of the logo should not +# exceed 55 pixels and the maximum width should not exceed 200 pixels. +# Doxygen will copy the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = docs/reference/ + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, +# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, +# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = YES + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful if your file system +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 8 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding +# "class=itcl::class" will allow you to use the command class in the +# itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given extension. +# Doxygen has a built-in mapping, but you can override or extend it using this +# tag. The format is ext=language, where ext is a file extension, and language +# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C, +# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make +# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C +# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions +# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate getter +# and setter methods for a property. Setting this option to YES (the default) +# will make doxygen replace the get and set methods by a property in the +# documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields will be shown inline in the documentation +# of the scope in which they are defined (i.e. file, namespace, or group +# documentation), provided this scope is documented. If set to NO (the default), +# structs, classes, and unions are shown on a separate page (for HTML and Man +# pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to +# determine which symbols to keep in memory and which to flush to disk. +# When the cache is full, less often used symbols will be written to disk. +# For small to medium size projects (<1000 input files) the default value is +# probably good enough. For larger projects a too small cache size can cause +# doxygen to be busy swapping symbols to and from disk most of the time +# causing a significant performance penalty. +# If the system has enough physical memory increasing the cache will improve the +# performance by keeping more symbols in memory. Note that the value works on +# a logarithmic scale so increasing the size by one will roughly double the +# memory usage. The cache size is given by this formula: +# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +SYMBOL_CACHE_SIZE = 0 + +# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be +# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given +# their name and scope. Since this can be an expensive process and often the +# same symbol appear multiple times in the code, doxygen keeps a cache of +# pre-resolved symbols. If the cache is too small doxygen will become slower. +# If the cache is too large, memory is wasted. The cache size is given by this +# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# If the sources in your project are distributed over multiple directories +# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy +# in the documentation. The default is NO. + +SHOW_DIRECTORIES = NO + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. The create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this +# feature you need bibtex and perl available in the search path. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_NO_PARAMDOC option can be enabled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = Rainy/ + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +FILE_PATTERNS = *.cs + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext= (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C and C++ comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = . + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet. Note that doxygen will try to copy +# the style sheet file to the HTML output directory, so don't put your own +# style sheet in the HTML output directory as well, or it will be erased! + +HTML_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = YES + +# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, +# files or namespaces will be aligned in HTML using tables. If set to +# NO a bullet list will be used. + +HTML_ALIGN_MEMBERS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. For this to work a browser that supports +# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox +# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). + +HTML_DYNAMIC_SECTIONS = NO + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# +# Qt Help Project / Custom Filters. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# +# Qt Help Project / Filter Attributes. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 4 + +# By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories, +# and Class Hierarchy pages using a tree view instead of an ordered list. + +USE_INLINE_TREES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the +# mathjax.org site, so you can quickly see the result without installing +# MathJax, but it is strongly recommended to install a local copy of MathJax +# before deployment. + +MATHJAX_RELPATH = http://www.mathjax.org/mathjax + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a PHP enabled web server instead of at the web client +# using Javascript. Doxygen will generate the search PHP script and index +# file to put on the web server. The advantage of the server +# based approach is that it scales better to large projects and allows +# full text search. The disadvantages are that it is more difficult to setup +# and does not have live searching capabilities. + +SERVER_BASED_SEARCH = NO + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. +# Optionally an initial location of the external documentation +# can be added for each tagfile. The format of a tag file without +# this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths or +# URLs. If a location is present for each tag, the installdox tool +# does not have to be run to correct the links. +# Note that each tag file must have a unique name +# (where the name does NOT include the path) +# If a tag file is not located in the directory in which doxygen +# is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = YES + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = YES + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 150 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = YES + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/JsonConfig b/JsonConfig index 7f25820..1e24623 160000 --- a/JsonConfig +++ b/JsonConfig @@ -1 +1 @@ -Subproject commit 7f258206a226ea2a7e9c5dfd448e39a00543de7e +Subproject commit 1e24623789b05cac5242b5b75855cb6655cf84f8 diff --git a/Makefile b/Makefile index 9fdfc17..364d05c 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ -RELEASEVER=0.2.3 +RELEASEVER=0.5.0 ZIPDIR=rainy-$(RELEASEVER) BINDIR=$(shell pwd)/Rainy/bin/Debug RELEASEDIR=$(shell pwd)/release -TMPDIR=$(shell pwd)/.tmp MONO=$(shell which mono) XBUILD=$(shell which xbuild) @@ -12,7 +11,10 @@ MKBUNDLE=$(shell which mkbundle) UNPACKED_EXE=$(BINDIR)/Rainy.exe PACKED_EXE=Rainy.exe -MIN_MONO_VERSION=2.10.9 + +# Note this is the min version for building from source; running might work +# on older mono versions +MIN_MONO_VERSION=3.0.0 pack: build @cp Rainy/settings.conf $(RELEASEDIR)/settings.conf @@ -34,17 +36,29 @@ pack: build @echo "" @echo "" -build: -## this is not working? -##pkg-config --atleast-version=$(MIN_MONO_VERSION) mono; if [ $$? != "0" ]; then $(error "mono >=2.10.9 is required"); - +checkout: +ifndef TEAMCITY # Fetching Rainy's submodules - @git submodule init - @git submodule update + @git submodule update --init --recursive +endif + +deps: + # if the next steps fails telling about security authentication, make sure + # you have imported trusted ssl CA certs with this command and re-run: + # + # mozroots --import --sync + # + + @mono tools/NuGet.exe install -o packages Rainy/packages.config + @mono tools/NuGet.exe install -o packages Rainy-tests/packages.config + @mono tools/NuGet.exe install -o packages tomboy-library-websync/packages.config + @echo "Successfully fetched dependencies." + +build: checkout deps + +## this is not working? +##pkg-config --atleast-version=$(MIN_MONO_VERSION) mono; if [ $$? != "0" ]; then $(error "mono >=$MIN_MONO_VERSION is required"); - # Fetching tomboy-library's submodules - @cd tomboy-library/ && git submodule init && git submodule update && cd .. - $(XBUILD) $(XBUILD_ARGS) Rainy.sln release: clean pack @@ -62,7 +76,6 @@ clean: rm -rf Rainy/obj/* rm -rf $(ZIPDIR) rm -rf $(ZIPDIR).zip - rm -rf $(TMPDIR) rm -rf $(BINDIR)/* rm -rf $(RELEASEDIR)/*.exe rm -rf $(RELEASEDIR)/*.mdb diff --git a/Rainy-tests/Admin/TestUserManagement.cs b/Rainy-tests/Admin/TestUserManagement.cs new file mode 100644 index 0000000..8a9e320 --- /dev/null +++ b/Rainy-tests/Admin/TestUserManagement.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Rainy.UserManagement; +using Rainy.WebService.Management; +using ServiceStack.ServiceClient.Web; +using Rainy.Tests.Db; + +namespace Rainy.Tests.RestApi.Management +{ + [TestFixture()] + public class TestUserManagement : DbTestsBase + { + protected JsonServiceClient adminClient; + protected DTOUser[] GetSampleUser () + { + var user = new List (); + + user.Add (new DTOUser () { + Username = "johndoe", + Password = "none", + EmailAddress = "john@doe.com", + FirstName = "John", + LastName = "Doe", + AdditionalData = "" + }); + user.Add (new DTOUser () { + Username = "janedoe", + Password = "none", + EmailAddress = "jane@doe.com", + FirstName = "Jane", + LastName = "Doe", + AdditionalData = "Jane, John's wife" + }); + + return user.ToArray (); + } + + [SetUp] + public new void SetUp () + { + adminClient = GetAdminServiceClient (); + + // add some sample users to the server + var client = GetAdminServiceClient (); + var url = new UserRequest ().ToUrl("POST"); + foreach (DTOUser user in GetSampleUser ()) { + client.Post (url, user); + } + } + + [Test] + [ExpectedException(typeof(WebServiceException),ExpectedMessage="Unauthorized.")] + public void UnauthorizedAccessFails () + { + var alluser_url = new AllUserRequest ().ToUrl("GET"); + var client = GetServiceClient (); + try { + client.Get (alluser_url); + } catch (WebServiceException e) { + Assert.AreEqual (401, e.StatusCode); + throw e; + } + } + + [Test] + public void AddNewUser () + { + var user = new DTOUser (); + user.Username = "michael"; + user.EmailAddress = "michael@knight.com"; + user.Password = "none"; + user.AdditionalData = "Some more info about Michael"; + + var user_url = new UserRequest ().ToUrl("POST"); + adminClient.Post (user_url, user); + + var user_get_url = new UserRequest () { Username = "michael" }.ToUrl("GET"); + var resp = adminClient.Get (user_get_url); + + Assert.AreEqual (1, resp.Length); + Assert.AreEqual (user.Username, resp[0].Username); + Assert.AreEqual (user.EmailAddress, resp[0].EmailAddress); + Assert.AreEqual (user.AdditionalData, resp[0].AdditionalData); + + } + + [Test] + [ExpectedException(typeof(WebServiceException))] + public void AddNewUserWithEmptyPasswordFails () + { + var user = new DTOUser (); + user.Username = "michael"; + user.EmailAddress = "michael@knight.com"; + user.Password = ""; + user.AdditionalData = "Some more info about Michael"; + + var user_url = new UserRequest ().ToUrl("POST"); + try { + adminClient.Post (user_url, user); + } catch (WebServiceException e) { + Assert.AreEqual (400, e.StatusCode); + throw e; + } + } + + [Test] + public void ChangeUserPassword () + { + var user = new DTOUser (); + user.Username = "michael"; + user.EmailAddress = "michael@knight.com"; + user.Password = "thisissecret"; + user.AdditionalData = "Some more info about Michael"; + + var user_url = new UserRequest ().ToUrl("POST"); + adminClient.Post (user_url, user); + + user.Password = "thisismynewpassword"; + var update_url = new UserRequest ().ToUrl ("PUT"); + adminClient.Put (update_url, user); + + // authorization with the old password fails for the user + Assert.Fail ("TODO: Password changing not possible with encryption"); + + // TODO: authorization with the new password works + } + + [Test] + public void DeleteUser () + { + var user_delete_url = new UserRequest () { Username = "johndoe" }.ToUrl ("DELETE"); + adminClient.Delete (user_delete_url); + + // make sure johndoe is not in the list of our users + var alluser_url = new AllUserRequest ().ToUrl ("GET"); + var allusers = adminClient.Get (alluser_url); + + var list_of_johndoes = allusers.Where(u => u.Username == "johndoe").ToArray (); + Assert.AreEqual (0, list_of_johndoes.Count ()); + } + + [Test] + public void DeleteUserDeletesAllData () + { + Assert.Fail ("TODO: implement me"); + } + + [Test] + public void UpdateUser () + { + var user = new DTOUser () { + Username = "johndoe", + Password = "abc123", + EmailAddress = "some@foo.com", + AdditionalData = "some text", + FirstName = "Jane", + LastName = "Doeson" + }; + + var user_url = new UserRequest ().ToUrl ("PUT"); + adminClient.Put (user_url, user); + + var all_users_url = new AllUserRequest ().ToUrl ("GET"); + var all_users = adminClient.Get (all_users_url); + + var johndoe = all_users.First (u => u.Username == "johndoe"); + Assert.AreEqual (user.Username, johndoe.Username); + //password is not returned + Assert.AreEqual (string.Empty, johndoe.Password); + Assert.AreEqual (user.EmailAddress, johndoe.EmailAddress); + Assert.AreEqual (user.AdditionalData, johndoe.AdditionalData); + Assert.AreEqual (user.FirstName, johndoe.FirstName); + Assert.AreEqual (user.LastName, johndoe.LastName); + + } + + + [Test] + [ExpectedException (typeof(WebServiceException))] + public void UpdateUserForUnknownUsername () + { + var user = new DTOUser () { + Username = "foobar" + }; + var user_url = new UserRequest ().ToUrl ("PUT"); + adminClient.Put (user_url, user); + } + } +} + diff --git a/Rainy-tests/Admin/TestUserSignup.cs b/Rainy-tests/Admin/TestUserSignup.cs new file mode 100644 index 0000000..e1ba430 --- /dev/null +++ b/Rainy-tests/Admin/TestUserSignup.cs @@ -0,0 +1,173 @@ +using System; +using System.Linq; +using System.Net; +using NUnit.Framework; +using Rainy.Db; +using Rainy.UserManagement; +using Rainy.WebService.Management; +using ServiceStack.OrmLite; +using ServiceStack.ServiceClient.Web; +using Rainy.Tests.Db; + +namespace Rainy.Tests.RestApi.Management +{ + [TestFixture()] + public class TestUserSignup : DbTestsBase + { + protected JsonServiceClient client; + protected JsonServiceClient adminClient; + + protected DTOUser getTestUser () + { + var user = new DTOUser () { + Username = "someuser", + FirstName = "John", + LastName = "Doe", + EmailAddress = "some@foo.com", + Password = "Foobar.123" + }; + return user; + } + + [SetUp] + public new void SetUp () + { + client = GetServiceClient (); + adminClient = GetAdminServiceClient (); + } + + [Test()] + public void SignupWorks() + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + } + + [Test] + public void SignupWithVerifyWorks () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + + // lookup activation key + var secret = ""; + using (var db = connFactory.OpenDbConnection ()) { + var db_user = db.First (u => u.Username == user.Username); + secret = db_user.VerifySecret; + client.Get ("/api/user/signup/verify/" + secret + "/"); + } + using (var db = connFactory.OpenDbConnection ()) { + var db_user = db.First (u => u.Username == user.Username); + Assert.IsTrue (db_user.IsVerified); + Assert.IsEmpty (db_user.VerifySecret); + } + } + [Test] + [ExpectedException(typeof(WebException))] + public void UnverifiedUserCannotAcquireAccessToken () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + testServer.GetAccessToken (); + } + + [Test] + [ExpectedException(typeof(WebException))] + public void UnactivatedUserCannotAcquireAccessToken () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + + // lookup activation key + var secret = ""; + using (var db = connFactory.OpenDbConnection ()) { + var db_user = db.First (u => u.Username == user.Username); + secret = db_user.VerifySecret; + } + client.Get ("/api/user/signup/verify/" + secret + "/"); + testServer.GetAccessToken (); + } + + + [Test] + [ExpectedException(typeof(WebServiceException))] + public void SignupUserWithUnsafePasswordFails () + { + var user = new DTOUser () { + Username = "testuser", + Password = "abc123", + FirstName = "John", + LastName = "Doe", + EmailAddress = "johndoe@foo.com" + }; + client.Post ("/api/user/signup/new/", user); + } + + [Test] + [ExpectedException(typeof(WebServiceException))] + public void SignupUsernameTwice () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + client.Post ("/api/user/signup/new/", user); + } + [Test] + [ExpectedException(typeof(WebServiceException))] + public void SignupUsernameTwiceWithDifferentCasing () + { + var user = getTestUser (); + user.Username = "someuser"; + client.Post ("/api/user/signup/new/", user); + user.Username = "SomEUseR"; + client.Post ("/api/user/signup/new/", user); + } + + [Test] + [ExpectedException(typeof(WebServiceException))] + public void SignupEmailTwice () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + user.Username = "otheruser"; + client.Post ("/api/user/signup/new/", user); + } + + [Test] + public void PendingUserForActivation () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + user.Username = "otheruser"; + user.EmailAddress = "other@foo.com"; + client.Post ("/api/user/signup/new/", user); + + var pending = adminClient.Get ("/api/user/signup/pending/"); +// Assert.AreEqual(2, pending.Length); + Assert.That (pending.ToList().Where(u => u.Username == "someuser").Count () == 1); + Assert.That (pending.ToList().Where(u => u.Username == "otheruser").Count () == 1); + } + + [Test] + public void PendingUserSuccessfullyActivated () + { + var user = getTestUser (); + client.Post ("/api/user/signup/new/", user); + + // lookup activation key + var secret = ""; + using (var db = connFactory.OpenDbConnection ()) { + var db_user = db.First (u => u.Username == user.Username); + secret = db_user.VerifySecret; + } + client.Get ("/api/user/signup/verify/" + secret + "/"); + + adminClient.Post ("/api/user/signup/activate/" + user.Username + "/", new object()); + + using (var db = connFactory.OpenDbConnection ()) { + var db_user = db.First (u => u.Username == user.Username); + Assert.IsTrue (db_user.IsActivated); + } + } + } +} + diff --git a/Rainy-tests/CryptoTests.cs b/Rainy-tests/CryptoTests.cs new file mode 100644 index 0000000..05b07fb --- /dev/null +++ b/Rainy-tests/CryptoTests.cs @@ -0,0 +1,140 @@ +using System; +using NUnit.Framework; +using Rainy.Db; +using System.Linq; +using Rainy.Crypto; + +namespace Rainy.Tests +{ + [TestFixture] + public class CryptoTests + { + private bool isOnlyLowercaseHexChars (string text) + { + char[] hex_chars = new char[] {'a','b','c','d','e','f'}; + var arr = text.ToCharArray (); + return arr.All (c => char.IsNumber (c) || hex_chars.Contains (c)); + } + + [Test] + public void TestKeyToStringGeneration () + { + var rng = new System.Security.Cryptography.RNGCryptoServiceProvider (); + + var key = rng.Create256BitLowerCaseHexKey (); + + Assert.AreEqual (64, key.Length); + Assert.That (isOnlyLowercaseHexChars (key)); + } + [Test] + public void TestKeyStringToBytes () + { + var key = "fdc6e6227bd83d807c0cf6a5ce6df303b4d580b672611a0a7676fd95d7525ec1"; + var key_bytes = new byte[] { 253, 198, 230, 34, 123, 216, 61, 128, 124, 12, 246, 165, 206, 109, 243, 3, 180, 213, 128, 182, 114, 97, 26, 10, 118, 118, 253, 149, 215, 82, 94, 193 }; + + var bytes = key.ToByteArray (); + Assert.AreEqual (key_bytes, bytes); + } + + [Test] + public void TestEncryptKeyWithKey () + { + // 256 bit keys + var random_key = "fdc6e6227bd83d807c0cf6a5ce6df303b4d580b672611a0a7676fd95d7525ec1"; + var master_key = "1ce5257d59df6767a0a116276b085d4b303fd6ec5a6fc0c708d38db7226e6cdf"; + + // 128 bit iv + var iv = "f6a5ce6df303b4d5affedeadbeef0ffe"; + + var token_key = master_key.EncryptWithKey (random_key, iv); + var plaintext_key = token_key.DecryptWithKey (random_key, iv); + + Assert.AreEqual (master_key, plaintext_key); + } + + [Test] + public void CreateCryptoFieldsForNewUser () + { + var u = new DBUser (); + u.Username = "johndoe"; + u.CreateCryptoFields ("foobar"); + + Assert.GreaterOrEqual (u.EncryptedMasterKey.Length, 32); + Assert.That (u.EncryptedMasterKey.Length % 2 == 0); + } + + [Test] + public void GetPlaintextMasterKeyReturns256Bit () + { + var u = new DBUser (); + u.Username = "johndoe"; + var password = "foobar123"; + u.CreateCryptoFields (password); + var master_key = u.GetPlaintextMasterKey (password); + Assert.AreEqual(32, master_key.Length); + } + [Test] + public void GetPlaintextMasterKeyReturnsSameKeyForSamePassword () + { + var u = new DBUser (); + u.Username = "johndoe"; + var password = "foobar123"; + u.CreateCryptoFields (password); + var key1 = u.GetPlaintextMasterKey (password); + var key2 = u.GetPlaintextMasterKey (password); + + Assert.AreEqual (key1, key2); + Assert.AreEqual (key1.ToHexString (), key2.ToHexString ()); + } + + [Test] + public void PasswordHashHasCorrectLength () + { + var u = new DBUser (); + u.Username = "johndoe"; + var password = "foobar123"; + u.CreateCryptoFields (password); + + Assert.AreEqual (64, u.PasswordHash.Length); + } + + [Test] + public void BasicEncryptAndDecrypt () + { + var u = new DBUser (); + u.Username = "johndoe"; + var password = "Asdf1234öäü%&"; + + u.CreateCryptoFields (password); + var test_string = "The quick brown fox jumps over the lazy dog."; + + var master_key = u.GetPlaintextMasterKey (password); + + byte[] encrypted_bytes = u.EncryptString (master_key, test_string); + string decrypted_string = u.DecryptUnicodeString (master_key, encrypted_bytes); + + Assert.AreEqual (test_string, decrypted_string); + } + + [Test] + public void EncryptDecryptWithHexRepresentation () + { + var u = new DBUser (); + u.Username = "johndoe"; + var password = "Asdf1234öäü%&"; + + u.CreateCryptoFields (password); + var master_key = u.GetPlaintextMasterKey (password); + var key = master_key.ToHexString (); + var test_string = "The quick brown fox jumps over the lazy dog."; + + byte[] encrypted_bytes = u.EncryptString (master_key, test_string); + string encrypted_string = encrypted_bytes.ToHexString (); + string decrypted_string = u.DecryptUnicodeString (master_key, encrypted_string.ToByteArray ()); + + Assert.AreEqual (test_string, decrypted_string); + + } + } +} + diff --git a/Rainy-tests/DtoConversionTests.cs b/Rainy-tests/DtoConversionTests.cs new file mode 100644 index 0000000..56a0cb5 --- /dev/null +++ b/Rainy-tests/DtoConversionTests.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Tomboy; +using Tomboy.Sync.Web.DTO; +using Tomboy.Tags; + +namespace Rainy.Tests +{ + [TestFixture] + public class DtoConversionTests + { + + [SetUp] + public void SetUp () + { + } + [TearDown] + public void TearDown () + { + } + + [Test] + public void ConvertUriTests () + { + var tomboy_note = new Note (); + + tomboy_note.CreateDate = DateTime.Now; + tomboy_note.ChangeDate = DateTime.Now; + tomboy_note.MetadataChangeDate = DateTime.Now; + + var dto_note = tomboy_note.ToDTONote (); + + Assert.That (!string.IsNullOrEmpty (dto_note.Guid)); + + Assert.AreEqual (tomboy_note.Guid, dto_note.Guid); + Assert.That (tomboy_note.Uri.Contains (dto_note.Guid)); + Assert.That (tomboy_note.Uri.Contains (tomboy_note.Guid)); + + var tomboy_note_2 = dto_note.ToTomboyNote (); + Assert.AreEqual (tomboy_note.Guid, tomboy_note_2.Guid); + Assert.AreEqual (tomboy_note.Uri, tomboy_note_2.Uri); + } + + [Test] + public void ConvertFromTomboyNoteToDTO() + { + var tomboy_note = new Note (); + tomboy_note.Title = "This is a sample note"; + tomboy_note.Text = "This is some sample text"; + + tomboy_note.ChangeDate = DateTime.Now; + tomboy_note.CreateDate = DateTime.Now; + tomboy_note.MetadataChangeDate = DateTime.Now; + + var dto_note = tomboy_note.ToDTONote (); + + Assert.AreEqual (tomboy_note.Title, dto_note.Title); + Assert.AreEqual (tomboy_note.Text, dto_note.Text); + + Assert.AreEqual (tomboy_note.ChangeDate, DateTime.Parse (dto_note.ChangeDate).ToUniversalTime ()); + Assert.AreEqual (tomboy_note.CreateDate, DateTime.Parse (dto_note.CreateDate).ToUniversalTime ()); + Assert.AreEqual (tomboy_note.MetadataChangeDate, DateTime.Parse (dto_note.MetadataChangeDate).ToUniversalTime ()); + + Assert.AreEqual (tomboy_note.Guid, dto_note.Guid); + + var tag_intersection = dto_note.Tags.Intersect (tomboy_note.Tags.Keys); + Assert.AreEqual (dto_note.Tags.Count (), tag_intersection.Count ()); + } + [Test] + public void ConvertFromDTONoteToTomboyNote() + { + var dto_note = new DTONote (); + dto_note.Title = "This is a sample note"; + dto_note.Text = "This is some sample text"; + + dto_note.ChangeDate = DateTime.Now.ToString (Tomboy.Writer.DATE_TIME_FORMAT); + dto_note.MetadataChangeDate = DateTime.Now.ToString (Tomboy.Writer.DATE_TIME_FORMAT); + dto_note.CreateDate = DateTime.Now.ToString (Tomboy.Writer.DATE_TIME_FORMAT); + + var tomboy_note = dto_note.ToTomboyNote (); + + Assert.AreEqual (tomboy_note.Title, dto_note.Title); + Assert.AreEqual (tomboy_note.Text, dto_note.Text); + + Assert.AreEqual (tomboy_note.ChangeDate, DateTime.Parse (dto_note.ChangeDate).ToUniversalTime ()); + Assert.AreEqual (tomboy_note.CreateDate, DateTime.Parse (dto_note.CreateDate).ToUniversalTime ()); + Assert.AreEqual (tomboy_note.MetadataChangeDate, DateTime.Parse (dto_note.MetadataChangeDate).ToUniversalTime ()); + + var tag_intersection = dto_note.Tags.Intersect (tomboy_note.Tags.Keys); + Assert.AreEqual (dto_note.Tags.Count (), tag_intersection.Count ()); + } + + [Test] + public void ConvertFromDTOWithTags () + { + var dto_note = new DTONote (); + dto_note.Tags = new string[] { "school", "shopping", "fun" }; + + var tomboy_note = dto_note.ToTomboyNote (); + + foreach (string tag in dto_note.Tags) { + Assert.Contains (tag, tomboy_note.Tags.Keys); + } + } + [Test] + public void ConvertToDTOWithTags () + { + var tomboy_note = new Note (); + tomboy_note.Tags.Add ("school", new Tag ("school")); + tomboy_note.Tags.Add ("shopping", new Tag ("shopping")); + tomboy_note.Tags.Add ("fun", new Tag ("fun")); + + var dto_note = tomboy_note.ToDTONote (); + + foreach (string tag in tomboy_note.Tags.Keys) { + Assert.Contains (tag, dto_note.Tags); + } + } + + [Test] + public void ConvertBackAndForth () + { + var tn1 = new Note () { + Title = "This is my Title with Umlauts: äöü", + Text = "This is my note body text.", + CreateDate = DateTime.Now - new TimeSpan (365, 0, 0, 0), + MetadataChangeDate = DateTime.Now, + ChangeDate = DateTime.Now - new TimeSpan (14, 0, 0, 0) + + // TODO check why OpenOnStartup is of type string in Tomboy + //OpenOnStartup = "true" + }; + + var dto_note = tn1.ToDTONote (); + var tn2 = dto_note.ToTomboyNote (); + + // notes should be identical + Assert.AreEqual (tn1.Guid, tn2.Guid); + Assert.AreEqual (tn1.Uri, tn2.Uri); + Assert.AreEqual (tn1.Title, tn2.Title); + Assert.AreEqual (tn1.Text, tn2.Text); + + Assert.AreEqual (tn1.ChangeDate, tn2.ChangeDate); + Assert.AreEqual (tn1.MetadataChangeDate, tn2.MetadataChangeDate); + Assert.AreEqual (tn1.CreateDate, tn2.CreateDate); + + Assert.AreEqual (tn1.OpenOnStartup, tn2.OpenOnStartup); + + Assert.AreEqual (tn1.Tags.Keys, tn2.Tags.Keys); + + } + } + +} diff --git a/Rainy-tests/NoteConversionTests.cs b/Rainy-tests/NoteConversionTests.cs new file mode 100644 index 0000000..f5e8e82 --- /dev/null +++ b/Rainy-tests/NoteConversionTests.cs @@ -0,0 +1,157 @@ +using System; +using NUnit.Framework; +using Rainy.NoteConversion; + +namespace Rainy.Tests.XmlNoteConversion +{ + [TestFixture] + public class NoteToHtmlConversionTests + { + [Test] + public void BoldNextToBold() + { + string note_body = "Bold1Bold2"; + var result = note_body.ToHtml (); + Assert.AreEqual ("Bold1Bold2", result); + } + [Test] + public void BoldNextToItalic() + { + string note_body = "Italic.This is bold."; + var result = note_body.ToHtml (); + Assert.AreEqual ("Italic.This is bold.", result); + + } + [Test] + public void BoldAndItalic() + { + string note_body = "Bold and Italic."; + var result = note_body.ToHtml (); + Assert.AreEqual ("Bold and Italic.", result); + + } + [Test] + public void MultilineItalic () + { + string note_body = "Multiline\nItalic"; + var result = note_body.ToHtml (); + Assert.AreEqual ("Multiline
Italic
", result); + } + + [Test] + public void UnorderedLists () + { + string note_body = "FooBar"; + var result = note_body.ToHtml (); + Assert.AreEqual ("
  • Foo
  • Bar
", result); + } + + [Test] + public void SizeTags () + { + string note_body = "Foobar"; + var result = note_body.ToHtml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + [Test] + public void HighlightTag () + { + string note_body = "Foobar"; + var result = note_body.ToHtml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + [Test] + public void InternalLink () + { + string note_body = "Foobar"; + var result = note_body.ToHtml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + [Test] + public void UrlLink () + { + string note_body = "http://www.example.com/index.php?foo=bar"; + var result = note_body.ToHtml (); + string expected = "http://www.example.com/index.php?foo=bar"; + Assert.AreEqual (expected, result); + } + } + + [TestFixture] + public class HtmlToTomboyNote + { + [Test] + public void BoldNextToBold() + { + string note_body = "Bold1Bold2"; + var result = note_body.ToTomboyXml (); + Assert.AreEqual ("Bold1Bold2", result); + } + [Test] + public void UnorderedLists () + { + string html_body = "
  • Foo
  • Bar
"; + var result = html_body.ToTomboyXml (); + string expected = "FooBar"; + Assert.AreEqual (expected, result); + } + + [Test] + public void LineBreaks() + { + string html_body = "Bold1Bold2
"; + var result = html_body.ToTomboyXml (); + string expected = "Bold1Bold2\n"; + Assert.AreEqual (expected, result); + } + + [Test] + public void SizeTagsSmall () + { + string html_body = "Foobar"; + var result = html_body.ToTomboyXml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + + [Test] + public void HighlightTags () + { + string html_body = "Foobar"; + var result = html_body.ToTomboyXml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + + [Test] + public void InternalLink () + { + string html_body = "Foobar"; + var result = html_body.ToTomboyXml (); + string expected = "Foobar"; + Assert.AreEqual (expected, result); + } + + [Test] + public void UrlLink () + { + string html_body = "http://www.example.com/index.php?foo=bar"; + var result = html_body.ToTomboyXml (); + string expected = "http://www.example.com/index.php?foo=bar"; + Assert.AreEqual (expected, result); + } + + [Test] + public void DivsAreRemoved () + { + string html_body = "
Blafoobar
"; + var result = html_body.ToTomboyXml (); + string expected = "Blafoobar"; + Assert.AreEqual (expected, result); + } + } +} + diff --git a/Rainy-tests/NoteHistoryTests.cs b/Rainy-tests/NoteHistoryTests.cs new file mode 100644 index 0000000..e992a59 --- /dev/null +++ b/Rainy-tests/NoteHistoryTests.cs @@ -0,0 +1,253 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Rainy.Db; +using Rainy.WebService; +using ServiceStack.ServiceClient.Web; +using Tomboy.Sync; +using Tomboy.Sync.Web; +using Tomboy.Sync.Web.DTO; + +namespace Rainy.Tests.RestApi +{ + public abstract class NoteHistoryTestsBase : Tomboy.Sync.AbstractSyncManagerTestsBase + { + protected RainyTestServer testServer; + protected DBUser testUser; + + protected string GetNoteHistoryUrl (string guid) { + var url = new GetNoteHistoryRequest () { + Guid = guid, + Username = RainyTestServer.TEST_USER + }.ToUrl ("GET"); + return testServer.ListenUrl + url; + } + protected string GetArchivedNoteUrl (string guid, long revision) { + var url = new GetArchivedNoteRequest () { + Guid = guid, + Username = RainyTestServer.TEST_USER, + Revision = revision + }.ToUrl ("GET"); + return testServer.ListenUrl + url; + } + + [Test] + public void NoteHistoryIsEmpty () + { + FirstSyncForBothSides (); + var client = testServer.GetJsonClient (); + + var first_note = clientEngineOne.GetNotes ().Values.First (); + var url = GetNoteHistoryUrl (first_note.Guid); + var resp = client.Get (url); + + Assert.AreEqual (0, resp.CurrentRevision); + //Assert.AreEqual (0, resp.Versions.Length); + } + + + + [Test] + [ExpectedException] + public void ExceptionForUnknownNote () + { + var client = testServer.GetJsonClient (); + var url = GetNoteHistoryUrl (Guid.NewGuid ().ToString ()); + client.Get (url); + + } + + [Test] + public void NoteHistoryIsPresentWithOneNoteAfterChange () + { + FirstSyncForBothSides (); + + var first_note = clientEngineOne.GetNotes ().Values.First (); + var new_title = "Some other title"; + var old_title = first_note.Title; + first_note.Title = new_title; + clientEngineOne.SaveNote (first_note); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = GetNoteHistoryUrl (first_note.Guid); + var resp = client.Get (url); + + Assert.AreEqual (1, resp.Versions.Length); + Assert.AreEqual (1, resp.Versions[0].Revision); + Assert.AreEqual (old_title, resp.Versions[0].Note.Title); + + } + + [Test] + public void RetrieveAnArchivedVersionOfANote () + { + FirstSyncForBothSides (); + + Tomboy.Note first_note = clientEngineOne.GetNotes ().Values.First (); + DTONote first_note_dto = first_note.ToDTONote (); + + var new_title = "Some other title"; + var old_title = first_note_dto.Title; + var new_text = "Some new text"; + var old_text = first_note_dto.Text; + + first_note.Title = new_title; + first_note.Text = new_text; + + clientEngineOne.SaveNote (first_note); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = GetNoteHistoryUrl (first_note.Guid); + var resp = client.Get (url); + + var rev = resp.Versions[0].Revision; + + url = GetArchivedNoteUrl (first_note.Guid, rev); + var note = client.Get (url); + + Assert.AreEqual (old_text, note.Text); + Assert.AreEqual (old_title, note.Title); + Assert.AreEqual (first_note_dto.Tags, note.Tags); + + } + + [Test] + public void NoteArchiveDoesHonorTheIncludeTextParameter () + { + FirstSyncForBothSides (); + + Tomboy.Note first_note = clientEngineOne.GetNotes ().Values.First (); + first_note.Title = "different"; + clientEngineOne.SaveNote (first_note); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = GetNoteHistoryUrl (first_note.Guid); + var resp = client.Get (url + "?include_text=false"); + + foreach (var archived_note in resp.Versions) { + Assert.AreEqual ("", archived_note.Note.Text); + } + + resp = client.Get (url + "?include_text=true"); + foreach (var archived_note in resp.Versions) { + Assert.AreNotEqual ("", archived_note.Note.Text); + } + } + + [Test] + public void NoteArchiveContainsAllGuids () + { + FirstSyncForBothSides (); + + // now, lets delete a note from the client + var first_note = clientEngineOne.GetNotes ().First ().Value; + first_note.Title = "different"; + clientEngineOne.SaveNote (first_note); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = testServer.ListenUrl + new GetNoteArchiveRequest (){ Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + var resp = client.Get (url); + + Assert.That (resp.Guids.Contains (first_note.Guid)); + Assert.AreEqual (1, resp.Guids.Count ()); + } + + [Test] + public void NoteArchiveContainsNoteDataButNoText () + { + FirstSyncForBothSides (); + + // now, lets delete a note from the client + var first_note = clientEngineOne.GetNotes ().First ().Value; + first_note.Title = "different"; + clientEngineOne.SaveNote (first_note); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = testServer.ListenUrl + new GetNoteArchiveRequest (){ Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + var resp = client.Get (url); + + Assert.AreEqual ("", resp.Notes[0].Text); + } + + [Test] + public void DeletedNoteShowsUpInNoteArchive() + { + FirstSyncForBothSides (); + + // now, lets delete a note from the client + var deleted_note = clientEngineOne.GetNotes ().First ().Value; + clientEngineOne.DeleteNote (deleted_note); + clientManifestOne.NoteDeletions.Add (deleted_note.Guid, deleted_note.Title); + + var sync_manager = new SyncManager (syncClientOne, syncServer); + sync_manager.DoSync (); + + var client = testServer.GetJsonClient (); + var url = testServer.ListenUrl + new GetNoteArchiveRequest (){ Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + var resp = client.Get (url); + + Assert.That (resp.Guids.Contains (deleted_note.Guid)); + } + + protected override void ClearServer (bool reset = false) + { + return; + } + } + + public class NoteHistoryTestsSqlite : NoteHistoryTestsBase + { + + [SetUp] + public new void SetUp () + { + testServer = new RainyTestServer (); + testServer.ScenarioSqlite (); + testServer.Start (); + + syncServer = new WebSyncServer (testServer.BaseUri, testServer.GetAccessToken ()); + } + + [TearDown] + public new void TearDown () + { + testServer.Stop (); + } + } + + public class NoteHistoryTestsPostgres : NoteHistoryTestsBase + { + + [SetUp] + public new void SetUp () + { + testServer = new RainyTestServer (); + testServer.ScenarioPostgres (); + testServer.Start (); + + syncServer = new WebSyncServer (testServer.BaseUri, testServer.GetAccessToken ()); + } + + [TearDown] + public new void TearDown () + { + testServer.Stop (); + } + } + +} diff --git a/Rainy-tests/OAuthTests.cs b/Rainy-tests/OAuthTests.cs index 78adbbc..e35217f 100644 --- a/Rainy-tests/OAuthTests.cs +++ b/Rainy-tests/OAuthTests.cs @@ -1,22 +1,32 @@ using System; -using NUnit.Framework; using DevDefined.OAuth.Consumer; using DevDefined.OAuth.Framework; +using NUnit.Framework; using ServiceStack.ServiceClient.Web; -using Tomboy.Sync.DTO; +using Tomboy.Sync.Web.DTO; +using Rainy.WebService.Management.Admin; +using System.Collections.Generic; +using System.Linq; +using Rainy.OAuth; +using Rainy.WebService.OAuth; -namespace Rainy +namespace Rainy.Tests.OAuth { [TestFixture()] - public class OAuthTests : RainyTestBase + public class OAuthTests : TestBase { + [SetUp] + public void SetUp () + { + testServer.Start (); + } [Test()] public void OAuthGetRequestToken () { var consumerContext = new OAuthConsumerContext () { ConsumerKey = "anyone" }; - var api_ref = RainyTestServer.GetRootApiRef (); + var api_ref = testServer.GetRootApiRef (); var session = new OAuthSession (consumerContext, api_ref.OAuthRequestTokenUrl, api_ref.OAuthAuthorizeUrl, api_ref.OAuthAccessTokenUrl); @@ -35,7 +45,7 @@ public void OAuthFullTokenExchange () { // the actual unit under test is GetAccessToken, but we // need it so often so it is its own method - IToken access_token = RainyTestServer.GetAccessToken (); + IToken access_token = testServer.GetAccessToken (); Assert.That (access_token.Token.Length > 14); Assert.That (access_token.TokenSecret.Length > 14); @@ -49,8 +59,8 @@ public void AccessFailsWithoutOAuthToken () { Exception caught_exception = new Exception (); try { - var apiResponse = RainyTestServer.GetRootApiRef (); - var restClient = new JsonServiceClient (RainyTestServer.BaseUri); + var apiResponse = testServer.GetRootApiRef (); + var restClient = new JsonServiceClient (testServer.BaseUri); restClient.Get (apiResponse.UserRef.ApiRef); // we are not allowed to reach here @@ -61,6 +71,86 @@ public void AccessFailsWithoutOAuthToken () Assert.AreEqual ("Unauthorized", caught_exception.Message); } } + + [Test] + public void BypassOAuthForTemporaryAccessToken () + { + var restClient = new JsonServiceClient (testServer.ListenUrl); + var req = new OAuthTemporaryAccessTokenRequest (); + req.Username = RainyTestServer.TEST_USER; + req.Password = RainyTestServer.TEST_PASS; + var token = restClient.Post ("/oauth/temporary_access_token", req); + Assert.That (!token.AccessToken.StartsWith ("oauth_")); + Assert.GreaterOrEqual (400, token.AccessToken.Length); + } + } + + [TestFixture()] + public class TokenApiTests : TestBase + { + [SetUp] + public void SetUp () + { + testServer.Start (); + } + [Test()] + public void GetListOfValidAccesstokens () + { + var client = testServer.GetJsonClient (); + var url = new GetTokenRequest ().ToUrl ("GET"); + var service_url = testServer.ListenUrl + url; + var tokens = client.Get> (service_url); + Assert.AreEqual (1, tokens.Count); + + testServer.GetAccessToken (); + testServer.GetAccessToken (); + tokens = client.Get> (service_url); + Assert.AreEqual(3, tokens.Count); + + } + [Test()] + public void DeleteAccessTokenRevokesTheToken () + { + var token = testServer.GetAccessToken (); + var token_part = token.Token.Substring (0, 24); + var client = testServer.GetJsonClient (); + var url = new DeleteTokenRequest () { + TokenPart = token_part + }.ToUrl ("DELETE"); + var service_url = testServer.ListenUrl + url; + client.Delete (service_url); + + // check that the token has been delete from the db + var get_url = new GetTokenRequest () { Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + var get_service_url = testServer.ListenUrl + get_url; + List tokens = client.Get> (get_service_url); + + var the_token = tokens.Where (t => token.Token.StartsWith (t.TokenPart)); + Assert.That (the_token.Count () == 0); + } + + [Test] + public void UpdateTokenDeviceName () + { + string new_device_name = "My home computer"; + + var token = testServer.GetAccessToken ().Token.ToShortToken (); + var client = testServer.GetJsonClient (); + var url = new UpdateTokenRequest ().ToUrl ("PUT"); + var req = new AccessTokenDto { + TokenPart = token, + DeviceName = new_device_name }; + var service_url = testServer.ListenUrl + url; + client.Put (service_url, req); + + // check that the token has been updated + var get_url = new GetTokenRequest () { Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + var get_service_url = testServer.ListenUrl + get_url; + List tokens = client.Get> (get_service_url); + var updated_token = tokens.First (t => token == t.TokenPart); + + Assert.AreEqual (new_device_name, updated_token.DeviceName); + } } } diff --git a/Rainy-tests/ORM/DatabaseBackendTests.cs b/Rainy-tests/ORM/DatabaseBackendTests.cs deleted file mode 100644 index f816380..0000000 --- a/Rainy-tests/ORM/DatabaseBackendTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using NUnit.Framework; - -namespace Rainy.Db -{ - - [TestFixture] - public class DatabaseBackendTests : DbTestsBase - { - Rainy.Interfaces.CredentialsVerifier auth = (user,pass) => { return true; }; - - [Test] - public void ReadWriteManifest () - { - var data_backend = new DatabaseBackend ("/tmp/rainy-test-data/rainy-test.db", auth, reset: true); - - var server_id = Guid.NewGuid ().ToString (); - using (var repo = data_backend.GetNoteRepository ("johndoe")) { - repo.Manifest.LastSyncRevision = 123; - repo.Manifest.ServerId = server_id; - } - - // check the manifest got saved - using (var repo = data_backend.GetNoteRepository ("johndoe")) { - Assert.AreEqual (123, repo.Manifest.LastSyncRevision); - Assert.AreEqual (server_id, repo.Manifest.ServerId); - } - } - } -} diff --git a/Rainy-tests/ORM/DbBasicTests.cs b/Rainy-tests/ORM/DbBasicTests.cs index c10d9d1..2b61f8c 100644 --- a/Rainy-tests/ORM/DbBasicTests.cs +++ b/Rainy-tests/ORM/DbBasicTests.cs @@ -1,21 +1,21 @@ using System; -using Tomboy.Sync.DTO; -using NUnit.Framework; -using System.Data; -using ServiceStack.OrmLite; -using ServiceStack.Common; using System.Collections.Generic; +using System.Data; using System.Linq; using DevDefined.OAuth.Framework; -using Rainy.OAuth.SimpleStore; using DevDefined.OAuth.Storage.Basic; +using NUnit.Framework; +using Rainy.Interfaces; using Rainy.OAuth; +using ServiceStack.Common; +using ServiceStack.OrmLite; +using Tomboy.Sync.Web.DTO; +using Rainy.WebService; +using Rainy.Db; -namespace Rainy.Db +namespace Rainy.Tests.Db { - - [TestFixture] - public class DbBasicTests : DbTestsBase + public abstract class DbBasicTests : DbTestsBase { [Test] public void StoreAndRetrieveNote () @@ -24,12 +24,12 @@ public void StoreAndRetrieveNote () db_old.Username = "test"; - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Insert (db_old); } DTONote dto_new = null; DBNote db_new; - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { db_new = conn.Single ("Username = {0}", "test"); } @@ -38,6 +38,7 @@ public void StoreAndRetrieveNote () // check for equalness Assert.AreEqual (db_old.Title, db_new.Title); Assert.AreEqual (db_old.Text, db_new.Text); + Assert.AreEqual (db_old.Tags, db_new.Tags); Assert.AreEqual (db_old.ChangeDate, db_new.ChangeDate); @@ -60,11 +61,11 @@ public void StoreOverlongText () sample_note.Title = overlong_string; sample_note.Username = "overlong"; - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Insert (sample_note); } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var note = conn.Single ("Username = {0}", "overlong"); Assert.AreEqual (20000, note.Text.Length); @@ -79,7 +80,7 @@ public void StoreLargeInsertTransaction () int num_samples = 250; var notes = GetDBSampleNotes (num_samples); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { using (var trans = conn.OpenTransaction ()) { conn.InsertAll (notes); trans.Commit (); @@ -95,12 +96,12 @@ public void UpdateNote () { var sample_note = GetDBSampleNote (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Insert (sample_note); sample_note.Title = "changed title"; conn.Update (sample_note); } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var db_notes = conn.Select (); Assert.AreEqual (1, db_notes.Count); var db_note = db_notes.First (); @@ -118,7 +119,7 @@ public void DeleteNote () var delete_note1 = sample_notes.First (); var delete_note2 = sample_notes.Last (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.InsertAll (sample_notes); conn.Delete (delete_note1); @@ -145,13 +146,13 @@ public void PendingTransaction () // make sure that inserted notes not yet commited can be // retrieved via .Select - var sample_note = GetDBSampleNote (username: "user"); + var sample_note = GetDBSampleNote (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { using (var trans = conn.BeginTransaction ()) { conn.Insert (sample_note); // get the note before it was commited - var db_note = conn.Single ("Username = {0}", "user"); + var db_note = conn.First (u => u.Username == testUser.Username); Assert.That (!ReferenceEquals (db_note, sample_note)); Assert.AreEqual (sample_note.Guid, db_note.Guid); @@ -160,19 +161,38 @@ public void PendingTransaction () } } + [Test] + public void ReadWriteManifest () + { + var data_backend = RainyTestServer.Container.Resolve (); + var requesting_user = new RequestingUser { Username = RainyTestServer.TEST_USER, EncryptionMasterKey = "deadbeefaffedeadbeef0ffe" }; + + var server_id = Guid.NewGuid ().ToString (); + using (var repo = data_backend.GetNoteRepository (requesting_user)) { + repo.Manifest.LastSyncRevision = 123; + repo.Manifest.ServerId = server_id; + } + + // check the manifest got saved + using (var repo = data_backend.GetNoteRepository (requesting_user)) { + Assert.AreEqual (123, repo.Manifest.LastSyncRevision); + Assert.AreEqual (server_id, repo.Manifest.ServerId); + } + } + [Test] public void UpdateNonExistingNoteDoesNotWork () { // test if we can update a non-existing note // (we assume we can't) - var sample_note = GetDBSampleNote (username: "test"); + var sample_note = GetDBSampleNote (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Update (sample_note); } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var result = conn.Select ("Username = {0}", "test"); Assert.AreEqual (0, result.Count); } @@ -183,14 +203,14 @@ public void DeleteNonExistingNote () { // test if it is ok to delete a note that does not exist - var sample_note = GetDBSampleNote (username: "test"); + var sample_note = GetDBSampleNote (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Delete (sample_note); } - using (var conn = dbFactory.OpenDbConnection ()) { - var result = conn.Select ("Username = {0}", "test"); + using (var conn = connFactory.OpenDbConnection ()) { + var result = conn.Select (u => u.Username == testUser.Username); Assert.AreEqual (0, result.Count); } } @@ -198,9 +218,9 @@ public void DeleteNonExistingNote () [Test] public void DeleteNonExistingNoteDoesNotCancelTransaction () { - var sample_note = GetDBSampleNote (username: "test"); + var sample_note = GetDBSampleNote (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { using (var trans = conn.BeginTransaction ()) { conn.Delete (sample_note); conn.Insert (sample_note); @@ -209,7 +229,7 @@ public void DeleteNonExistingNoteDoesNotCancelTransaction () } } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var result = conn.Select ("Username = {0}", "test"); Assert.AreEqual (1, result.Count); } @@ -220,7 +240,7 @@ public void SaveAndReadUser () { var user = new DBUser (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { user.Username = "test"; user.Manifest.ServerId = Guid.NewGuid ().ToString (); user.Manifest.LastSyncRevision = 123; @@ -228,7 +248,7 @@ public void SaveAndReadUser () conn.Save (user); } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var db_user = conn.First ("Username = {0}", "test"); Assert.AreEqual (user.Manifest.ServerId, db_user.Manifest.ServerId); @@ -249,10 +269,10 @@ public void SaveAndReadTokenBase () token.Token = Guid.NewGuid ().ToString (); token.TokenSecret = Guid.NewGuid ().ToString (); - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { conn.Insert (token.ToDBAccessToken ()); } - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { var dbtoken = conn.Select ().First (); Assert.AreEqual (token.Token, dbtoken.Token); Assert.AreEqual (token.TokenSecret, dbtoken.TokenSecret); @@ -260,9 +280,9 @@ public void SaveAndReadTokenBase () } [Test] - public void DbTokenRepository () + public void DbAccessTokenRepository () { - var repo = new DbAccessTokenRepository (); + var repo = new DbAccessTokenRepository (this.connFactory); var token1 = new AccessToken () { ConsumerKey = "anyone", @@ -280,8 +300,26 @@ public void DbTokenRepository () Assert.AreEqual (token1.Realm, token2.Realm); Assert.AreEqual (token1.UserName, token2.UserName); Assert.AreEqual (token1.ExpiryDate, token2.ExpiryDate); - Assert.AreEqual (token1.Token, token2.Token); + + // the token is only the first 16 byte = 192 bits - the token is + // 160 byte = 920 bits long (due to the padding added) + Assert.AreEqual (token1.Token.Substring (0, 24), token2.Token); Assert.AreEqual (token1.TokenSecret, token2.TokenSecret); } } + + public class DbBasicTestsSqlite : DbBasicTests + { + public DbBasicTestsSqlite () + { + this.dbScenario = "sqlite"; + } + } + public class DbBasicTestsPostgres : DbBasicTests + { + public DbBasicTestsPostgres () + { + this.dbScenario = "postgres"; + } + } } diff --git a/Rainy-tests/ORM/DbBenchmarks.cs b/Rainy-tests/ORM/DbBenchmarks.cs index 5efff9b..44fabcf 100644 --- a/Rainy-tests/ORM/DbBenchmarks.cs +++ b/Rainy-tests/ORM/DbBenchmarks.cs @@ -1,19 +1,20 @@ using System; -using NUnit.Framework; +using System.Collections.Generic; using System.Data; -using ServiceStack.OrmLite; using System.Diagnostics; -using System.Collections.Generic; using System.Threading.Tasks; +using NUnit.Framework; +using ServiceStack.OrmLite; +using Rainy.Tests.Db; +using Rainy.Db; -namespace Rainy.Db +namespace Rainy.Tests.Benchmarks { - [Ignore] - [TestFixture] - public class DbBenchmarks : DbTestsBase + public abstract class DbBenchmarks : DbTestsBase { private int num_runs = 10; private int num_notes = 200; + private int num_threads = 16; [SetUp] public new void SetUp () @@ -49,10 +50,10 @@ public void InsertBenchmark_SingleThreaded () protected void InsertBenchmark_Worker (List notes) { // now insert the notes - using (var conn = dbFactory.OpenDbConnection ()) { + using (var conn = connFactory.OpenDbConnection ()) { using (var trans = conn.OpenTransaction ()) { foreach (var note in notes) { - conn.Insert (note); + conn.Insert (note); } trans.Commit (); } @@ -63,7 +64,7 @@ protected void InsertBenchmark_Worker (List notes) public void InsertBenchmark_MultiThreaded () { Run("Insert_MultiThreaded", () => { - InsertBenchmark_MultiThreadedWorker (16, num_notes); + InsertBenchmark_MultiThreadedWorker (num_threads, num_notes); }); } @@ -85,4 +86,24 @@ private void InsertBenchmark_MultiThreadedWorker (int num_threads, int num_notes Task.WaitAll (tasks); } } + + [Ignore] + [TestFixture] + public class DbBenchmarksSqlite : DbBenchmarks + { + public DbBenchmarksSqlite () + { + this.dbScenario = "sqlite"; + } + } + + [Ignore] + [TestFixture] + public class DbBenchmarksPostgres : DbBenchmarks + { + public DbBenchmarksPostgres () + { + this.dbScenario = "postgres"; + } + } } diff --git a/Rainy-tests/ORM/DbEncryptedStorageTests.cs b/Rainy-tests/ORM/DbEncryptedStorageTests.cs new file mode 100644 index 0000000..8780781 --- /dev/null +++ b/Rainy-tests/ORM/DbEncryptedStorageTests.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using NUnit.Framework; +using Rainy.Crypto; +using ServiceStack.OrmLite; +using Tomboy; +using Rainy.Db; +using System.Security.Cryptography; + +namespace Rainy.Tests.Db +{ + + public abstract class DbEncryptedStorageTests : DbTestsBase + { + protected string key = "d019f8a34c5b2c0fd1444e27ba02eec1f7816739ff98a674043fb3da72bbd625"; + + [Test] + public void EncryptedStorageStoresNoPlaintextNotes () + { + var storage = new DbEncryptedStorage (connFactory, testUser, key); + + var sample_notes = DbStorageTests.GetSampleNotes (); + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + storage.Dispose (); + + foreach(var note in sample_notes) { + // the stored notes should only contain hex chars + using (var db = connFactory.OpenDbConnection ()) { + var db_note = db.First (n => n.Guid == note.Guid); + // this will fail if any non-hex chars are in + db_note.Text.ToByteArray (); + } + } + } + + [Test] + public void NoteIsAlwaysEncryptedWithSameKey () + { + string first_key; + var note = DbStorageTests.GetSampleNotes ()[0]; + // save for first time + using (var storage = new DbEncryptedStorage (connFactory, testUser, key)) { + storage.SaveNote (note); + } + using (var db = connFactory.OpenDbConnection ()) { + var db_note = db.First (n => n.Guid == note.Guid); + first_key = db_note.EncryptedKey; + Assert.That (!string.IsNullOrEmpty (first_key)); + } + + // change the text and store note again + note.Text = "Foobar"; + + // save for first time + using (var storage = new DbEncryptedStorage (connFactory, testUser, key)) { + storage.SaveNote (note); + } + using (var db = connFactory.OpenDbConnection ()) { + var db_note = db.First (n => n.Guid == note.Guid); + Assert.AreEqual (first_key, db_note.EncryptedKey); + } + } + + [Test] + public void NoteIsEncryptedIsCorrectlySet () + { + // test with encrypted notes + DbStorage storage = new DbEncryptedStorage (this.connFactory, this.testUser, key); + var sample_notes = DbStorageTests.GetSampleNotes (); + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + storage.Dispose (); + + foreach(var note in sample_notes) { + // the stored notes should only contain hex chars + using (var db = connFactory.OpenDbConnection ()) { + var db_note = db.First (n => n.Guid == note.Guid); + // this will fail if any non-hex chars are in + Assert.IsTrue (db_note.IsEncypted); + } + } + + storage = new DbStorage (this.connFactory, this.testUser); + sample_notes = DbStorageTests.GetSampleNotes (); + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + storage.Dispose (); + + foreach(var note in sample_notes) { + // the stored notes should only contain hex chars + using (var db = connFactory.OpenDbConnection ()) { + var db_note = db.First (n => n.Guid == note.Guid); + // this will fail if any non-hex chars are in + Assert.IsFalse (db_note.IsEncypted); + } + } + } + + [Test] + public void ReEncryptNotes () + { + DbEncryptedStorage storage = new DbEncryptedStorage (this.connFactory, this.testUser, key); + var sample_notes = DbStorageTests.GetSampleNotes (); + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + storage.Dispose (); + + storage = new DbEncryptedStorage (connFactory, testUser, key); + var new_key = CryptoHelper.Create256BitLowerCaseHexKey (new RNGCryptoServiceProvider ()); + + storage.ReEncryptAllNotes (new_key); + storage.Dispose (); + + // try to open the notes with the new key + storage = new DbEncryptedStorage (connFactory, testUser, new_key); + var decrypted_notes = storage.GetNotes (); + foreach (var note in sample_notes) { + Assert.AreEqual (note.Text, decrypted_notes.Values.First (n => n.Guid == note.Guid).Text); + } + } + + [Test] + public void StorageCanBeReusedAfterReEncryptNotes () + { + DbEncryptedStorage storage = new DbEncryptedStorage (this.connFactory, this.testUser, key); + var sample_notes = DbStorageTests.GetSampleNotes (); + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + storage.Dispose (); + + storage = new DbEncryptedStorage (connFactory, testUser, key); + var new_key = CryptoHelper.Create256BitLowerCaseHexKey (new RNGCryptoServiceProvider ()); + + storage.ReEncryptAllNotes (new_key); + var decrypted_notes = storage.GetNotes (); + + foreach (var note in sample_notes) { + Assert.AreEqual (note.Text, decrypted_notes.Values.First (n => n.Guid == note.Guid).Text); + } + } + } + + [TestFixture()] + public class DbEncryptedStorageTestsSqlite : DbEncryptedStorageTests + { + public DbEncryptedStorageTestsSqlite () + { + this.dbScenario = "sqlite"; + } + } + + [TestFixture()] + public class DbEncryptedStorageTestsPostgres : DbEncryptedStorageTests + { + public DbEncryptedStorageTestsPostgres () + { + this.dbScenario = "postgres"; + } + } +} diff --git a/Rainy-tests/ORM/DbStorageTests.cs b/Rainy-tests/ORM/DbStorageTests.cs index a0c4360..aad4cef 100644 --- a/Rainy-tests/ORM/DbStorageTests.cs +++ b/Rainy-tests/ORM/DbStorageTests.cs @@ -1,24 +1,17 @@ using System; +using System.Collections.Generic; using System.Data; +using System.Linq; using NUnit.Framework; +using ServiceStack.OrmLite; using Tomboy; -using System.Collections.Generic; -using System.Linq; +using Rainy.Db; -namespace Rainy.Db +namespace Rainy.Tests.Db { - [TestFixture()] - public class DbStorageTests : DbTestsBase + public abstract class DbStorageTests : DbTestsBase { - private IDbConnection dbConnection; - - [SetUp] - public void SetUp () - { - this.dbConnection = this.dbFactory.OpenDbConnection (); - } - - protected List GetSampleNotes () + public static List GetSampleNotes () { var sample_notes = new List (); @@ -55,19 +48,24 @@ protected List GetSampleNotes () [Test] public void StoreSomeNotes () { - string username = "test"; var sample_notes = GetSampleNotes (); - using (var store = new DbStorage (username)) { + using (var store = new DbStorage (connFactory, testUser)) { sample_notes.ForEach (n => store.SaveNote (n)); } // now check if we have stored that notes - using (var store = new DbStorage (username)) { + using (var store = new DbStorage (connFactory, testUser)) { var stored_notes = store.GetNotes ().Values.ToList (); Assert.AreEqual (sample_notes.Count, stored_notes.Count); stored_notes.ForEach(n => Assert.Contains (n, sample_notes)); + // check that the dates are still the same + stored_notes.ForEach(n => { + var sample_note = sample_notes.First(sn => sn.Guid == n.Guid); + Assert.AreEqual (n.ChangeDate, sample_note.ChangeDate); + }); + } } @@ -76,7 +74,7 @@ public void StoreAndDelete () { StoreSomeNotes (); - using (var store = new DbStorage ("test")) { + using (var store = new DbStorage (connFactory, testUser)) { var stored_notes = store.GetNotes ().Values.ToList (); var deleted_note = stored_notes[0]; @@ -95,6 +93,92 @@ public void StoreAndDelete () Assert.That (! store.GetNotes ().Values.Contains (deleted_note)); } } + + [Test] + public void DateUtcIsCorrectlyStored () + { + DbStorage storage = new DbStorage(connFactory, testUser); + + var tomboy_note = new Note (); + tomboy_note.ChangeDate = new DateTime (2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + tomboy_note.CreateDate = tomboy_note.ChangeDate; + tomboy_note.MetadataChangeDate = tomboy_note.ChangeDate; + + storage.SaveNote (tomboy_note); + var stored_note = storage.GetNotes ().Values.First (); + + storage.Dispose (); + + Assert.AreEqual (tomboy_note.ChangeDate, stored_note.ChangeDate.ToUniversalTime ()); + + } + [Test] + public void DateLocalIsCorrectlyStored () + { + DbStorage storage = new DbStorage(connFactory, testUser); + + var tomboy_note = new Note (); + tomboy_note.ChangeDate = new DateTime (2000, 1, 1, 0, 0, 0, DateTimeKind.Local); + tomboy_note.CreateDate = tomboy_note.ChangeDate; + tomboy_note.MetadataChangeDate = tomboy_note.ChangeDate; + + storage.SaveNote (tomboy_note); + var stored_note = storage.GetNotes ().Values.First (); + + storage.Dispose (); + + Assert.AreEqual (tomboy_note.ChangeDate, stored_note.ChangeDate.ToUniversalTime ()); + + } + + // note history tests + [Test] + public void NoteHistoryIsSaved () + { + var sample_notes = DbStorageTests.GetSampleNotes (); + + using (var storage = new DbStorage (this.connFactory, this.testUser, use_history: true)) { + foreach(var note in sample_notes) { + storage.SaveNote (note); + } + } + + // modify the notes + using (var storage = new DbStorage (this.connFactory, this.testUser, use_history: true)) { + foreach(var note in sample_notes) { + note.Title = "Random new title"; + storage.SaveNote (note); + } + } + + // for each note there should exist a backup copy + foreach (var note in sample_notes) { + using (var db = connFactory.OpenDbConnection ()) { + var archived_note = db.FirstOrDefault (n => n.Guid == note.Guid); + Assert.IsNotNull (archived_note); + Assert.AreNotEqual ("Random new title", archived_note.Title); + } + } + } + } + + + [TestFixture()] + public class DbStorageTestsSqlite : DbStorageTests + { + public DbStorageTestsSqlite () + { + this.dbScenario = "sqlite"; + } + } + + [TestFixture()] + public class DbStorageTestsPostgres : DbStorageTests + { + public DbStorageTestsPostgres () + { + this.dbScenario = "postgres"; + } } } diff --git a/Rainy-tests/ORM/DbTestsBase.cs b/Rainy-tests/ORM/DbTestsBase.cs index 2927cad..8d13693 100644 --- a/Rainy-tests/ORM/DbTestsBase.cs +++ b/Rainy-tests/ORM/DbTestsBase.cs @@ -1,38 +1,40 @@ using System; -using Tomboy.Sync.DTO; -using NUnit.Framework; +using System.Collections.Generic; using System.Data; +using NUnit.Framework; +using Rainy.Db; using ServiceStack.OrmLite; -using System.Collections.Generic; -using System.IO; +using Tomboy.Sync.Web.DTO; +using Rainy.Tests; -namespace Rainy.Db +namespace Rainy.Tests.Db { - public class DbTestsBase + public class DbTestsBase : TestBase { - protected OrmLiteConnectionFactory dbFactory; - + protected DBUser testUser; + protected IDbConnectionFactory connFactory; + protected string dbScenario; + [TestFixtureSetUp] public void FixtureSetUp () { - DbConfig.SetSqliteFile ("/tmp/rainy-test-data/rainy-test.db"); - // remove the rainy-test.db file if it exists - if (File.Exists (DbConfig.SqliteFile)) { - File.Delete (DbConfig.SqliteFile); - } - - dbFactory = new OrmLiteConnectionFactory (DbConfig.ConnectionString, SqliteDialect.Provider); - } [SetUp] - public void SetUp () + public new void SetUp () { - // Start with empty tables in each test run - using (var c = dbFactory.OpenDbConnection ()) { - c.DropAndCreateTable (); - c.DropAndCreateTable (); - c.DropAndCreateTable (); + testServer = new RainyTestServer (); + if (dbScenario == "postgres") { + testServer.ScenarioPostgres (); + } else if (dbScenario == "sqlite" || string.IsNullOrEmpty (dbScenario)) { + testServer.ScenarioSqlite (); + } + + testServer.Start (); + + this.connFactory = RainyTestServer.Container.Resolve (); + using (var db = connFactory.OpenDbConnection ()) { + testUser = db.First (u => u.Username == RainyTestServer.TEST_USER); } } @@ -55,13 +57,12 @@ protected DTONote GetDTOSampleNote () Guid = Guid.NewGuid ().ToString () }; } - protected DBNote GetDBSampleNote (string username = "test") + protected DBNote GetDBSampleNote () { - var db_note = GetDTOSampleNote ().ToDBNote (); - db_note.Username = username; + var db_note = GetDTOSampleNote ().ToDBNote (testUser); return db_note; } - protected List GetDTOSampleNotes (int num, string username = "test") + protected List GetDTOSampleNotes (int num) { var notes = new List (); @@ -70,12 +71,12 @@ protected List GetDTOSampleNotes (int num, string username = "test") } return notes; } - protected List GetDBSampleNotes (int num, string username = "test") + protected List GetDBSampleNotes (int num) { var notes = new List (); for (int i=0; i < num; i++) { - notes.Add (GetDBSampleNote (username)); + notes.Add (GetDBSampleNote ()); } return notes; diff --git a/Rainy-tests/PublicNoteShareTestsSqlite.cs b/Rainy-tests/PublicNoteShareTestsSqlite.cs new file mode 100644 index 0000000..4cc771e --- /dev/null +++ b/Rainy-tests/PublicNoteShareTestsSqlite.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Net; +using NUnit.Framework; +using Rainy.Db; +using Rainy.WebService; +using ServiceStack.ServiceClient.Web; +using Tomboy.Sync.Web; + +namespace Rainy.Tests.RestApi +{ + public class PublicNoteShareTestsSqlite : Tomboy.Sync.AbstractSyncManagerTestsBase + { + protected RainyTestServer testServer; + protected DBUser testUser; + + [SetUp] + public new void SetUp () + { + testServer = new RainyTestServer (); + testServer.ScenarioSqlite (); + testServer.Start (); + + syncServer = new WebSyncServer (testServer.BaseUri, testServer.GetAccessToken ()); + } + + [TearDown] + public new void TearDown () + { + testServer.Stop (); + } + + [Test] + public void GetShareableUrlWorks () + { + FirstSyncForBothSides (); + var client = testServer.GetJsonClient (); + + var first_note = clientEngineOne.GetNotes ().Values.First (); + var url = testServer.ListenUrl + new GetPublicUrlForNote () { Username = RainyTestServer.TEST_USER }.ToUrl ("GET"); + + var resp = client.Get (url); + + // fetch the note from that url via simple, unauthed http request + var wc = new WebClient (); + var content = wc.DownloadString (resp); + + Assert.Fail ("Not implemented yet"); + } + + protected override void ClearServer (bool reset) + { + return; + } + } + +} diff --git a/Rainy-tests/Rainy-tests.csproj b/Rainy-tests/Rainy-tests.csproj index 208802d..19738fc 100644 --- a/Rainy-tests/Rainy-tests.csproj +++ b/Rainy-tests/Rainy-tests.csproj @@ -28,43 +28,69 @@ 4 False + + false + bin\Testing + 4 + + + + False + + + + ..\lib\Npgsql.dll + + + ..\packages\ServiceStack.3.9.61\lib\net35\ServiceStack.dll + - ..\tomboy-library\Tomboy-library\Libs\ServiceStack-v3.9.32\ServiceStack.Common.dll + ..\packages\ServiceStack.Common.3.9.61\lib\net35\ServiceStack.Common.dll - - ..\tomboy-library\Tomboy-library\Libs\ServiceStack-v3.9.32\ServiceStack.Text.dll + + ..\packages\ServiceStack.Common.3.9.61\lib\net35\ServiceStack.Interfaces.dll - - ..\lib\ServiceStack-v3.9.32\ServiceStack.OrmLite.Sqlite.dll + + ..\packages\ServiceStack.OrmLite.PostgreSQL.3.9.61\lib\net35\ServiceStack.OrmLite.PostgreSQL.dll - ..\lib\ServiceStack-v3.9.32\ServiceStack.OrmLite.dll + ..\packages\ServiceStack.OrmLite.PostgreSQL.3.9.61\lib\net35\ServiceStack.OrmLite.dll - - - - ..\tomboy-library\Tomboy-library\Libs\ServiceStack-v3.9.32\ServiceStack.Interfaces.dll + + ..\packages\ServiceStack.OrmLite.Sqlite.Mono.3.9.61\lib\net35\Mono.Data.Sqlite.dll - - False + + ..\packages\ServiceStack.OrmLite.Sqlite.Mono.3.9.61\lib\net35\ServiceStack.OrmLite.Sqlite.dll + + + ..\packages\ServiceStack.Text.3.9.61\lib\net35\ServiceStack.Text.dll + + + ..\packages\ServiceStack.3.9.61\lib\net35\ServiceStack.ServiceInterface.dll - - - - - + + + + + + + + + + + @@ -80,12 +106,25 @@ {216746A7-D297-4108-933A-1E926EB9B167} Tomboy-library-tests - + + {BA3396DF-4D7E-45C0-9BF1-3F52FE6FFA92} + tomboy-library-websync + + {61B700B6-3911-4CEF-8751-7F42CA765068} DevDefined.OAuth + + {10095092-E1E5-441F-9380-B6329F3CFEB4} + JsonConfig + + + + + + \ No newline at end of file diff --git a/Rainy-tests/RainySyncManagerTests.cs b/Rainy-tests/RainySyncManagerTests.cs deleted file mode 100644 index 120348e..0000000 --- a/Rainy-tests/RainySyncManagerTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using NUnit.Framework; -using Tomboy.Sync.Web; - -namespace Rainy -{ - - [TestFixture] - public class RainySyncManagerTests : Tomboy.Sync.AbstractSyncManagerTests - { - [SetUp] - public new void SetUp () - { - RainyTestServer.StartNewServer (); - - syncServer = new WebSyncServer (RainyTestServer.BaseUri, RainyTestServer.GetAccessToken ()); - } - - [TearDown] - public new void TearDown () - { - RainyTestServer.Stop (); - } - - protected override void ClearServer (bool reset = false) - { - return; - } - } -} diff --git a/Rainy-tests/RainyTestBase.cs b/Rainy-tests/RainyTestBase.cs deleted file mode 100644 index 0598fb3..0000000 --- a/Rainy-tests/RainyTestBase.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NUnit.Framework; - -namespace Rainy -{ - public abstract class RainyTestBase - { - protected string baseUri; - protected string listenUri; - - [SetUp] - public void SetUp () - { - RainyTestServer.StartNewServer (); - baseUri = RainyTestServer.BaseUri; - } - [TearDown] - public void TearDown () - { - RainyTestServer.Stop (); - } - } - -} diff --git a/Rainy-tests/RainyWebSyncServerTests.cs b/Rainy-tests/RainyWebSyncServerTests.cs deleted file mode 100644 index 547d1c8..0000000 --- a/Rainy-tests/RainyWebSyncServerTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NUnit.Framework; -using Tomboy.Sync.Web; - -namespace Rainy -{ - [TestFixture] - public class RainyWebSyncServerTests : Tomboy.Sync.AbstractSyncServerTests - { - - [SetUp] - public void SetUp () - { - CreateSomeSampleNotes (); - - RainyTestServer.StartNewServer (); - - syncServer = new WebSyncServer (RainyTestServer.BaseUri, RainyTestServer.GetAccessToken ()); - } - - [TearDown] - public void TearDown () - { - RainyTestServer.Stop (); - } - } - -} diff --git a/Rainy-tests/TestBase.cs b/Rainy-tests/TestBase.cs new file mode 100644 index 0000000..68ea964 --- /dev/null +++ b/Rainy-tests/TestBase.cs @@ -0,0 +1,65 @@ +using NUnit.Framework; +using ServiceStack.ServiceClient.Web; +using Rainy.Interfaces; + +namespace Rainy.Tests +{ + public class DummyAuthenticator : IAuthenticator + { + public bool VerifyCredentials (string username, string password) + { + return true; + } + } + public class DummyAdminAuthenticator : IAdminAuthenticator + { + string Password; + public DummyAdminAuthenticator () + { + } + public DummyAdminAuthenticator (string pass) + { + Password = pass; + } + public bool VerifyAdminPassword (string password) + { + if (string.IsNullOrEmpty (Password)) + return true; + else return Password == password; + } + } + + public abstract class TestBase + { + protected RainyTestServer testServer; + + public TestBase () + { + } + + [SetUp] + public virtual void SetUp () + { + testServer = new RainyTestServer (); + } + [TearDown] + public virtual void TearDown () + { + testServer.Stop (); + } + + protected JsonServiceClient GetAdminServiceClient () + { + var client = new JsonServiceClient (testServer.ListenUrl); + client.LocalHttpWebRequestFilter += (request) => { + request.Headers.Add ("Authority", RainyTestServer.ADMIN_TEST_PASS); + }; + + return client; + } + protected JsonServiceClient GetServiceClient () + { + return new JsonServiceClient (testServer.ListenUrl); + } + } +} diff --git a/Rainy-tests/RainyTestServerTests.cs b/Rainy-tests/TestServerTests.cs similarity index 67% rename from Rainy-tests/RainyTestServerTests.cs rename to Rainy-tests/TestServerTests.cs index d73517b..42a76e6 100644 --- a/Rainy-tests/RainyTestServerTests.cs +++ b/Rainy-tests/TestServerTests.cs @@ -1,17 +1,25 @@ using System; using NUnit.Framework; -using Tomboy.Sync.DTO; +using Tomboy.Sync.Web.DTO; +using Rainy.Tests; -namespace Rainy +namespace Rainy.Tests { - public class RainyTestServerTests : RainyTestBase + + public class TestServerTests : TestBase { + [SetUp] + public void SetUp () + { + testServer.ScenarioSqlite (); + testServer.Start (); + } [Test] public void CheckApiRef () { - var response = RainyTestServer.GetRootApiRef (); + var response = testServer.GetRootApiRef (); - var rainy_listen_url = RainyTestServer.RainyListenUrl; + var rainy_listen_url = testServer.ListenUrl; Assert.AreEqual ("1.0", response.ApiVersion); // check the OAuth urls @@ -28,9 +36,9 @@ public void CheckApiRef () [Test] public void GetUser () { - var user_response = RainyTestServer.GetUserInfo (); + var user_response = testServer.GetUserInfo (); - Assert.AreEqual (user_response.Username, "johndoe"); + Assert.AreEqual (user_response.Username, RainyTestServer.TEST_USER); Assert.AreEqual (user_response.LatestSyncRevision, -1); Assert.That (Uri.IsWellFormedUriString (user_response.NotesRef.ApiRef, UriKind.Absolute)); diff --git a/Rainy-tests/UbuntuOneWebSyncServerTests.cs b/Rainy-tests/UbuntuOneWebSyncServerTests.cs new file mode 100644 index 0000000..f7027b0 --- /dev/null +++ b/Rainy-tests/UbuntuOneWebSyncServerTests.cs @@ -0,0 +1,70 @@ +// +// Author: +// Timo Dörr +// +// Copyright (c) 2012 Timo Dörr +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using NUnit.Framework; +using DevDefined.OAuth.Framework; +using DevDefined.OAuth.Storage.Basic; +using Tomboy.Sync.Web; +using System.Linq; +using Tomboy.Sync; + +namespace Tomboy +{ + [TestFixture] + + // UbuntuOne currently broken + // although GetAllNotes () works, DeleteNotes() does not + // and we need this to reset U1 notes to zero before each test + [Ignore] + public class UbuntuOneWebSyncServerTests : AbstractSyncServerTests + { + protected IToken GetAccessToken () + { + // access tokens can be retrieved with gconf once tomboy is setup for syncing + // use those paths: + // /apps/tomboy/sync/tomboyweb/oauth_token + // /apps/tomboy/sync/tomboyweb/oauth_token_secret + + IToken access_token = new AccessToken (); + access_token.ConsumerKey = "anyone"; + access_token.Token = "zqkX2sJ0DN2xS2wp7Vjb"; + access_token.TokenSecret = "zjhRkTWWFSJCQdZgr61thWD7qDz7z3t7LT3F9mQ7Hxk0cDV0hqF11xcRR38dLVJxX1Qb3lxCcRN5nwXt"; + + return access_token; + } + + [SetUp] + public void SetUp () + { + + var uri = "https://one.ubuntu.com/notes/"; + + this.syncServer = new WebSyncServer (uri, GetAccessToken ()); + + // delete all notes on the server before every test + syncServer.BeginSyncTransaction (); + var notes = syncServer.GetAllNotes (false); + syncServer.DeleteNotes (notes.Select (n => n.Guid).ToList ()); + + notes = syncServer.GetAllNotes (false); + Assert.AreEqual (0, notes.Count); + } + } +} diff --git a/Rainy-tests/WebSyncServerTests.cs b/Rainy-tests/WebSyncServerTests.cs new file mode 100644 index 0000000..6d41a9e --- /dev/null +++ b/Rainy-tests/WebSyncServerTests.cs @@ -0,0 +1,62 @@ +using NUnit.Framework; +using Rainy.Db; +using Tomboy.Sync.Web; + +namespace Rainy.Tests.RestApi +{ + + [TestFixture] + public class WebSyncServerTestsSqlite : Tomboy.Sync.AbstractSyncManagerTests + { + protected RainyTestServer testServer; + protected DBUser testUser; + + [SetUp] + public new void SetUp () + { + testServer = new RainyTestServer (); + testServer.ScenarioSqlite (); + testServer.Start (); + + syncServer = new WebSyncServer (testServer.BaseUri, testServer.GetAccessToken ()); + } + + [TearDown] + public new void TearDown () + { + testServer.Stop (); + } + + protected override void ClearServer (bool reset = false) + { + return; + } + } + [TestFixture] + public class WebSyncServerTestsPostgres : Tomboy.Sync.AbstractSyncManagerTests + { + protected RainyTestServer testServer; + protected DBUser testUser; + + [SetUp] + public new void SetUp () + { + testServer = new RainyTestServer (); + testServer.ScenarioPostgres (); + testServer.Start (); + + syncServer = new WebSyncServer (testServer.BaseUri, testServer.GetAccessToken ()); + } + + [TearDown] + public new void TearDown () + { + testServer.Stop (); + } + + protected override void ClearServer (bool reset = false) + { + return; + } + } +} diff --git a/Rainy-tests/packages.config b/Rainy-tests/packages.config new file mode 100644 index 0000000..0e4d8a7 --- /dev/null +++ b/Rainy-tests/packages.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Rainy.UI/.gitignore b/Rainy.UI/.gitignore new file mode 100644 index 0000000..863758e --- /dev/null +++ b/Rainy.UI/.gitignore @@ -0,0 +1,7 @@ +bower_components/ +node_modules/ +components/ +test/ +scripts/ +config/ +logs/ diff --git a/Rainy.UI/.jshintrc b/Rainy.UI/.jshintrc new file mode 100644 index 0000000..6c3c975 --- /dev/null +++ b/Rainy.UI/.jshintrc @@ -0,0 +1,23 @@ +{ + "sub": true, + "indent": 4, + "curly": false, + "eqnull": true, + "eqeqeq": true, + "undef": true, + "browser": true, + "quotmark": "single", + + "globals": { + "admin_pw": true, + "jQuery": true, + "$": true, + "console": true, + "angular": true, + "module": true, + "noty": true, + "app": true, + "_": true, + "wysihtml5": true + } +} diff --git a/Rainy.UI/Gruntfile.js b/Rainy.UI/Gruntfile.js new file mode 100755 index 0000000..9b9b461 --- /dev/null +++ b/Rainy.UI/Gruntfile.js @@ -0,0 +1,96 @@ +module.exports = function (grunt) { + + // Project configuration. + grunt.initConfig({ + jshint: { + options: { + jshintrc: '.jshintrc' + }, + files: [ 'app_admin/**/*.js' ] + }, + concat: { + options: { + separator: '\n' + }, + scripts_common: { + src: [ + 'bower_components/jquery/jquery.min.js', + 'bower_components/bootstrap.zip/js/bootstrap.min.js', + 'bower_components/angular/angular.min.js', + 'bower_components/angular-strap/dist/angular-strap.min.js', + 'bower_components/underscore/underscore-min.js', + 'bower_components/noty/js/noty/jquery.noty.js', + 'bower_components/noty/js/noty/themes/default.js', + 'bower_components/noty/js/noty/layouts/topCenter.js' + ], + dest: 'dist/common.js' + }, + scripts_admin: { + src: [ + 'app_admin/js/admin/**/*.js' + ], + dest: 'dist/admin/admin.js' + }, + scripts_client: { + src: [ + 'bower_components/wysihtml5/dist/wysihtml5-0.3.0.min.js', + 'bower_components/bootstrap-wysihtml5/dist/bootstrap-wysihtml5-0.0.2.min.js', + 'app_admin/js/client/**/*.js', + 'app_admin/js/shared/**/*.js' + ], + dest: 'dist/client.js' + }, + css_client: { + src: [ + 'bower_components/bootstrap-wysihtml5/dist/bootstrap-wysihtml5-0.0.2.css' + ], + dest: 'dist/client.css' + }, + admin_ui: { + src: ['app_admin/admin.html', 'app_admin/html/views_admin/*.html'], + dest: 'dist/admin/index.html' + }, + client_ui: { + src: ['app_admin/client.html', 'app_admin/html/views_client/*.html'], + dest: 'dist/index.html' + }, + }, + copy: { + wysi: { + expand: false, + flatten: true, + src: [ 'app_admin/css/wysihtml5_style.css' ], + dest: 'dist/wysihtml5_style.css' + }, + favicon: { + expand: false, + flatten: true, + src: [ 'app_admin/favicon.ico' ], + dest: 'dist/favicon.ico' + } + }, + watch: { + files: [ + 'app_admin/**/*.js', + 'app_admin/**/*.html', + 'app_admin/**/*.css', + 'Gruntfile.js' + ], + tasks: ['default'] + }, + reload: { + port: 35729, + liveReload: {} + } + }); + + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-reload'); + + // Default task. + grunt.registerTask('default', ['concat', 'copy', 'reload', 'jshint', 'watch']); + +}; diff --git a/Rainy.UI/app_admin/admin.html b/Rainy.UI/app_admin/admin.html new file mode 100755 index 0000000..efee5ab --- /dev/null +++ b/Rainy.UI/app_admin/admin.html @@ -0,0 +1,54 @@ + + + + + + + Rainy - Admin + + + + + + +
+ +
+ +
+ +
+
+ + + +
+ + + diff --git a/Rainy.UI/app_admin/client.html b/Rainy.UI/app_admin/client.html new file mode 100644 index 0000000..c0745c1 --- /dev/null +++ b/Rainy.UI/app_admin/client.html @@ -0,0 +1,55 @@ + + + + + + + Rainy - 2 GO + + + + + + + +
+ +
+ +
+ +
+ + + + + + diff --git a/Rainy.UI/app_admin/css/wysihtml5_style.css b/Rainy.UI/app_admin/css/wysihtml5_style.css new file mode 100644 index 0000000..33f06b2 --- /dev/null +++ b/Rainy.UI/app_admin/css/wysihtml5_style.css @@ -0,0 +1,13 @@ +/* this file is loaded by the wysihtm5 editor to get styling information */ + +h1, h2, h3, pre, small { + display: inline; + line-height: 1em; +} +.highlight { + background-color: yellow; +} + +small { + font-size: 8pt; +} diff --git a/Rainy.UI/app_admin/favicon.ico b/Rainy.UI/app_admin/favicon.ico new file mode 100644 index 0000000..88759f3 Binary files /dev/null and b/Rainy.UI/app_admin/favicon.ico differ diff --git a/Rainy.UI/app_admin/html/.gitignore b/Rainy.UI/app_admin/html/.gitignore new file mode 100755 index 0000000..e69de29 diff --git a/Rainy.UI/app_admin/html/views_admin/alluser.html b/Rainy.UI/app_admin/html/views_admin/alluser.html new file mode 100755 index 0000000..448538a --- /dev/null +++ b/Rainy.UI/app_admin/html/views_admin/alluser.html @@ -0,0 +1,117 @@ + diff --git a/Rainy.UI/app_admin/html/views_admin/auth.html b/Rainy.UI/app_admin/html/views_admin/auth.html new file mode 100755 index 0000000..6649be0 --- /dev/null +++ b/Rainy.UI/app_admin/html/views_admin/auth.html @@ -0,0 +1,22 @@ + + + diff --git a/Rainy.UI/app_admin/html/views_admin/login.html b/Rainy.UI/app_admin/html/views_admin/login.html new file mode 100755 index 0000000..6cc33ef --- /dev/null +++ b/Rainy.UI/app_admin/html/views_admin/login.html @@ -0,0 +1,42 @@ + diff --git a/Rainy.UI/app_admin/html/views_admin/overview.html b/Rainy.UI/app_admin/html/views_admin/overview.html new file mode 100755 index 0000000..0edc1d4 --- /dev/null +++ b/Rainy.UI/app_admin/html/views_admin/overview.html @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/Rainy.UI/app_admin/html/views_client/login.html b/Rainy.UI/app_admin/html/views_client/login.html new file mode 100644 index 0000000..f195372 --- /dev/null +++ b/Rainy.UI/app_admin/html/views_client/login.html @@ -0,0 +1,60 @@ + diff --git a/Rainy.UI/app_admin/html/views_client/notes.html b/Rainy.UI/app_admin/html/views_client/notes.html new file mode 100644 index 0000000..ee572dc --- /dev/null +++ b/Rainy.UI/app_admin/html/views_client/notes.html @@ -0,0 +1,54 @@ + diff --git a/Rainy.UI/app_admin/html/views_client/signup.html b/Rainy.UI/app_admin/html/views_client/signup.html new file mode 100644 index 0000000..62e8e23 --- /dev/null +++ b/Rainy.UI/app_admin/html/views_client/signup.html @@ -0,0 +1,109 @@ + diff --git a/Rainy.UI/app_admin/html/views_client/wysiwyg.html b/Rainy.UI/app_admin/html/views_client/wysiwyg.html new file mode 100644 index 0000000..12b453f --- /dev/null +++ b/Rainy.UI/app_admin/html/views_client/wysiwyg.html @@ -0,0 +1,12 @@ + diff --git a/Rainy.UI/app_admin/js/admin/app_admin.js b/Rainy.UI/app_admin/js/admin/app_admin.js new file mode 100755 index 0000000..3c5f5c3 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/app_admin.js @@ -0,0 +1,95 @@ +// Declare app level module which depends on filters, and services +var app = angular.module('myApp', [ + 'myApp.filters', + 'myApp.services', + 'myApp.directives', + + // anguar-strap.js + '$strap.directives' +]) +.config(['$routeProvider', + function($routeProvider) { + // admin interface + $routeProvider.when('/user', { + templateUrl: 'user.html', + controller: 'AllUserCtrl' + }); + $routeProvider.when('/overview', { + templateUrl: 'overview.html', + controller: 'StatusCtrl' + }); + + // login page for OAUTH + $routeProvider.when('/login', { + templateUrl: 'login.html', + controller: 'LoginCtrl' + }); + + // default is the admin overview + $routeProvider.otherwise({ + redirectTo: '/user' + }); + } +]) +// disable the X-Requested-With header +.config(['$httpProvider', function($httpProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + } +]) +.config(['$locationProvider', + function($locationProvider) { + $locationProvider.html5Mode(false); + } +]) +.factory('notyService', function($rootScope) { + var notyService = {}; + + function showNoty (msg, type, timeout) { + timeout = timeout || 5000; + var n = noty({ + text: msg, + layout: 'topCenter', + timeout: 5000, + type: 'error' + }); + } + + $rootScope.$on('$routeChangeStart', function() { + $.noty.clearQueue(); + $.noty.closeAll(); + }); + notyService.error = function (msg, timeout) { + return showNoty(msg, 'error', timeout); + }; + notyService.warn = function (msg, timeout) { + return showNoty(msg, 'warn', timeout); + }; + + return notyService; +}) + +.run(['$rootScope', '$modal', '$route', '$q', function($rootScope, $modal, $route, $q) { + var backend = { + ajax: function(rel_url, options) { + var backend_url = '/'; + + if (options === undefined) + options = {}; + + var abs_url = backend_url + rel_url; + options.beforeSend = function(request) { + request.setRequestHeader('Authority', admin_pw); + }; + var ret = $.ajax(abs_url, options); + + ret.fail(function(jqxhr, textStatus) { + if (jqxhr.status === 401) { + $('#loginModal').modal(); + $('#loginModal').find(':password').focus(); + } + }); + return ret; + } + }; + $rootScope.backend = backend; +}]); diff --git a/Rainy.UI/app_admin/js/admin/controller/alluser_ctrl.js b/Rainy.UI/app_admin/js/admin/controller/alluser_ctrl.js new file mode 100755 index 0000000..8980d00 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/controller/alluser_ctrl.js @@ -0,0 +1,79 @@ + +function AllUserCtrl($scope, $route) { + $scope.currently_edited_user = null; + $scope.new_user = {}; + + /*$scope.sendMail = true; + $scope.sendMailDisabled = true; + $scope.$watch('new_user.Password', function() { + if ($scope.new_user.Password === undefined) { + $scope.sendMail = true; + $scope.sendMailDisabled = true; + } else { + $scope.sendMailDisabled = false; + } + });*/ + + $scope.reload_user_list = function() { + $scope.backend.ajax('api/admin/alluser/').success(function(data) { + $scope.alluser = data; + $scope.$apply(); + }); + }; + $scope.reload_user_list(); + + $scope.start_edit = function(user) { + $scope.currently_edited_user = jQuery.extend(true, {}, user); + $scope.currently_edited_user.Password = ''; + }; + $scope.stop_edit = function() { + $scope.currently_edited_user = null; + }; + + $scope.save_user = function(is_new) { + var ajax_req; + $scope.new_user.IsActivated = true; + $scope.new_user.IsVerified = true; + if(is_new === true) { + ajax_req = $scope.backend.ajax('api/admin/user/', { + data: JSON.stringify($scope.new_user), + type:'POST', + contentType:'application/json; charset=utf-8', + dataType:'json' + }); + } else { + // update user is done via PUT request + ajax_req = $scope.backend.ajax('api/admin/user/', { + data: JSON.stringify($scope.currently_edited_user), + type:'PUT', + contentType:'application/json; charset=utf-8', + dataType:'json' + }); + } + ajax_req.done(function() { + if (is_new === true) { + $scope.new_user = null; + } else { + $scope.stop_edit(); + } + $scope.reload_user_list(); + $('#inputUsername').focus(); + }); + }; + + $scope.delete_user = function(user, $event) { + $event.stopPropagation(); + /* global confirm: true */ + if(!confirm('Really delete user \'' + user.Username + '\' ?')) { + return; + } + $scope.backend.ajax('api/admin/user/' + user.Username, { + type:'DELETE', + data: JSON.stringify(user), + contentType:'application/json; charset=utf-8', + dataType:'json' + }).done(function() { + $scope.reload_user_list(); + }); + }; +} diff --git a/Rainy.UI/app_admin/js/admin/controller/auth_ctrl.js b/Rainy.UI/app_admin/js/admin/controller/auth_ctrl.js new file mode 100755 index 0000000..81063e5 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/controller/auth_ctrl.js @@ -0,0 +1,32 @@ +/*global admin_pw:true*/ +var admin_pw=''; +function AuthCtrl($scope, $route, $location) { + + var url_pw = ($location.search()).password; + if (url_pw !== undefined && url_pw.length > 0) { + // new admin pw, update teh cookie + admin_pw = url_pw; + } else if (!$location.path().startsWith('/login')) { + $('#loginModal').modal(); + $('#loginModal').find(':password').focus(); + } + + $scope.doLogin = function() { + // test request to the server + // check if pw was correct by + // doing dummy request + admin_pw = $scope.adminPassword; + + $scope.backend.ajax('api/admin/status/') + .success (function() { + $('#loginModal').modal('hide'); + admin_pw = $scope.adminPassword; + }).fail(function () { + $scope.adminPassword=''; + $('#loginModal').find(':password').focus(); + $scope.$apply(); + }); + $route.reload(); + }; +} +AuthCtrl.$inject = [ '$scope','$route', '$location' ]; diff --git a/Rainy.UI/app_admin/js/admin/controller/login_ctrl.js b/Rainy.UI/app_admin/js/admin/controller/login_ctrl.js new file mode 100755 index 0000000..877f446 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/controller/login_ctrl.js @@ -0,0 +1,34 @@ + +function LoginCtrl($scope, $rootScope, $http, notyService) { + + $scope.getUrlVars = function() { + var vars = [], hash; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for(var i = 0; i < hashes.length; i++) + { + hash = hashes[i].split('='); + vars.push(hash[0]); + vars[hash[0]] = hash[1]; + } + return vars; + }; + + var url_vars = $scope.getUrlVars(); + + $scope.authData = { Username: '', Password: '', RequestToken: '' }; + $scope.authData.RequestToken = url_vars['oauth_token']; + + $scope.doLogin = function () { + $http.post('/oauth/authenticate', $scope.authData) + .success(function (data, status, headers, config) { + window.document.location = data.RedirectUrl; + }) + .error(function (data, status, headers, config) { + if (status === 412) + notyService.error('Login failed. User ' + $scope.authData.Username + ' requires activation by an admin (Moderation is enabled)'); + else + notyService.error('Login failed. Check username and password'); + }); + }; +} +//LoginCtrl.$inject = [ '$scope','$http' ]; diff --git a/Rainy.UI/app_admin/js/admin/controller/main_ctrl.js b/Rainy.UI/app_admin/js/admin/controller/main_ctrl.js new file mode 100644 index 0000000..67cfb1c --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/controller/main_ctrl.js @@ -0,0 +1,19 @@ +function MainCtrl($scope, $routeParams, $route, $location) { + + $scope.checkLocation = function() { + if (!$location.path().startsWith('/login')) { + $scope.hideAdminNav = false; + $scope.dontAskForPassword = false; + } else { + $scope.hideAdminNav = true; + $scope.dontAskForPassword = true; + } + }; + $scope.checkLocation(); + + // bug in angular prevents this from firing when the back button is used + // (fixed in 1.1.5) - see https://github.com/angular/angular.js/pull/2206 + $scope.$on('$locationChangeStart', function(ev, oldloc, newloc) { + $scope.checkLocation(); + }); +} diff --git a/Rainy.UI/app_admin/js/admin/controller/status_ctrl.js b/Rainy.UI/app_admin/js/admin/controller/status_ctrl.js new file mode 100755 index 0000000..57437d8 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/controller/status_ctrl.js @@ -0,0 +1,22 @@ +function StatusCtrl($scope, $http, $route) { + $scope.serverStatus = {}; + + $scope.getStatus = function () { + $scope.backend.ajax('api/admin/status/') + .success(function(data) { + $scope.serverStatus = data; + + var today = new Date(); + var then = new Date(data.Uptime); + var dt = today - then; + + $scope.upSinceDays = Math.round(dt / 86400000); // days + $scope.upSinceHours = Math.round((dt % 86400000) / 3600000); // hours + $scope.upSinceMinutes = Math.round(((dt % 86400000) % 3600000) / 60000); // minutes + + $scope.$apply(); + }); + }(); + +} +StatusCtrl.$inject = [ '$scope', '$http', '$route' ]; diff --git a/Rainy.UI/app_admin/js/admin/directives.js b/Rainy.UI/app_admin/js/admin/directives.js new file mode 100755 index 0000000..f59acc0 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/directives.js @@ -0,0 +1,17 @@ +/*global $:false */ +/*global angular:false */ +angular.module('myApp.directives', []) + .directive('appVersion', ['version', + function(version) { + return function(scope, elm, attrs) { + elm.text(version); + }; + } + ]) +; + +if (typeof String.prototype.startsWith !== 'function') { + String.prototype.startsWith = function(str) { + return this.slice(0, str.length) === str; + }; +} diff --git a/Rainy.UI/app_admin/js/admin/filters.js b/Rainy.UI/app_admin/js/admin/filters.js new file mode 100755 index 0000000..94ff798 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/filters.js @@ -0,0 +1,9 @@ +/*global $:false */ +/*global angular:false */ + +angular.module('myApp.filters', []). + filter('interpolate', ['version', function(version) { + return function(text) { + return String(text).replace(/\%VERSION\%/mg, version); + }; +}]); diff --git a/Rainy.UI/app_admin/js/admin/services.js b/Rainy.UI/app_admin/js/admin/services.js new file mode 100755 index 0000000..ef67cf4 --- /dev/null +++ b/Rainy.UI/app_admin/js/admin/services.js @@ -0,0 +1,12 @@ +/*global $:false */ +/*global angular:false */ +/*global myModule:true*/ + +/* Services */ + + +// Demonstrate how to register services +// In this case it is a simple value service. +angular.module('myApp.services', []) + .value('version', '0.1'); + diff --git a/Rainy.UI/app_admin/js/client/app_client.js b/Rainy.UI/app_admin/js/client/app_client.js new file mode 100644 index 0000000..21050a5 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/app_client.js @@ -0,0 +1,127 @@ +// Declare app level module which depends on filters, and services +var app = angular.module('clientApp', [ + 'clientApp.filters', + 'clientApp.services', + 'clientApp.directives', + + // anguar-strap.js + '$strap.directives' +]) +.config(['$routeProvider', + function($routeProvider) { + // login page + $routeProvider.when('/login', { + templateUrl: 'login.html', + controller: 'LoginCtrl' + }); + + $routeProvider.when('/notes/:guid', { + templateUrl: 'notes.html', + controller: 'NoteCtrl' + }); + $routeProvider.when('/logout', { + template: '
', + controller: 'LogoutCtrl' + }); + $routeProvider.when('/signup', { + templateUrl: 'signup.html', + controller: 'SignupCtrl' + }); + + $routeProvider.otherwise({ + redirectTo: '/login' + }); + } +]) +// disable the X-Requested-With header +.config(['$httpProvider', function($httpProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + } +]) +.config(['$locationProvider', + function($locationProvider) { + $locationProvider.html5Mode(false); + } +]); + +// register the interceptor as a service +app.factory('loginInterceptor', function($q, $location) { + return function(promise) { + return promise.then(function(response) { + // do something on success + return response; + }, function(response) { + // do something on error + if (response.status === 401) { + if (window.localStorage) + window.localStorage.removeItem('accessToken'); + $location.path('/login'); + } + return $q.reject(response); + }); + }; +}); +app.config(['$httpProvider', function($httpProvider) { + $httpProvider.responseInterceptors.push('loginInterceptor'); + } +]); + + +// FILTERS +angular.module('clientApp.filters', []) + .filter('interpolate', ['version', function(version) { + return function(text) { + return String(text).replace(/\%VERSION\%/mg, version); + }; + }]); + +// SERVICES +angular.module('clientApp.services', []) + .value('version', '0.1'); + + +// DIRECTIVES +angular.module('clientApp.directives', []) + .directive('appVersion', ['version', + function(version) { + return function(scope, elm, attrs) { + elm.text(version); + }; + } + ]) + .directive('uniqueUsername', ['$http', function ($http) { + return { + require:'ngModel', + restrict: 'A', + link:function (scope, el, attrs, ctrl) { + + var check_username_avail = _.debounce(function (username) { + $http.get('/api/user/signup/check_username/' + username) + .success(function (data) { + console.log(data); + if (data.Available === true) { + ctrl.$setValidity('username_avail', true); + //scope.username = data.Username; + } + else + ctrl.$setValidity('username_avail', false); + }); + }, 800); + + // push to the end of all other validity parsers + ctrl.$parsers.push(function (viewValue) { + if (viewValue) { + check_username_avail(viewValue); + return viewValue; + } + }); + } + }; + }]) +; + +if (typeof String.prototype.startsWith !== 'function') { + String.prototype.startsWith = function(str) { + return this.slice(0, str.length) === str; + }; +} diff --git a/Rainy.UI/app_admin/js/client/controller/login_ctrl.js b/Rainy.UI/app_admin/js/client/controller/login_ctrl.js new file mode 100644 index 0000000..e860765 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/controller/login_ctrl.js @@ -0,0 +1,53 @@ +function LoginCtrl($scope, $location, $rootScope, + loginService, notyService, configService) { + + $scope.username = ''; + $scope.password = ''; + $scope.rememberMe = false; + $scope.isTestServer = $location.host() === 'testserver.notesync.org'; + + $scope.serverConfig = configService.serverConfig; + + if (loginService.userIsLoggedIn()) { + $location.path('/notes/'); + } + + var useStorage = window.localStorage && window.sessionStorage; + if (useStorage) { + $scope.username = window.sessionStorage.getItem('username'); + + $scope.$watch('username', function (newval, oldval) { + if (!newval) + window.sessionStorage.removeItem('username'); + else + window.sessionStorage.setItem('username', newval); + }); + + + $scope.rememberMe = window.sessionStorage.getItem('rememberMe') === 'true'; + $scope.$watch('rememberMe', function (newval, oldval) { + window.sessionStorage.setItem('rememberMe', newval); + }); + } + + if (!$scope.username || $scope.username.length === 0) + $('#inputUsername').focus(); + else + $('#inputPassword').focus(); + + $scope.doLogin = function () { + var remember = $scope.rememberMe; + + loginService.login($scope.username, $scope.password, remember) + .then(function () { + $location.path('/notes/'); + }, function (error_status) { + console.log(error_status); + if (error_status === 412) + notyService.error('Login failed. User ' + $scope.username + ' requires activation by an admin (moderation is enabled)'); + else + notyService.error('Login failed. Check username and password.'); + }); + }; +} +//LoginCtrl.$inject = [ '$scope','$http' ]; diff --git a/Rainy.UI/app_admin/js/client/controller/logout_ctrl.js b/Rainy.UI/app_admin/js/client/controller/logout_ctrl.js new file mode 100644 index 0000000..7bf059f --- /dev/null +++ b/Rainy.UI/app_admin/js/client/controller/logout_ctrl.js @@ -0,0 +1,5 @@ +function LogoutCtrl($location, loginService) { + + loginService.logout(); + $location.path('/login/'); +} diff --git a/Rainy.UI/app_admin/js/client/controller/main_ctrl.js b/Rainy.UI/app_admin/js/client/controller/main_ctrl.js new file mode 100644 index 0000000..49a7177 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/controller/main_ctrl.js @@ -0,0 +1,7 @@ +function MainCtrl ($scope, loginService) { + $scope.isLoggedIn = loginService.userIsLoggedIn(); + + $scope.$on('loginStatus', function(ev, isLoggedIn) { + $scope.isLoggedIn = isLoggedIn; + }); +} \ No newline at end of file diff --git a/Rainy.UI/app_admin/js/client/controller/notes_ctrl.js b/Rainy.UI/app_admin/js/client/controller/notes_ctrl.js new file mode 100644 index 0000000..251f8e4 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/controller/notes_ctrl.js @@ -0,0 +1,132 @@ +function NoteCtrl($scope,$location, $routeParams, $timeout, $q, $rootScope, + noteService, loginService, notyService, configService) { + + $scope.notebooks = {}; + $scope.notes = []; + $scope.noteService = noteService; + $scope.username = loginService.username; + $scope.enableSyncButton = false; + $scope.config = configService.serverConfig; + + noteService.fetchNotes(); + + // deep watching, will get triggered if a note content's changes, too + $scope.$watch('noteService.notes', function (newval, oldval) { + if (newval && newval.length === 0) return; + + if (oldval && oldval.length === 0 && newval && newval.length > 0) { + // first time the notes become ready + } + + $scope.notebooks = noteService.notebooks; + $scope.notes = newval; + + loadNote(); + + }, true); + + + var initialAutosyncSeconds = 300; + function startAutosyncTimer () { + $timeout.cancel($rootScope.timer_dfd); + $rootScope.autosyncSeconds = initialAutosyncSeconds; + $scope.enableSyncButton = true; + $rootScope.timer_dfd = $timeout(function autosync(){ + if ($rootScope.autosyncSeconds % 10 === 0) + console.log('next sync in: ' + $rootScope.autosyncSeconds + ' seconds'); + if ($rootScope.autosyncSeconds <= 0) { + $scope.sync(); + return; + } + else { + $rootScope.autosyncSeconds--; + $rootScope.timer_dfd = $timeout(autosync, 1000); + } + }, 1000); + } + function stopAutosyncTimer () { + $rootScope.autosyncSeconds = initialAutosyncSeconds; + $timeout.cancel($rootScope.timer_dfd); + $scope.enableSyncButton = false; + } + + function setSyncButtonTooltip () { + // we need to recreate the tooltip every mouseover due to bootstrap internals + $('#sync_btn').mouseenter(function() { + var caption = 'Next autosync in ' + $rootScope.autosyncSeconds + ' seconds or press to perform manual sync'; + $('#sync_btn').data('tooltip', false); + if ($scope.enableSyncButton) { + $('#sync_btn').tooltip({ title: caption }); + $('#sync_btn').tooltip('show'); + } + }); + } + setSyncButtonTooltip (); + + function setWindowCloseMessage () { + window.onbeforeunload = function () { + if ($scope.enableSyncButton) { + return 'There are unsaved notes, please push the synchronize button!'; + } else { + } + }; + } + setWindowCloseMessage(); + + + $scope.$watch('noteService.needsSyncing', function (newval, oldval) { + $scope.enableSyncButton = newval; + if (newval === true && oldval === false) { + startAutosyncTimer(); + } + }); + + function loadNote () { + var guid = $routeParams.guid; + + if (!guid) return; + var n = noteService.getNoteByGuid(guid); + if (!n) return; + if ($scope.selectedNote && n.guid === $scope.selectedNote.guid) return; + $scope.selectedNote = n; + $scope.setWysiText(n['note-content']); + } + + $scope.onNoteChange = function (note) { + noteService.markAsTainted(note); + }; + + $scope.selectNote = function (note) { + var guid = note.guid; + $location.path('/notes/' + guid); + }; + + $scope.sync = function () { + if ($scope.enableSyncButton === false) + return; + + $('#sync_btn').tooltip('hide'); + stopAutosyncTimer(); + $scope.flushWysi(); + // HACK we give 100ms before we sync to wait for the editor to flush + $timeout(function () { + noteService.uploadChanges().then(function (note_changes) { + notyService.success('Successfully synced ' + note_changes.length + ' notes.', 2000); + },function () { + notyService.error('Error occured during syncing!'); + }); + + }, 100); + }; + + $scope.deleteNote = function () { + noteService.deleteNote($scope.selectedNote); + $location.path('/notes/'); + }; + + $scope.newNote = function () { + var note = noteService.newNote(); + noteService.markAsTainted(note); + $scope.selectNote(note); + }; +} diff --git a/Rainy.UI/app_admin/js/client/controller/signup_ctrl.js b/Rainy.UI/app_admin/js/client/controller/signup_ctrl.js new file mode 100644 index 0000000..8aba32d --- /dev/null +++ b/Rainy.UI/app_admin/js/client/controller/signup_ctrl.js @@ -0,0 +1,48 @@ +function SignupCtrl($scope, $location, $http, $timeout, notyService) { + $scope.username = ''; + $scope.password1 = ''; + $scope.password2 = ''; + $scope.email = ''; + $scope.toc = false; + + $scope.$watch(combinedPassword, function (newval, oldval) { + if (newval === oldval) + return; + passwordMatch(); + }); + + $scope.$watch('toc', function (newval) { + if (newval === true) + $scope.formSignup.$setValidity('toc', true); + else + $scope.formSignup.$setValidity('toc', false); + }); + + function combinedPassword () { + return $scope.password1 + ' ' + $scope.password2; + } + + function passwordMatch () { + if ($scope.password1 === $scope.password2) + $scope.formSignup.$setValidity('passwdmatch', true); + else + $scope.formSignup.$setValidity('passwdmatch', false); + } + + $scope.signUp = function () { + var new_user = { + Username: $scope.username, + Password: $scope.password1, + EmailAddress: $scope.email + }; + $http.post('/api/user/signup/new/', new_user).success(function (data) { + window.sessionStorage.setItem('username', $scope.username); + notyService.success('Signup successfull! You will be redirected to the login page...'); + $timeout(function () { + $location.path('#/login/'); + }, 2000); + }).error(function (data, status, headers, config) { + notyService.error('ERROR: ' + status); + }); + }; +} diff --git a/Rainy.UI/app_admin/js/client/loginService.js b/Rainy.UI/app_admin/js/client/loginService.js new file mode 100644 index 0000000..b3fd843 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/loginService.js @@ -0,0 +1,71 @@ +app.factory('loginService', function($q, $http, $rootScope) { + var loginService = { + username: '', + accessToken: '' + }; + + var useStorage = window.sessionStorage && window.localStorage; + if (useStorage) { + loginService.accessToken = window.localStorage.getItem('accessToken'); + loginService.username = window.localStorage.getItem('username'); + } + + loginService.login = function (user, pass, remember) { + var deferred = $q.defer(); + var expiry = 1440; // 1d + + if (remember) + expiry = 14 * 1440; // 14d + + var credentials = { + Username: user, + Password: pass, + Expiry: expiry + }; + + $http.post('/oauth/temporary_access_token', credentials) + .success(function (data, status, headers, config) { + loginService.accessToken = data.AccessToken; + loginService.username = user; + + if (useStorage && remember) { + window.localStorage.setItem('username', user); + window.localStorage.setItem('accessToken', data.AccessToken); + } + $rootScope.$broadcast('loginStatus', true); + deferred.resolve(); + }) + .error(function (data, status) { + deferred.reject(status); + $rootScope.$broadcast('loginStatus', false); + }); + return deferred.promise; + }; + + loginService.logout = function () { + loginService.accessToken = ''; + loginService.username = ''; + + if (useStorage) { + window.localStorage.removeItem('accessToken'); + window.sessionStorage.removeItem('accessToken'); + } + $rootScope.$broadcast('loginStatus', false); + }; + + loginService.userIsLoggedIn = function () { + // TODO check for expiry + var logged = !(loginService.accessToken === '' || + loginService.accessToken === undefined); + + if (useStorage && logged) { + var ret = window.localStorage.getItem('accessToken') && true; + return ret; + } + else return logged; + }; + + loginService.isLoggedIn = loginService.userIsLoggedIn(); + + return loginService; +}); diff --git a/Rainy.UI/app_admin/js/client/noteService.js b/Rainy.UI/app_admin/js/client/noteService.js new file mode 100644 index 0000000..b9df252 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/noteService.js @@ -0,0 +1,208 @@ +app.factory('noteService', function($http, $rootScope, $q, loginService) { + + var noteService = {}; + var notes, latest_sync_revision, manifest; + + function initialize () { + notes = null; + + latest_sync_revision = 0; + manifest = { + taintedNotes: [], + deletedNotes: [], + }; + } + + initialize(); + + Object.defineProperty(noteService, 'notebooks', { + get: function () { + return buildNotebooks(notes); + } + }); + + Object.defineProperty(noteService, 'notes', { + get: function () { + return filterDeletedNotes(notes); + } + }); + + Object.defineProperty(noteService, 'needsSyncing', { + get: function () { + return manifest.taintedNotes.length > 0 || manifest.deletedNotes.length > 0; + } + }); + + $rootScope.$on('loginStatus', function(ev, isLoggedIn) { + if (!isLoggedIn) { + console.log('cleaning note service'); + initialize(); + } + }); + + function getNotebookFromNote (note) { + var nb_name = null; + _.each(note.tags, function (tag) { + if (tag.startsWith('system:notebook:')) { + nb_name = tag.substring(16); + } + }); + return nb_name; + } + + function notesByNotebook (notes, notebook_name) { + if (notebook_name) { + return _.filter(notes, function (note) { + var nb = getNotebookFromNote(note); + return nb === notebook_name; + }); + } else { + // return notes that don't have a notebook + return _.filter(notes, function (note) { + return getNotebookFromNote(note) === null; + }); + } + } + + function buildNotebooks (notes) { + var notebooks = {}; + var notebook_names = []; + + notebooks.Unsorted = notesByNotebook(notes); + + _.each(notes, function (note) { + var nb = getNotebookFromNote (note); + if (nb) + notebook_names.push(nb); + }); + notebook_names = _.uniq(notebook_names); + + _.each(notebook_names, function(name) { + notebooks[name] = notesByNotebook(notes, name); + }); + + // filter out notes marked as deleted & empty notebooks + var filtered_nb = {}; + for (var nb in notebooks) { + var filtered = filterDeletedNotes(notebooks[nb]); + if (filtered.length > 0) + filtered_nb[nb] = filtered; + } + + return filtered_nb; + } + + function filterDeletedNotes(notes) { + var filtered = _.filter(notes, function(note) { + return !_.contains(manifest.deletedNotes, note.guid); + }); + return filtered; + } + + function guid () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } + + // PUBLIC functions + // + noteService.getNoteByGuid = function (guid) { + if (noteService.notes.length === 0) + return null; + return _.findWhere(notes, {guid: guid}); + }; + + noteService.fetchNotes = function(force) { + + if (notes !== null && !force) return; + + manifest.taintedNotes = []; + manifest.deletedNotes = []; + + $http({ + method: 'GET', + url: '/api/1.0/' + loginService.username + '/notes?include_notes=true¬es_as_html=true', + headers: { 'AccessToken': loginService.accessToken } + }).success(function (data, status, headers, config) { + notes = data.notes; + latest_sync_revision = data['latest-sync-revision']; + }).error(function () { + // console.log('fail'); + }); + }; + + noteService.uploadChanges = function () { + var dfd_complete = $q.defer(); + var note_changes = []; + _.each(manifest.taintedNotes, function(guid) { + var n = noteService.getNoteByGuid(guid); + note_changes.push(n); + }); + _.each(manifest.deletedNotes, function(guid) { + var n = noteService.getNoteByGuid(guid); + n.command = 'delete'; + note_changes.push(n); + }); + + if (note_changes.length > 0) { + latest_sync_revision++; + var req = { + 'latest-sync-revision': latest_sync_revision, + }; + req['note-changes'] = note_changes; + + console.log(req); + + $http({ + method: 'PUT', + url: '/api/1.0/' + loginService.username + '/notes?notes_as_html=true', + headers: { 'AccessToken': loginService.accessToken }, + data: req + }).success(function (data, status, headers, config) { + console.log('successfully synced'); + noteService.fetchNotes(true); + dfd_complete.resolve(note_changes); + }).error(function () { + dfd_complete.reject(); + }); + } else { + console.log ('no changes, not syncing'); + dfd_complete.resolve(); + } + return dfd_complete.promise; + }; + + noteService.deleteNote = function (note) { + if (!_.contains(manifest.deletedNotes, note)) { + manifest.deletedNotes.push(note.guid); + } + }; + + noteService.newNote = function (initial_note) { + var proto = {}; + proto.title = 'New note'; + proto['note-content'] = 'Enter your note.'; + proto['create-date'] = new Date().toISOString(); + proto.guid = guid(); + proto.tags = []; + + var note = $.extend(proto, initial_note); + notes.push(note); + return note; + }; + + noteService.markAsTainted = function (note) { + var now = new Date().toISOString(); + note['last-change-date'] = now; + note['last-metadata-change-date'] = now; + + if (!_.contains(manifest.taintedNotes, note.guid)) { + console.log('marking note ' + note.guid + ' as tainted'); + manifest.taintedNotes.push(note.guid); + } + }; + + return noteService; +}); diff --git a/Rainy.UI/app_admin/js/client/wysiwygDirective.js b/Rainy.UI/app_admin/js/client/wysiwygDirective.js new file mode 100644 index 0000000..d098058 --- /dev/null +++ b/Rainy.UI/app_admin/js/client/wysiwygDirective.js @@ -0,0 +1,200 @@ +app.directive('wysiwyg', ['$q', function($q){ + + var setupWysiwyg = function (tElement, scope) { + var parserRules = { + classes: { + // for urls + 'internal': 1, + 'url': 1, + 'title': 0 + }, + tags: { + 'div': { + 'remove': 1 + }, + 'strike': {}, + 'del': {}, + 'span': {}, + 'small': {}, + 'i': {}, + 'a': {}, + 'b': {}, + 'br': {}, + 'li': {}, + 'ul': {}, + 'h1': {}, + 'h2': {} + } + }; + + tElement.wysihtml5({ + html: false, + link: false, + image: false, + color: false, + // HACK this identity function prevents the wysi to sometimes remove valid + // tags from the editor; see http://stackoverflow.com/a/17656572/777928 + // this renders our parser rules obsolete + parser: function (html) { return html; }, + parserRules: parserRules, + stylesheets: ['wysihtml5_style.css'], + events: { + change: function () { + /* the change event prevents events, like links clicks :( */ + scope.updateWysiText(); + }, + blur: function() { + scope.updateWysiText(); + }, + load: function () { + var $doc = $(scope.wysiEditor.composer.doc); + $doc.keyup(function (evt) { + scope.updateWysiText(); + }); + $doc.keydown(function (evt) { + scope.updateWysiText(); + }); + } + } + }); + // HACK we modify/remove the bootstrap-wysihtml5 elements from the bar + $('[data-wysihtml5-command=insertOrderedList]').remove(); + $('[data-wysihtml5-command=Outdent]').remove(); + $('[data-wysihtml5-command=Indent]').remove(); + + $('[data-wysihtml5-command=underline]').remove(); + + var huge_btn= $('

Huge

'); + $('[data-wysihtml5-command-value=h1]').replaceWith(huge_btn); + + var large_btn= $('

Large

'); + $('[data-wysihtml5-command-value=h2]').replaceWith(large_btn); + + var small_btn = $('Small'); + $('[data-wysihtml5-command-value=h3]').replaceWith(small_btn); + + var strike_btn = $('Strike'); + strike_btn.insertAfter($('[data-wysihtml5-command=italic]')); + + var highlight_btn = $('Highlight'); + highlight_btn.insertAfter(strike_btn); + + var fixed_btn = $('Fixed'); + fixed_btn.insertAfter(strike_btn); + + //$('[data-wysihtml5-command-value=h3]').replaceWith('Small​'); + //$('[data-wysihtml5-command-value=h3]').replaceWith('Small​'); + $('.wysihtml5-toolbar').find('a').click(function () { + scope.updateWysiText(); + }); + }; + + function setupTitleInput (tElement, scope) { + // we don't want line breaks in the title so ignore press of enter key + tElement.keypress(function (e){ + if (e.which === 13) { + return false; + } + }); + // HACK update title on keyup + var titleUpdateFn = function () { + scope.$apply(function() { + var txt = tElement.text(); + if (txt !== '') { + scope.selectedNote.title = txt; + } + }); + }; + tElement.keyup(titleUpdateFn); + } + + function setupChangeListeners (tElement, scope) { + + scope.$watch('selectedNote', function (newval, oldval) { + if (!newval) return; + + if (oldval && newval['note-content'] === oldval['note-content'] && + newval.title === oldval.title) { + // nothing changed + return; + } + if (oldval) { + scope.onNoteChange(newval); + } + }, true); + } + + return { + name: 'wysiwyg', + // priority: 1, + // terminal: true, + + scope: false, + //{} = isolate, true = child, false/undefined = no change + + // controller: function ($scope, $element) {}, + // require: '', // Array = multiple requires, ? = optional, ^ = check parent elements + restrict: 'E', // E = Element, A = Attribute, C = Class, M = Comment + // template: '', + templateUrl: 'wysiwyg.html', + replace: true, + transclude: false, + compile: function compile(tElement, tAttrs, transclude) { + return { + post: function preLink(scope, tElement, iAttrs, controller){ + + wysihtml5.commands.highlight = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, 'span', 'highlight', 'highlight'); + }, + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, 'span', 'highlight', 'highlight'); + } + }; + wysihtml5.commands.fixedwidth = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, 'pre'); + }, + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, 'pre'); + } + }; + + + scope.selectedNote = null; + + var textarea = tElement.find('textarea'); + setupWysiwyg(textarea, scope); + setupTitleInput(tElement.find('h2'), scope); + scope.wysiEditor = textarea.data('wysihtml5').editor; + + scope.updateWysiText = _.throttle(function () { + var newtext = textarea.val(); + scope.$apply(function () { + if (scope.selectedNote) + scope.selectedNote['note-content'] = newtext; + //console.log('text changed: ' + newtext); + }); + }, 800, { leading: false }); + + scope.setWysiText = function (text) { + scope.wysiEditor.setValue(text); + }; + + // HACK we sometimes miss any character for any reason? + // allow explicit flush (i.e. before sync) + scope.flushWysi = function () { + var newtext = textarea.val(); + if (scope.selectedNote) + scope.selectedNote['note-content'] = newtext; + //console.log('flushed text: ' + newtext); + }; + + setupChangeListeners (tElement, scope); + }, + pre: function postLink(scope, tElement, iAttrs, controller){ + } + }; + }, + }; +}]); diff --git a/Rainy.UI/app_admin/js/shared/configService.js b/Rainy.UI/app_admin/js/shared/configService.js new file mode 100644 index 0000000..5f9c15d --- /dev/null +++ b/Rainy.UI/app_admin/js/shared/configService.js @@ -0,0 +1,16 @@ +angular.module('clientApp').factory('configService', function($http) { + var configService = {}; + var conf = {}; + + Object.defineProperty(configService, 'serverConfig', { + get: function () { + return conf; + } + }); + + $http.get('/api/config').success(function (data) { + conf = $.extend(conf, data); + }); + + return configService; +}); diff --git a/Rainy.UI/app_admin/js/shared/notyService.js b/Rainy.UI/app_admin/js/shared/notyService.js new file mode 100644 index 0000000..1943d49 --- /dev/null +++ b/Rainy.UI/app_admin/js/shared/notyService.js @@ -0,0 +1,29 @@ +app.factory('notyService', function($rootScope) { + var notyService = {}; + + function showNoty (msg, type, timeout) { + timeout = timeout || 5000; + var n = noty({ + text: msg, + layout: 'topCenter', + timeout: timeout, + type: type + }); + } + + $rootScope.$on('$routeChangeStart', function() { + $.noty.clearQueue(); + $.noty.closeAll(); + }); + notyService.error = function (msg, timeout) { + return showNoty(msg, 'error', timeout); + }; + notyService.warn = function (msg, timeout) { + return showNoty(msg, 'warn', timeout); + }; + notyService.success = function (msg, timeout) { + return showNoty(msg, 'success', timeout); + }; + + return notyService; +}); diff --git a/Rainy.UI/bower.json b/Rainy.UI/bower.json new file mode 100644 index 0000000..ad9ab13 --- /dev/null +++ b/Rainy.UI/bower.json @@ -0,0 +1,17 @@ +{ + "name": "Rainy-UI", + "version": "0.0.1", + "devDependencies": { + "jquery": "~2.0.3", + "angular-strap": "~0.7.5", + "angular": "~1.0.7", + "bootstrap.zip": "http://getbootstrap.com/2.3.2/assets/bootstrap.zip", + "bootswatch-united": "http://netdna.bootstrapcdn.com/bootswatch/2.3.2/united/bootstrap.min.css", + "underscore": "~1.5.1" + }, + "dependencies": { + "noty": "~2.1.0", + "wysihtml5": "~0.3.0", + "bootstrap-wysihtml5": "*" + } +} diff --git a/Rainy.UI/dist/admin/admin.js b/Rainy.UI/dist/admin/admin.js new file mode 100644 index 0000000..bbfd579 --- /dev/null +++ b/Rainy.UI/dist/admin/admin.js @@ -0,0 +1,327 @@ +// Declare app level module which depends on filters, and services +var app = angular.module('myApp', [ + 'myApp.filters', + 'myApp.services', + 'myApp.directives', + + // anguar-strap.js + '$strap.directives' +]) +.config(['$routeProvider', + function($routeProvider) { + // admin interface + $routeProvider.when('/user', { + templateUrl: 'user.html', + controller: 'AllUserCtrl' + }); + $routeProvider.when('/overview', { + templateUrl: 'overview.html', + controller: 'StatusCtrl' + }); + + // login page for OAUTH + $routeProvider.when('/login', { + templateUrl: 'login.html', + controller: 'LoginCtrl' + }); + + // default is the admin overview + $routeProvider.otherwise({ + redirectTo: '/user' + }); + } +]) +// disable the X-Requested-With header +.config(['$httpProvider', function($httpProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + } +]) +.config(['$locationProvider', + function($locationProvider) { + $locationProvider.html5Mode(false); + } +]) +.factory('notyService', function($rootScope) { + var notyService = {}; + + function showNoty (msg, type, timeout) { + timeout = timeout || 5000; + var n = noty({ + text: msg, + layout: 'topCenter', + timeout: 5000, + type: 'error' + }); + } + + $rootScope.$on('$routeChangeStart', function() { + $.noty.clearQueue(); + $.noty.closeAll(); + }); + notyService.error = function (msg, timeout) { + return showNoty(msg, 'error', timeout); + }; + notyService.warn = function (msg, timeout) { + return showNoty(msg, 'warn', timeout); + }; + + return notyService; +}) + +.run(['$rootScope', '$modal', '$route', '$q', function($rootScope, $modal, $route, $q) { + var backend = { + ajax: function(rel_url, options) { + var backend_url = '/'; + + if (options === undefined) + options = {}; + + var abs_url = backend_url + rel_url; + options.beforeSend = function(request) { + request.setRequestHeader('Authority', admin_pw); + }; + var ret = $.ajax(abs_url, options); + + ret.fail(function(jqxhr, textStatus) { + if (jqxhr.status === 401) { + $('#loginModal').modal(); + $('#loginModal').find(':password').focus(); + } + }); + return ret; + } + }; + $rootScope.backend = backend; +}]); + + +function AllUserCtrl($scope, $route) { + $scope.currently_edited_user = null; + $scope.new_user = {}; + + /*$scope.sendMail = true; + $scope.sendMailDisabled = true; + $scope.$watch('new_user.Password', function() { + if ($scope.new_user.Password === undefined) { + $scope.sendMail = true; + $scope.sendMailDisabled = true; + } else { + $scope.sendMailDisabled = false; + } + });*/ + + $scope.reload_user_list = function() { + $scope.backend.ajax('api/admin/alluser/').success(function(data) { + $scope.alluser = data; + $scope.$apply(); + }); + }; + $scope.reload_user_list(); + + $scope.start_edit = function(user) { + $scope.currently_edited_user = jQuery.extend(true, {}, user); + $scope.currently_edited_user.Password = ''; + }; + $scope.stop_edit = function() { + $scope.currently_edited_user = null; + }; + + $scope.save_user = function(is_new) { + var ajax_req; + $scope.new_user.IsActivated = true; + $scope.new_user.IsVerified = true; + if(is_new === true) { + ajax_req = $scope.backend.ajax('api/admin/user/', { + data: JSON.stringify($scope.new_user), + type:'POST', + contentType:'application/json; charset=utf-8', + dataType:'json' + }); + } else { + // update user is done via PUT request + ajax_req = $scope.backend.ajax('api/admin/user/', { + data: JSON.stringify($scope.currently_edited_user), + type:'PUT', + contentType:'application/json; charset=utf-8', + dataType:'json' + }); + } + ajax_req.done(function() { + if (is_new === true) { + $scope.new_user = null; + } else { + $scope.stop_edit(); + } + $scope.reload_user_list(); + $('#inputUsername').focus(); + }); + }; + + $scope.delete_user = function(user, $event) { + $event.stopPropagation(); + /* global confirm: true */ + if(!confirm('Really delete user \'' + user.Username + '\' ?')) { + return; + } + $scope.backend.ajax('api/admin/user/' + user.Username, { + type:'DELETE', + data: JSON.stringify(user), + contentType:'application/json; charset=utf-8', + dataType:'json' + }).done(function() { + $scope.reload_user_list(); + }); + }; +} + +/*global admin_pw:true*/ +var admin_pw=''; +function AuthCtrl($scope, $route, $location) { + + var url_pw = ($location.search()).password; + if (url_pw !== undefined && url_pw.length > 0) { + // new admin pw, update teh cookie + admin_pw = url_pw; + } else if (!$location.path().startsWith('/login')) { + $('#loginModal').modal(); + $('#loginModal').find(':password').focus(); + } + + $scope.doLogin = function() { + // test request to the server + // check if pw was correct by + // doing dummy request + admin_pw = $scope.adminPassword; + + $scope.backend.ajax('api/admin/status/') + .success (function() { + $('#loginModal').modal('hide'); + admin_pw = $scope.adminPassword; + }).fail(function () { + $scope.adminPassword=''; + $('#loginModal').find(':password').focus(); + $scope.$apply(); + }); + $route.reload(); + }; +} +AuthCtrl.$inject = [ '$scope','$route', '$location' ]; + + +function LoginCtrl($scope, $rootScope, $http, notyService) { + + $scope.getUrlVars = function() { + var vars = [], hash; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for(var i = 0; i < hashes.length; i++) + { + hash = hashes[i].split('='); + vars.push(hash[0]); + vars[hash[0]] = hash[1]; + } + return vars; + }; + + var url_vars = $scope.getUrlVars(); + + $scope.authData = { Username: '', Password: '', RequestToken: '' }; + $scope.authData.RequestToken = url_vars['oauth_token']; + + $scope.doLogin = function () { + $http.post('/oauth/authenticate', $scope.authData) + .success(function (data, status, headers, config) { + window.document.location = data.RedirectUrl; + }) + .error(function (data, status, headers, config) { + if (status === 412) + notyService.error('Login failed. User ' + $scope.authData.Username + ' requires activation by an admin (Moderation is enabled)'); + else + notyService.error('Login failed. Check username and password'); + }); + }; +} +//LoginCtrl.$inject = [ '$scope','$http' ]; + +function MainCtrl($scope, $routeParams, $route, $location) { + + $scope.checkLocation = function() { + if (!$location.path().startsWith('/login')) { + $scope.hideAdminNav = false; + $scope.dontAskForPassword = false; + } else { + $scope.hideAdminNav = true; + $scope.dontAskForPassword = true; + } + }; + $scope.checkLocation(); + + // bug in angular prevents this from firing when the back button is used + // (fixed in 1.1.5) - see https://github.com/angular/angular.js/pull/2206 + $scope.$on('$locationChangeStart', function(ev, oldloc, newloc) { + $scope.checkLocation(); + }); +} + +function StatusCtrl($scope, $http, $route) { + $scope.serverStatus = {}; + + $scope.getStatus = function () { + $scope.backend.ajax('api/admin/status/') + .success(function(data) { + $scope.serverStatus = data; + + var today = new Date(); + var then = new Date(data.Uptime); + var dt = today - then; + + $scope.upSinceDays = Math.round(dt / 86400000); // days + $scope.upSinceHours = Math.round((dt % 86400000) / 3600000); // hours + $scope.upSinceMinutes = Math.round(((dt % 86400000) % 3600000) / 60000); // minutes + + $scope.$apply(); + }); + }(); + +} +StatusCtrl.$inject = [ '$scope', '$http', '$route' ]; + +/*global $:false */ +/*global angular:false */ +angular.module('myApp.directives', []) + .directive('appVersion', ['version', + function(version) { + return function(scope, elm, attrs) { + elm.text(version); + }; + } + ]) +; + +if (typeof String.prototype.startsWith !== 'function') { + String.prototype.startsWith = function(str) { + return this.slice(0, str.length) === str; + }; +} + +/*global $:false */ +/*global angular:false */ + +angular.module('myApp.filters', []). + filter('interpolate', ['version', function(version) { + return function(text) { + return String(text).replace(/\%VERSION\%/mg, version); + }; +}]); + +/*global $:false */ +/*global angular:false */ +/*global myModule:true*/ + +/* Services */ + + +// Demonstrate how to register services +// In this case it is a simple value service. +angular.module('myApp.services', []) + .value('version', '0.1'); + diff --git a/Rainy.UI/dist/admin/index.html b/Rainy.UI/dist/admin/index.html new file mode 100644 index 0000000..50ef374 --- /dev/null +++ b/Rainy.UI/dist/admin/index.html @@ -0,0 +1,268 @@ + + + + + + + Rainy - Admin + + + + + + +
+ +
+ +
+ +
+
+ + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/Rainy.UI/dist/client.css b/Rainy.UI/dist/client.css new file mode 100644 index 0000000..44ed777 --- /dev/null +++ b/Rainy.UI/dist/client.css @@ -0,0 +1,102 @@ +ul.wysihtml5-toolbar { + margin: 0; + padding: 0; + display: block; +} + +ul.wysihtml5-toolbar::after { + clear: both; + display: table; + content: ""; +} + +ul.wysihtml5-toolbar > li { + float: left; + display: list-item; + list-style: none; + margin: 0 5px 10px 0; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=bold] { + font-weight: bold; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=italic] { + font-style: italic; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=underline] { + text-decoration: underline; +} + +ul.wysihtml5-toolbar a.btn.wysihtml5-command-active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #E6E6E6; + background-color: #D9D9D9; + outline: 0; +} + +ul.wysihtml5-commands-disabled .dropdown-menu { + display: none !important; +} + +ul.wysihtml5-toolbar div.wysihtml5-colors { + display:block; + width: 50px; + height: 20px; + margin-top: 2px; + margin-left: 5px; + position: absolute; + pointer-events: none; +} + +ul.wysihtml5-toolbar a.wysihtml5-colors-title { + padding-left: 70px; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="black"] { + background: black !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="silver"] { + background: silver !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="gray"] { + background: gray !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="maroon"] { + background: maroon !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="red"] { + background: red !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="purple"] { + background: purple !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="green"] { + background: green !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="olive"] { + background: olive !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="navy"] { + background: navy !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="blue"] { + background: blue !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="orange"] { + background: orange !important; +} diff --git a/Rainy.UI/dist/client.js b/Rainy.UI/dist/client.js new file mode 100644 index 0000000..83315cf --- /dev/null +++ b/Rainy.UI/dist/client.js @@ -0,0 +1,1168 @@ +/* + wysihtml5 v0.3.0 + https://github.com/xing/wysihtml5 + + Author: Christopher Blum (https://github.com/tiff) + + Copyright (C) 2012 XING AG + Licensed under the MIT license (MIT) + + Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Copyright 2011, Tim Down + Licensed under the MIT license. + Version: 1.2.2 + Build date: 13 November 2011 +*/ +var wysihtml5={version:"0.3.0",commands:{},dom:{},quirks:{},toolbar:{},lang:{},selection:{},views:{},INVISIBLE_SPACE:"\ufeff",EMPTY_FUNCTION:function(){},ELEMENT_NODE:1,TEXT_NODE:3,BACKSPACE_KEY:8,ENTER_KEY:13,ESCAPE_KEY:27,SPACE_KEY:32,DELETE_KEY:46}; +window.rangy=function(){function b(a,b){var c=typeof a[b];return c==k||!!(c==h&&a[b])||"unknown"==c}function c(a,b){return!!(typeof a[b]==h&&a[b])}function a(a,b){return typeof a[b]!=j}function d(a){return function(b,c){for(var d=c.length;d--;)if(!a(b,c[d]))return!1;return!0}}function e(a){return a&&m(a,r)&&x(a,q)}function f(a){window.alert("Rangy not supported in your browser. Reason: "+a);o.initialized=!0;o.supported=!1}function g(){if(!o.initialized){var a,d=!1,h=!1;b(document,"createRange")&& +(a=document.createRange(),m(a,p)&&x(a,n)&&(d=!0),a.detach());if((a=c(document,"body")?document.body:document.getElementsByTagName("body")[0])&&b(a,"createTextRange"))a=a.createTextRange(),e(a)&&(h=!0);!d&&!h&&f("Neither Range nor TextRange are implemented");o.initialized=!0;o.features={implementsDomRange:d,implementsTextRange:h};d=w.concat(z);h=0;for(a=d.length;h["+a.childNodes.length+"]":a.nodeName}function k(a){this._next=this.root=a}function j(a,b){this.node=a;this.offset=b}function n(a){this.code=this[a];this.codeName=a;this.message="DOMException: "+this.codeName} +var p="undefined",q=b.util;q.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||c.fail("document missing a Node creation method");q.isHostMethod(document,"getElementsByTagName")||c.fail("document missing getElementsByTagName method");var r=document.createElement("div");q.areHostMethods(r,["insertBefore","appendChild","cloneNode"])||c.fail("Incomplete Element implementation");q.isHostProperty(r,"innerHTML")||c.fail("Element is missing innerHTML property");r=document.createTextNode("test"); +q.areHostMethods(r,["splitText","deleteData","insertData","appendData","cloneNode"])||c.fail("Incomplete Text Node implementation");var m=function(a,b){for(var c=a.length;c--;)if(a[c]===b)return!0;return!1};k.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var a=this._current=this._next,b;if(this._current){b=a.firstChild;if(!b)for(b=null;a!==this.root&&!(b=a.nextSibling);)a=a.parentNode;this._next=b}return this._current},detach:function(){this._current=this._next=this.root= +null}};j.prototype={equals:function(a){return this.node===a.node&this.offset==a.offset},inspect:function(){return"[DomPosition("+h(this.node)+":"+this.offset+")]"}};n.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11};n.prototype.toString=function(){return this.message};b.dom={arrayContains:m,isHtmlNamespace:function(a){var b;return typeof a.namespaceURI==p||null===(b=a.namespaceURI)||"http://www.w3.org/1999/xhtml"== +b},parentElement:function(a){a=a.parentNode;return 1==a.nodeType?a:null},getNodeIndex:a,getNodeLength:function(a){var b;return f(a)?a.length:(b=a.childNodes)?b.length:0},getCommonAncestor:d,isAncestorOf:function(a,b,c){for(b=c?b:b.parentNode;b;){if(b===a)return!0;b=b.parentNode}return!1},getClosestAncestorIn:e,isCharacterDataNode:f,insertAfter:g,splitDataNode:function(a,b){var c=a.cloneNode(!1);c.deleteData(0,b);a.deleteData(b,a.length-b);g(c,a);return c},getDocument:i,getWindow:function(a){a=i(a); +if(typeof a.defaultView!=p)return a.defaultView;if(typeof a.parentWindow!=p)return a.parentWindow;throw Error("Cannot get a window object for node");},getIframeWindow:function(a){if(typeof a.contentWindow!=p)return a.contentWindow;if(typeof a.contentDocument!=p)return a.contentDocument.defaultView;throw Error("getIframeWindow: No Window object found for iframe element");},getIframeDocument:function(a){if(typeof a.contentDocument!=p)return a.contentDocument;if(typeof a.contentWindow!=p)return a.contentWindow.document; +throw Error("getIframeWindow: No Document object found for iframe element");},getBody:function(a){return q.isHostObject(a,"body")?a.body:a.getElementsByTagName("body")[0]},getRootContainer:function(a){for(var b;b=a.parentNode;)a=b;return a},comparePoints:function(b,c,h,j){var k;if(b==h)return c===j?0:c=b.childNodes.length?b.appendChild(a):b.insertBefore(a,b.childNodes[c]);return d}function i(b){for(var c,d,h=a(b.range).createDocumentFragment();d=b.next();){c=b.isPartiallySelectedSubtree();d=d.cloneNode(!c);c&&(c=b.getSubtreeIterator(),d.appendChild(i(c)),c.detach(!0));if(10==d.nodeType)throw new B("HIERARCHY_REQUEST_ERR");h.appendChild(d)}return h}function h(a,b,c){for(var d,e,c=c||{stop:!1};d=a.next();)if(a.isPartiallySelectedSubtree())if(!1=== +b(d)){c.stop=!0;break}else{if(d=a.getSubtreeIterator(),h(d,b,c),d.detach(!0),c.stop)break}else for(d=l.createIterator(d);e=d.next();)if(!1===b(e)){c.stop=!0;return}}function k(a){for(var b;a.next();)a.isPartiallySelectedSubtree()?(b=a.getSubtreeIterator(),k(b),b.detach(!0)):a.remove()}function j(b){for(var c,d=a(b.range).createDocumentFragment(),h;c=b.next();){b.isPartiallySelectedSubtree()?(c=c.cloneNode(!1),h=b.getSubtreeIterator(),c.appendChild(j(h)),h.detach(!0)):b.remove();if(10==c.nodeType)throw new B("HIERARCHY_REQUEST_ERR"); +d.appendChild(c)}return d}function n(a,b,c){var d=!(!b||!b.length),e,j=!!c;d&&(e=RegExp("^("+b.join("|")+")$"));var k=[];h(new q(a,!1),function(a){(!d||e.test(a.nodeType))&&(!j||c(a))&&k.push(a)});return k}function p(a){return"["+("undefined"==typeof a.getName?"Range":a.getName())+"("+l.inspectNode(a.startContainer)+":"+a.startOffset+", "+l.inspectNode(a.endContainer)+":"+a.endOffset+")]"}function q(a,b){this.range=a;this.clonePartiallySelectedTextNodes=b;if(!a.collapsed){this.sc=a.startContainer; +this.so=a.startOffset;this.ec=a.endContainer;this.eo=a.endOffset;var c=a.commonAncestorContainer;this.sc===this.ec&&l.isCharacterDataNode(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc===c&&!l.isCharacterDataNode(this.sc)?this.sc.childNodes[this.so]:l.getClosestAncestorIn(this.sc,c,!0),this._last=this.ec===c&&!l.isCharacterDataNode(this.ec)?this.ec.childNodes[this.eo-1]:l.getClosestAncestorIn(this.ec,c,!0))}}function r(a){this.code= +this[a];this.codeName=a;this.message="RangeException: "+this.codeName}function m(a,b,c){this.nodes=n(a,b,c);this._next=this.nodes[0];this._position=0}function s(a){return function(b,c){for(var d,h=c?b:b.parentNode;h;){d=h.nodeType;if(l.arrayContains(a,d))return h;h=h.parentNode}return null}}function x(a,b){if($(a,b))throw new r("INVALID_NODE_TYPE_ERR");}function o(a){if(!a.startContainer)throw new B("INVALID_STATE_ERR");}function z(a,b){if(!l.arrayContains(b,a.nodeType))throw new r("INVALID_NODE_TYPE_ERR"); +}function w(a,b){if(0>b||b>(l.isCharacterDataNode(a)?a.length:a.childNodes.length))throw new B("INDEX_SIZE_ERR");}function y(a,b){if(O(a,!0)!==O(b,!0))throw new B("WRONG_DOCUMENT_ERR");}function A(a){if(aa(a,!0))throw new B("NO_MODIFICATION_ALLOWED_ERR");}function t(a,b){if(!a)throw new B(b);}function v(a){o(a);if(!l.arrayContains(G,a.startContainer.nodeType)&&!O(a.startContainer,!0)||!l.arrayContains(G,a.endContainer.nodeType)&&!O(a.endContainer,!0)||!(a.startOffset<=(l.isCharacterDataNode(a.startContainer)? +a.startContainer.length:a.startContainer.childNodes.length))||!(a.endOffset<=(l.isCharacterDataNode(a.endContainer)?a.endContainer.length:a.endContainer.childNodes.length)))throw Error("Range error: Range is no longer valid after DOM mutation ("+a.inspect()+")");}function D(){}function K(a){a.START_TO_START=Q;a.START_TO_END=U;a.END_TO_END=ba;a.END_TO_START=V;a.NODE_BEFORE=W;a.NODE_AFTER=X;a.NODE_BEFORE_AND_AFTER=Y;a.NODE_INSIDE=R}function F(a){K(a);K(a.prototype)}function E(a,b){return function(){v(this); +var c=this.startContainer,d=this.startOffset,e=this.commonAncestorContainer,j=new q(this,!0);c!==e&&(c=l.getClosestAncestorIn(c,e,!0),d=f(c),c=d.node,d=d.offset);h(j,A);j.reset();e=a(j);j.detach();b(this,c,d,c,d);return e}}function I(a,d,h){function g(a,b){return function(c){o(this);z(c,L);z(M(c),G);c=(a?e:f)(c);(b?i:n)(this,c.node,c.offset)}}function i(a,b,c){var h=a.endContainer,e=a.endOffset;if(b!==a.startContainer||c!==a.startOffset){if(M(b)!=M(h)||1==l.comparePoints(b,c,h,e))h=b,e=c;d(a,b,c, +h,e)}}function n(a,b,c){var h=a.startContainer,e=a.startOffset;if(b!==a.endContainer||c!==a.endOffset){if(M(b)!=M(h)||-1==l.comparePoints(b,c,h,e))h=b,e=c;d(a,h,e,b,c)}}a.prototype=new D;b.util.extend(a.prototype,{setStart:function(a,b){o(this);x(a,!0);w(a,b);i(this,a,b)},setEnd:function(a,b){o(this);x(a,!0);w(a,b);n(this,a,b)},setStartBefore:g(!0,!0),setStartAfter:g(!1,!0),setEndBefore:g(!0,!1),setEndAfter:g(!1,!1),collapse:function(a){v(this);a?d(this,this.startContainer,this.startOffset,this.startContainer, +this.startOffset):d(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(a){o(this);x(a,!0);d(this,a,0,a,l.getNodeLength(a))},selectNode:function(a){o(this);x(a,!1);z(a,L);var b=e(a),a=f(a);d(this,b.node,b.offset,a.node,a.offset)},extractContents:E(j,d),deleteContents:E(k,d),canSurroundContents:function(){v(this);A(this.startContainer);A(this.endContainer);var a=new q(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);a.detach();return!b}, +detach:function(){h(this)},splitBoundaries:function(){v(this);var a=this.startContainer,b=this.startOffset,c=this.endContainer,h=this.endOffset,e=a===c;l.isCharacterDataNode(c)&&(0=l.getNodeIndex(a)&&h++,b=0);d(this,a,b,c,h)},normalizeBoundaries:function(){v(this);var a=this.startContainer,b=this.startOffset,c=this.endContainer,h=this.endOffset,e=function(a){var b= +a.nextSibling;b&&b.nodeType==a.nodeType&&(c=a,h=a.length,a.appendData(b.data),b.parentNode.removeChild(b))},j=function(d){var e=d.previousSibling;if(e&&e.nodeType==d.nodeType){a=d;var j=d.length;b=e.length;d.insertData(0,e.data);e.parentNode.removeChild(e);a==c?(h+=b,c=a):c==d.parentNode&&(e=l.getNodeIndex(d),h==e?(c=d,h=j):h>e&&h--)}},k=!0;l.isCharacterDataNode(c)?c.length==h&&e(c):(0x",S=3==Z.firstChild.nodeType}catch(ca){}b.features.htmlParsingConforms=S;var T="startContainer startOffset endContainer endOffset collapsed commonAncestorContainer".split(" "),Q=0,U=1,ba=2,V=3,W=0,X=1,Y=2,R=3;D.prototype={attachListener:function(a,b){this._listeners[a].push(b)},compareBoundaryPoints:function(a,b){v(this); +y(this.startContainer,b.startContainer);var c=a==V||a==Q?"start":"end",d=a==U||a==Q?"start":"end";return l.comparePoints(this[c+"Container"],this[c+"Offset"],b[d+"Container"],b[d+"Offset"])},insertNode:function(a){v(this);z(a,P);A(this.startContainer);if(l.isAncestorOf(a,this.startContainer,!0))throw new B("HIERARCHY_REQUEST_ERR");this.setStartBefore(g(a,this.startContainer,this.startOffset))},cloneContents:function(){v(this);var b,c;if(this.collapsed)return a(this).createDocumentFragment();if(this.startContainer=== +this.endContainer&&l.isCharacterDataNode(this.startContainer))return b=this.startContainer.cloneNode(!0),b.data=b.data.slice(this.startOffset,this.endOffset),c=a(this).createDocumentFragment(),c.appendChild(b),c;c=new q(this,!0);b=i(c);c.detach();return b},canSurroundContents:function(){v(this);A(this.startContainer);A(this.endContainer);var a=new q(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);a.detach();return!b},surroundContents:function(a){z(a,H);if(!this.canSurroundContents())throw new r("BAD_BOUNDARYPOINTS_ERR"); +var b=this.extractContents();if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild);g(a,this.startContainer,this.startOffset);a.appendChild(b);this.selectNode(a)},cloneRange:function(){v(this);for(var b=new C(a(this)),c=T.length,d;c--;)d=T[c],b[d]=this[d];return b},toString:function(){v(this);var a=this.startContainer;if(a===this.endContainer&&l.isCharacterDataNode(a))return 3==a.nodeType||4==a.nodeType?a.data.slice(this.startOffset,this.endOffset):"";var b=[],a=new q(this,!0);h(a,function(a){(3== +a.nodeType||4==a.nodeType)&&b.push(a.data)});a.detach();return b.join("")},compareNode:function(a){v(this);var b=a.parentNode,c=l.getNodeIndex(a);if(!b)throw new B("NOT_FOUND_ERR");a=this.comparePoint(b,c);b=this.comparePoint(b,c+1);return 0>a?0l.comparePoints(a,b,this.startContainer,this.startOffset)?-1:0=e&&0<=d:0>e&&0=l.comparePoints(a,b,this.endContainer,this.endOffset)},intersectsRange:function(b,c){v(this);if(a(b)!=a(this))throw new B("WRONG_DOCUMENT_ERR");var d=l.comparePoints(this.startContainer,this.startOffset,b.endContainer,b.endOffset),h=l.comparePoints(this.endContainer,this.endOffset,b.startContainer,b.startOffset);return c?0>=d&&0<=h:0>d&&0=this.comparePoint(a,l.getNodeLength(a))},containsRange:function(a){return this.intersection(a).equals(a)},containsNodeText:function(a){var b=this.cloneRange();b.selectNode(a);var c=b.getNodes([3]);return 012");b.close();var c=m.getIframeWindow(a).getSelection(), +d=b.documentElement.lastChild.firstChild,b=b.createRange();b.setStart(d,1);b.collapse(true);c.addRange(b);C=c.rangeCount==1;c.removeAllRanges();var e=b.cloneRange();b.setStart(d,0);e.setEnd(d,2);c.addRange(b);c.addRange(e);J=c.rangeCount==2;b.detach();e.detach();F.removeChild(a)}();b.features.selectionSupportsMultipleRanges=J;b.features.collapsedNonEditableSelectionsSupported=C;var l=!1,u;F&&s.isHostMethod(F,"createControlRange")&&(u=F.createControlRange(),s.areHostProperties(u,["item","add"])&&(l= +!0));b.features.implementsControlRange=l;A=E?function(a){return a.anchorNode===a.focusNode&&a.anchorOffset===a.focusOffset}:function(a){return a.rangeCount?a.getRangeAt(a.rangeCount-1).collapsed:false};var B;s.isHostMethod(t,"getRangeAt")?B=function(a,b){try{return a.getRangeAt(b)}catch(c){return null}}:E&&(B=function(a){var c=m.getDocument(a.anchorNode),c=b.createRange(c);c.setStart(a.anchorNode,a.anchorOffset);c.setEnd(a.focusNode,a.focusOffset);if(c.collapsed!==this.isCollapsed){c.setStart(a.focusNode, +a.focusOffset);c.setEnd(a.anchorNode,a.anchorOffset)}return c});b.getSelection=function(a){var a=a||window,b=a._rangySelection,c=y(a),e=v?d(a):null;if(b){b.nativeSelection=c;b.docSelection=e;b.refresh(a)}else{b=new n(c,e,a);a._rangySelection=b}return b};b.getIframeSelection=function(a){return b.getSelection(m.getIframeWindow(a))};u=n.prototype;if(!D&&E&&s.areHostMethods(t,["removeAllRanges","addRange"])){u.removeAllRanges=function(){this.nativeSelection.removeAllRanges();f(this)};var L=function(a, +c){var d=x.getRangeDocument(c),d=b.createRange(d);d.collapseToPoint(c.endContainer,c.endOffset);a.nativeSelection.addRange(g(d));a.nativeSelection.extend(c.startContainer,c.startOffset);a.refresh()};u.addRange=N?function(a,c){if(l&&v&&this.docSelection.type=="Control")j(this,a);else if(c&&I)L(this,a);else{var d;if(J)d=this.rangeCount;else{this.removeAllRanges();d=0}this.nativeSelection.addRange(g(a));this.rangeCount=this.nativeSelection.rangeCount;if(this.rangeCount==d+1){if(b.config.checkSelectionRanges)(d= +B(this.nativeSelection,this.rangeCount-1))&&!x.rangesEqual(d,a)&&(a=new o(d));this._ranges[this.rangeCount-1]=a;e(this,a,H(this.nativeSelection));this.isCollapsed=A(this)}else this.refresh()}}:function(a,b){if(b&&I)L(this,a);else{this.nativeSelection.addRange(g(a));this.refresh()}};u.setRanges=function(a){if(l&&a.length>1)p(this,a);else{this.removeAllRanges();for(var b=0,c=a.length;b1?p(this,a):b&&this.addRange(a[0])};else return c.fail("No means of selecting a Range or TextRange was found"),!1;u.getRangeAt=function(a){if(a<0||a>=this.rangeCount)throw new z("INDEX_SIZE_ERR");return this._ranges[a]};var G;if(D)G=function(a){var c;if(b.isSelectionValid(a.win))c=a.docSelection.createRange();else{c=m.getBody(a.win.document).createTextRange();c.collapse(true)}a.docSelection.type== +"Control"?k(a):c&&typeof c.text!="undefined"?h(a,c):f(a)};else if(s.isHostMethod(t,"getRangeAt")&&"number"==typeof t.rangeCount)G=function(a){if(l&&v&&a.docSelection.type=="Control")k(a);else{a._ranges.length=a.rangeCount=a.nativeSelection.rangeCount;if(a.rangeCount){for(var c=0,d=a.rangeCount;c(/ipad|iphone|ipod/.test(a)&&a.match(/ os (\d+).+? like mac os x/)||[,0])[1]||-1!==a.indexOf("opera mobi")||-1!==a.indexOf("hpwos/");return b&&d&&e&&!a},isTouchDevice:function(){return this.supportsEvent("touchmove")},isIos:function(){var a=this.USER_AGENT.toLowerCase();return-1!==a.indexOf("webkit")&&-1!==a.indexOf("mobile")},supportsSandboxedIframes:function(){return a},throwsMixedContentWarningWhenIframeSrcIsEmpty:function(){return!("querySelector"in document)},displaysCaretInEmptyContentEditableCorrectly:function(){return!d}, +hasCurrentStyleProperty:function(){return"currentStyle"in c},insertsLineBreaksOnReturn:function(){return d},supportsPlaceholderAttributeOn:function(a){return"placeholder"in a},supportsEvent:function(a){var b;if(!(b="on"+a in c))c.setAttribute("on"+a,"return;"),b="function"===typeof c["on"+a];return b},supportsEventsInIframeCorrectly:function(){return!g},firesOnDropOnlyWhenOnDragOverIsCancelled:function(){return e||d},supportsDataTransfer:function(){try{return e&&(window.Clipboard||window.DataTransfer).prototype.getData}catch(a){return!1}}, +supportsHTML5Tags:function(a){a=a.createElement("div");a.innerHTML="
foo
";return"
foo
"===a.innerHTML.toLowerCase()},supportsCommand:function(){var b={formatBlock:a,insertUnorderedList:a||g||e,insertOrderedList:a||g||e},c={insertHTML:d};return function(a,d){if(!b[d]){try{return a.queryCommandSupported(d)}catch(e){}try{return a.queryCommandEnabled(d)}catch(f){return!!c[d]}}return!1}}(),doesAutoLinkingInContentEditable:function(){return a},canDisableAutoLinking:function(){return this.supportsCommand(document, +"AutoUrlDetect")},clearsContentEditableCorrectly:function(){return d||g||e},supportsGetAttributeCorrectly:function(){return"1"!=document.createElement("td").getAttribute("rowspan")},canSelectImagesInContentEditable:function(){return d||a||g},clearsListsInContentEditableCorrectly:function(){return d||a||e},autoScrollsToCaret:function(){return!e},autoClosesUnclosedTags:function(){var a=c.cloneNode(!1),b;a.innerHTML="

";a=a.innerHTML.toLowerCase();b="

"===a||"

"=== +a;this.autoClosesUnclosedTags=function(){return b};return b},supportsNativeGetElementsByClassName:function(){return-1!==(""+document.getElementsByClassName).indexOf("[native code]")},supportsSelectionModify:function(){return"getSelection"in window&&"modify"in window.getSelection()},supportsClassList:function(){return"classList"in c},needsSpaceAfterLineBreak:function(){return g},supportsSpeechApiOn:function(a){return 11<=(b.match(/Chrome\/(\d+)/)||[,0])[1]&&("onwebkitspeechchange"in a||"speech"in a)}, +crashesWhenDefineProperty:function(b){return a&&("XMLHttpRequest"===b||"XDomainRequest"===b)},doesAsyncFocus:function(){return a},hasProblemsSettingCaretAfterImg:function(){return a},hasUndoInContextMenu:function(){return d||f||g}}}(); +wysihtml5.lang.array=function(b){return{contains:function(c){if(b.indexOf)return-1!==b.indexOf(c);for(var a=0,d=b.length;ab.split(c).length&&(b+=c,c="");var e=d=b;b.length>g&&(e=e.substr(0,g)+"...");"www."===d.substr(0,4)&&(d="http://"+d);return''+e+""+c})}function a(h){if(!d.contains(h.nodeName))if(h.nodeType===b.TEXT_NODE&&h.data.match(e)){var f=h.parentNode,j;j=f.ownerDocument;var g=j._wysihtml5_tempElement;g||(g=j._wysihtml5_tempElement=j.createElement("div"));j= +g;j.innerHTML=""+c(h.data);for(j.removeChild(j.firstChild);j.firstChild;)f.insertBefore(j.firstChild,h);f.removeChild(h)}else{f=b.lang.array(h.childNodes).get();j=f.length;for(g=0;g=g.childNodes.length&&g.nodeName.toLowerCase()===d&&!g.attributes.length?g.firstChild:g}function c(a,b){var b=b.toLowerCase(),c;if(c="IMG"==a.nodeName)if(c="src"==b){var d;try{d=a.complete&&!a.mozMatchesSelector(":-moz-broken")}catch(e){a.complete&&"complete"===a.readyState&&(d=!0)}c=!0===d}return c? +a.src:i&&"outerHTML"in a?-1!=a.outerHTML.toLowerCase().indexOf(" "+b+"=")?a.getAttribute(b):null:a.getAttribute(b)}var a={1:function(a){var b,f,i=g.tags;f=a.nodeName.toLowerCase();b=a.scopeName;if(a._wysihtml5)return null;a._wysihtml5=1;if("wysihtml5-temp"===a.className)return null;b&&"HTML"!=b&&(f=b+":"+f);"outerHTML"in a&&!wysihtml5.browser.autoClosesUnclosedTags()&&("P"===a.nodeName&&"

"!==a.outerHTML.slice(-4).toLowerCase())&&(f="div");if(f in i){b=i[f];if(!b||b.remove)return null;b="string"=== +typeof b?{rename_tag:b}:b}else if(a.firstChild)b={rename_tag:d};else return null;f=a.ownerDocument.createElement(b.rename_tag||f);var i={},r=b.set_class,m=b.add_class,s=b.set_attributes,x=b.check_attributes,o=g.classes,z=0,w=[];b=[];var y=[],A=[],t;s&&(i=wysihtml5.lang.object(s).clone());if(x)for(t in x)if(s=h[x[t]])s=s(c(a,t)),"string"===typeof s&&(i[t]=s);r&&w.push(r);if(m)for(t in m)if(s=k[m[t]])r=s(c(a,t)),"string"===typeof r&&w.push(r);o["_wysihtml5-temp-placeholder"]=1;(A=a.getAttribute("class"))&& +(w=w.concat(A.split(e)));for(m=w.length;z';a.stylesheets=d;return b.lang.string('#{stylesheets}').interpolate(a)},_unset:function(a,c,d,e){try{a[c]=d}catch(k){}try{a.__defineGetter__(c,function(){return d})}catch(j){}if(e)try{a.__defineSetter__(c, +function(){})}catch(n){}if(!b.browser.crashesWhenDefineProperty(c))try{var p={get:function(){return d}};e&&(p.set=function(){});Object.defineProperty(a,c,p)}catch(q){}}})})(wysihtml5);(function(){var b={className:"class"};wysihtml5.dom.setAttributes=function(c){return{on:function(a){for(var d in c)a.setAttribute(b[d]||d,c[d])}}}})(); +wysihtml5.dom.setStyles=function(b){return{on:function(c){c=c.style;if("string"===typeof b)c.cssText+=";"+b;else for(var a in b)"float"===a?(c.cssFloat=b[a],c.styleFloat=b[a]):c[a]=b[a]}}}; +(function(b){b.simulatePlaceholder=function(c,a,d){var e=function(){a.hasPlaceholderSet()&&a.clear();b.removeClass(a.element,"placeholder")},f=function(){a.isEmpty()&&(a.setValue(d),b.addClass(a.element,"placeholder"))};c.observe("set_placeholder",f).observe("unset_placeholder",e).observe("focus:composer",e).observe("paste:composer",e).observe("blur:composer",f);f()}})(wysihtml5.dom); +(function(b){var c=document.documentElement;"textContent"in c?(b.setTextContent=function(a,b){a.textContent=b},b.getTextContent=function(a){return a.textContent}):"innerText"in c?(b.setTextContent=function(a,b){a.innerText=b},b.getTextContent=function(a){return a.innerText}):(b.setTextContent=function(a,b){a.nodeValue=b},b.getTextContent=function(a){return a.nodeValue})})(wysihtml5.dom); +wysihtml5.quirks.cleanPastedHTML=function(){var b={"a u":wysihtml5.dom.replaceWithChildNodes};return function(c,a,d){var a=a||b,d=d||c.ownerDocument||document,e="string"===typeof c,f,g,i,h=0,c=e?wysihtml5.dom.getAsDom(c,d):c;for(i in a){f=c.querySelectorAll(i);d=a[i];for(g=f.length;h 

"==b||"

 

 

"==b)a.innerHTML=""},0)};return function(b){c.observe(b.element,["cut","keydown"],a)}}();b.quirks.ensureProperClearingOfLists=function(){var a=["OL","UL","MENU"];return function(d){c.observe(d.element,"keydown",function(e){if(e.keyCode===b.BACKSPACE_KEY){var f=d.selection.getSelectedNode(),e=d.element; +e.firstChild&&b.lang.array(a).contains(e.firstChild.nodeName)&&(f=c.getParentElement(f,{nodeName:a}))&&f==e.firstChild&&1>=f.childNodes.length&&(f.firstChild?""===f.firstChild.innerHTML:1)&&f.parentNode.removeChild(f)}})}}()})(wysihtml5); +(function(b){b.quirks.getCorrectInnerHTML=function(c){var a=c.innerHTML;if(-1===a.indexOf("%7E"))return a;var c=c.querySelectorAll("[href*='~'], [src*='~']"),d,e,f,g;g=0;for(f=c.length;g'+b.INVISIBLE_SPACE+"",h=this.getRange(this.doc);if(h){i=h.createContextualFragment(i);h.insertNode(i);try{a(h.startContainer,h.endContainer)}catch(k){setTimeout(function(){throw k; +},0)}(caretPlaceholder=this.doc.querySelector("._wysihtml5-temp-placeholder"))?(h=rangy.createRange(this.doc),h.selectNode(caretPlaceholder),h.deleteContents(),this.setSelection(h)):e.focus();c&&(e.scrollTop=f,e.scrollLeft=g);try{caretPlaceholder.parentNode.removeChild(caretPlaceholder)}catch(j){}}else a(e,e)},executeAndRestoreSimple:function(a){var b,c,f=this.getRange(),g=this.doc.body,i;if(f){b=f.getNodes([3]);g=b[0]||f.startContainer;i=b[b.length-1]||f.endContainer;b=g===f.startContainer?f.startOffset: +0;c=i===f.endContainer?f.endOffset:i.length;try{a(f.startContainer,f.endContainer)}catch(h){setTimeout(function(){throw h;},0)}a=rangy.createRange(this.doc);try{a.setStart(g,b)}catch(k){}try{a.setEnd(i,c)}catch(j){}try{this.setSelection(a)}catch(n){}}else a(g,g)},insertHTML:function(a){var a=rangy.createRange(this.doc).createContextualFragment(a),b=a.lastChild;this.insertNode(a);b&&this.setAfter(b)},insertNode:function(a){var b=this.getRange();b&&b.insertNode(a)},surround:function(a){var b=this.getRange(); +if(b)try{b.surroundContents(a),this.selectNode(a)}catch(c){a.appendChild(b.extractContents()),b.insertNode(a)}},scrollIntoView:function(){var a=this.doc,c=a.documentElement.scrollHeight>a.documentElement.offsetHeight,e;if(!(e=a._wysihtml5ScrollIntoViewElement))e=a.createElement("span"),e.innerHTML=b.INVISIBLE_SPACE;e=a._wysihtml5ScrollIntoViewElement=e;if(c){this.insertNode(e);var c=e,f=0;if(c.parentNode){do f+=c.offsetTop||0,c=c.offsetParent;while(c)}c=f;e.parentNode.removeChild(e);c>a.body.scrollTop&& +(a.body.scrollTop=c)}},selectLine:function(){b.browser.supportsSelectionModify()?this._selectLine_W3C():this.doc.selection&&this._selectLine_MSIE()},_selectLine_W3C:function(){var a=this.doc.defaultView.getSelection();a.modify("extend","left","lineboundary");a.modify("extend","right","lineboundary")},_selectLine_MSIE:function(){var a=this.doc.selection.createRange(),b=a.boundingTop,c=this.doc.body.scrollWidth,f;if(a.moveToPoint){0===b&&(f=this.doc.createElement("span"),this.insertNode(f),b=f.offsetTop, +f.parentNode.removeChild(f));b+=1;for(f=-10;f",a=''+b.INVISIBLE_SPACE+"",d=b.dom;b.UndoManager=b.lang.Dispatcher.extend({constructor:function(a){this.editor=a;this.composer=a.composer;this.element=this.composer.element;this.history=[this.composer.getValue()];this.position=1;this.composer.commands.support("insertHTML")&&this._observe()},_observe:function(){var e=this,f=this.composer.sandbox.getDocument(), +g;d.observe(this.element,"keydown",function(a){if(!(a.altKey||!a.ctrlKey&&!a.metaKey)){var b=a.keyCode,c=90===b&&a.shiftKey||89===b;90===b&&!a.shiftKey?(e.undo(),a.preventDefault()):c&&(e.redo(),a.preventDefault())}});d.observe(this.element,"keydown",function(a){a=a.keyCode;a!==g&&(g=a,(8===a||46===a)&&e.transact())});if(b.browser.hasUndoInContextMenu()){var i,h,k=function(){for(var a;a=f.querySelector("._wysihtml5-temp");)a.parentNode.removeChild(a);clearInterval(i)};d.observe(this.element,"contextmenu", +function(){k();e.composer.selection.executeAndRestoreSimple(function(){e.element.lastChild&&e.composer.selection.setAfter(e.element.lastChild);f.execCommand("insertHTML",!1,c);f.execCommand("insertHTML",!1,a);f.execCommand("undo",!1,null)});i=setInterval(function(){f.getElementById("_wysihtml5-redo")?(k(),e.redo()):f.getElementById("_wysihtml5-undo")||(k(),e.undo())},400);h||(h=!0,d.observe(document,"mousedown",k),d.observe(f,["mousedown","paste","cut","copy"],k))})}this.editor.observe("newword:composer", +function(){e.transact()}).observe("beforecommand:composer",function(){e.transact()})},transact:function(){var a=this.history[this.position-1],b=this.composer.getValue();if(b!=a){if(40<(this.history.length=this.position))this.history.shift(),this.position--;this.position++;this.history.push(b)}},undo:function(){this.transact();1>=this.position||(this.set(this.history[--this.position-1]),this.editor.fire("undo:composer"))},redo:function(){this.position>=this.history.length||(this.set(this.history[++this.position- +1]),this.editor.fire("redo:composer"))},set:function(a){this.composer.setValue(a);this.editor.focus(!0)}})})(wysihtml5); +wysihtml5.views.View=Base.extend({constructor:function(b,c,a){this.parent=b;this.element=c;this.config=a;this._observeViewChange()},_observeViewChange:function(){var b=this;this.parent.observe("beforeload",function(){b.parent.observe("change_view",function(c){c===b.name?(b.parent.currentView=b,b.show(),setTimeout(function(){b.focus()},0)):b.hide()})})},focus:function(){if(this.element.ownerDocument.querySelector(":focus")!==this.element)try{this.element.focus()}catch(b){}},hide:function(){this.element.style.display= +"none"},show:function(){this.element.style.display=""},disable:function(){this.element.setAttribute("disabled","disabled")},enable:function(){this.element.removeAttribute("disabled")}}); +(function(b){var c=b.dom,a=b.browser;b.views.Composer=b.views.View.extend({name:"composer",CARET_HACK:"
",constructor:function(a,b,c){this.base(a,b,c);this.textarea=this.parent.textarea;this._initSandbox()},clear:function(){this.element.innerHTML=a.displaysCaretInEmptyContentEditableCorrectly()?"":this.CARET_HACK},getValue:function(a){var c=this.isEmpty()?"":b.quirks.getCorrectInnerHTML(this.element);a&&(c=this.parent.parse(c));return c=b.lang.string(c).replace(b.INVISIBLE_SPACE).by("")},setValue:function(a, +b){b&&(a=this.parent.parse(a));this.element.innerHTML=a},show:function(){this.iframe.style.display=this._displayStyle||"";this.disable();this.enable()},hide:function(){this._displayStyle=c.getStyle("display").from(this.iframe);"none"===this._displayStyle&&(this._displayStyle=null);this.iframe.style.display="none"},disable:function(){this.element.removeAttribute("contentEditable");this.base()},enable:function(){this.element.setAttribute("contentEditable","true");this.base()},focus:function(a){b.browser.doesAsyncFocus()&& +this.hasPlaceholderSet()&&this.clear();this.base();var c=this.element.lastChild;a&&c&&("BR"===c.nodeName?this.selection.setBefore(this.element.lastChild):this.selection.setAfter(this.element.lastChild))},getTextContent:function(){return c.getTextContent(this.element)},hasPlaceholderSet:function(){return this.getTextContent()==this.textarea.element.getAttribute("placeholder")},isEmpty:function(){var a=this.element.innerHTML;return""===a||a===this.CARET_HACK||this.hasPlaceholderSet()||""===this.getTextContent()&& +!this.element.querySelector("blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea")},_initSandbox:function(){var a=this;this.sandbox=new c.Sandbox(function(){a._create()},{stylesheets:this.config.stylesheets});this.iframe=this.sandbox.getIframe();var b=document.createElement("input");b.type="hidden";b.name="_wysihtml5_mode";b.value=1;var f=this.textarea.element;c.insert(this.iframe).after(f);c.insert(b).after(f)},_create:function(){var d=this;this.doc= +this.sandbox.getDocument();this.element=this.doc.body;this.textarea=this.parent.textarea;this.element.innerHTML=this.textarea.getValue(!0);this.enable();this.selection=new b.Selection(this.parent);this.commands=new b.Commands(this.parent);c.copyAttributes("className spellcheck title lang dir accessKey".split(" ")).from(this.textarea.element).to(this.element);c.addClass(this.element,this.config.composerClassName);this.config.style&&this.style();this.observe();var e=this.config.name;e&&(c.addClass(this.element, +e),c.addClass(this.iframe,e));(e="string"===typeof this.config.placeholder?this.config.placeholder:this.textarea.element.getAttribute("placeholder"))&&c.simulatePlaceholder(this.parent,this,e);this.commands.exec("styleWithCSS",!1);this._initAutoLinking();this._initObjectResizing();this._initUndoManager();(this.textarea.element.hasAttribute("autofocus")||document.querySelector(":focus")==this.textarea.element)&&setTimeout(function(){d.focus()},100);b.quirks.insertLineBreakOnReturn(this);a.clearsContentEditableCorrectly()|| +b.quirks.ensureProperClearing(this);a.clearsListsInContentEditableCorrectly()||b.quirks.ensureProperClearingOfLists(this);this.initSync&&this.config.sync&&this.initSync();this.textarea.hide();this.parent.fire("beforeload").fire("load")},_initAutoLinking:function(){var d=this,e=a.canDisableAutoLinking(),f=a.doesAutoLinkingInContentEditable();e&&this.commands.exec("autoUrlDetect",!1);if(this.config.autoLink){(!f||f&&e)&&this.parent.observe("newword:composer",function(){d.selection.executeAndRestore(function(a, +b){c.autoLink(b.parentNode)})});var g=this.sandbox.getDocument().getElementsByTagName("a"),i=c.autoLink.URL_REG_EXP,h=function(a){a=b.lang.string(c.getTextContent(a)).trim();"www."===a.substr(0,4)&&(a="http://"+a);return a};c.observe(this.element,"keydown",function(a){if(g.length){var a=d.selection.getSelectedNode(a.target.ownerDocument),b=c.getParentElement(a,{nodeName:"A"},4),e;b&&(e=h(b),setTimeout(function(){var a=h(b);a!==e&&a.match(i)&&b.setAttribute("href",a)},0))}})}},_initObjectResizing:function(){var d= +["width","height"],e=d.length,f=this.element;this.commands.exec("enableObjectResizing",this.config.allowObjectResizing);this.config.allowObjectResizing?a.supportsEvent("resizeend")&&c.observe(f,"resizeend",function(a){for(var a=a.target||a.srcElement,c=a.style,h=0,k;h "+b.font_styles.normal+" "+""+""+"",emphasis:"
  • "+"
  • ",lists:"
  • "+""+""+""+"
    "+"
  • ",link:"
  • "+""+"
  • ",image:"
  • "+""+"
  • ",html:"
  • "+"
    "+"
  • ",color:""};return c[a]},d=function(b,c){this.el=b,this.toolbar=this.createToolbar(b,c||f),this.editor=this.createEditor(c),window.editor=this.editor,a("iframe.wysihtml5-sandbox").each(function(b,c){a(c.contentWindow).off("focus.wysihtml5").on({"focus.wysihtml5":function(){a("li.dropdown").removeClass("open")}})})};d.prototype={constructor:d,createEditor:function(a){a=a||{},a.toolbar=this.toolbar[0];var c=new b.Editor(this.el[0],a);if(a&&a.events)for(var d in a.events)c.on(d,a.events[d]);return c},createToolbar:function(b,d){var e=this,h=a("
      ",{"class":"wysihtml5-toolbar",style:"display:none"}),i=d.locale||f.locale||"en";for(var j in f){var k=!1;d[j]!==undefined?d[j]===!0&&(k=!0):k=f[j],k===!0&&(h.append(c(j,g[i])),j==="html"&&this.initHtml(h),j==="link"&&this.initInsertLink(h),j==="image"&&this.initInsertImage(h))}if(d.toolbar)for(j in d.toolbar)h.append(d.toolbar[j]);return h.find("a[data-wysihtml5-command='formatBlock']").click(function(b){var c=b.target||b.srcElement,d=a(c);e.toolbar.find(".current-font").text(d.html())}),h.find("a[data-wysihtml5-command='foreColor']").click(function(b){var c=b.target||b.srcElement,d=a(c);e.toolbar.find(".current-color").text(d.html())}),this.el.before(h),h},initHtml:function(a){var b="a[data-wysihtml5-action='change_view']";a.find(b).click(function(c){a.find("a.btn").not(b).toggleClass("disabled")})},initInsertImage:function(b){var c=this,d=b.find(".bootstrap-wysihtml5-insert-image-modal"),e=d.find(".bootstrap-wysihtml5-insert-image-url"),f=d.find("a.btn-primary"),g=e.val(),h=function(){var a=e.val();e.val(g),c.editor.composer.commands.exec("insertImage",a)};e.keypress(function(a){a.which==13&&(h(),d.modal("hide"))}),f.click(h),d.on("shown",function(){e.focus()}),d.on("hide",function(){c.editor.currentView.element.focus()}),b.find("a[data-wysihtml5-command=insertImage]").click(function(){var b=a(this).hasClass("wysihtml5-command-active");return b?!0:(d.modal("show"),d.on("click.dismiss.modal",'[data-dismiss="modal"]',function(a){a.stopPropagation()}),!1)})},initInsertLink:function(b){var c=this,d=b.find(".bootstrap-wysihtml5-insert-link-modal"),e=d.find(".bootstrap-wysihtml5-insert-link-url"),f=d.find("a.btn-primary"),g=e.val(),h=function(){var a=e.val();e.val(g),c.editor.composer.commands.exec("createLink",{href:a,target:"_blank",rel:"nofollow"})},i=!1;e.keypress(function(a){a.which==13&&(h(),d.modal("hide"))}),f.click(h),d.on("shown",function(){e.focus()}),d.on("hide",function(){c.editor.currentView.element.focus()}),b.find("a[data-wysihtml5-command=createLink]").click(function(){var b=a(this).hasClass("wysihtml5-command-active");return b?!0:(d.appendTo("body").modal("show"),d.on("click.dismiss.modal",'[data-dismiss="modal"]',function(a){a.stopPropagation()}),!1)})}};var e={resetDefaults:function(){a.fn.wysihtml5.defaultOptions=a.extend(!0,{},a.fn.wysihtml5.defaultOptionsCache)},bypassDefaults:function(b){return this.each(function(){var c=a(this);c.data("wysihtml5",new d(c,b))})},shallowExtend:function(b){var c=a.extend({},a.fn.wysihtml5.defaultOptions,b||{}),d=this;return e.bypassDefaults.apply(d,[c])},deepExtend:function(b){var c=a.extend(!0,{},a.fn.wysihtml5.defaultOptions,b||{}),d=this;return e.bypassDefaults.apply(d,[c])},init:function(a){var b=this;return e.shallowExtend.apply(b,[a])}};a.fn.wysihtml5=function(b){if(e[b])return e[b].apply(this,Array.prototype.slice.call(arguments,1));if(typeof b=="object"||!b)return e.init.apply(this,arguments);a.error("Method "+b+" does not exist on jQuery.wysihtml5")},a.fn.wysihtml5.Constructor=d;var f=a.fn.wysihtml5.defaultOptions={"font-styles":!0,color:!1,emphasis:!0,lists:!0,html:!1,link:!0,image:!0,events:{},parserRules:{classes:{"wysiwyg-color-silver":1,"wysiwyg-color-gray":1,"wysiwyg-color-white":1,"wysiwyg-color-maroon":1,"wysiwyg-color-red":1,"wysiwyg-color-purple":1,"wysiwyg-color-fuchsia":1,"wysiwyg-color-green":1,"wysiwyg-color-lime":1,"wysiwyg-color-olive":1,"wysiwyg-color-yellow":1,"wysiwyg-color-navy":1,"wysiwyg-color-blue":1,"wysiwyg-color-teal":1,"wysiwyg-color-aqua":1,"wysiwyg-color-orange":1},tags:{b:{},i:{},br:{},ol:{},ul:{},li:{},h1:{},h2:{},h3:{},blockquote:{},u:1,img:{check_attributes:{width:"numbers",alt:"alt",src:"url",height:"numbers"}},a:{set_attributes:{target:"_blank",rel:"nofollow"},check_attributes:{href:"url"}},span:1,div:1}},stylesheets:["./lib/css/wysiwyg-color.css"],locale:"en"};typeof a.fn.wysihtml5.defaultOptionsCache=="undefined"&&(a.fn.wysihtml5.defaultOptionsCache=a.extend(!0,{},a.fn.wysihtml5.defaultOptions));var g=a.fn.wysihtml5.locale={en:{font_styles:{normal:"Normal text",h1:"Heading 1",h2:"Heading 2",h3:"Heading 3"},emphasis:{bold:"Bold",italic:"Italic",underline:"Underline"},lists:{unordered:"Unordered list",ordered:"Ordered list",outdent:"Outdent",indent:"Indent"},link:{insert:"Insert link",cancel:"Cancel"},image:{insert:"Insert image",cancel:"Cancel"},html:{edit:"Edit HTML"},colours:{black:"Black",silver:"Silver",gray:"Grey",maroon:"Maroon",red:"Red",purple:"Purple",green:"Green",olive:"Olive",navy:"Navy",blue:"Blue",orange:"Orange"}}}}(window.jQuery,window.wysihtml5); +// Declare app level module which depends on filters, and services +var app = angular.module('clientApp', [ + 'clientApp.filters', + 'clientApp.services', + 'clientApp.directives', + + // anguar-strap.js + '$strap.directives' +]) +.config(['$routeProvider', + function($routeProvider) { + // login page + $routeProvider.when('/login', { + templateUrl: 'login.html', + controller: 'LoginCtrl' + }); + + $routeProvider.when('/notes/:guid', { + templateUrl: 'notes.html', + controller: 'NoteCtrl' + }); + $routeProvider.when('/logout', { + template: '
      ', + controller: 'LogoutCtrl' + }); + $routeProvider.when('/signup', { + templateUrl: 'signup.html', + controller: 'SignupCtrl' + }); + + $routeProvider.otherwise({ + redirectTo: '/login' + }); + } +]) +// disable the X-Requested-With header +.config(['$httpProvider', function($httpProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + } +]) +.config(['$locationProvider', + function($locationProvider) { + $locationProvider.html5Mode(false); + } +]); + +// register the interceptor as a service +app.factory('loginInterceptor', function($q, $location) { + return function(promise) { + return promise.then(function(response) { + // do something on success + return response; + }, function(response) { + // do something on error + if (response.status === 401) { + if (window.localStorage) + window.localStorage.removeItem('accessToken'); + $location.path('/login'); + } + return $q.reject(response); + }); + }; +}); +app.config(['$httpProvider', function($httpProvider) { + $httpProvider.responseInterceptors.push('loginInterceptor'); + } +]); + + +// FILTERS +angular.module('clientApp.filters', []) + .filter('interpolate', ['version', function(version) { + return function(text) { + return String(text).replace(/\%VERSION\%/mg, version); + }; + }]); + +// SERVICES +angular.module('clientApp.services', []) + .value('version', '0.1'); + + +// DIRECTIVES +angular.module('clientApp.directives', []) + .directive('appVersion', ['version', + function(version) { + return function(scope, elm, attrs) { + elm.text(version); + }; + } + ]) + .directive('uniqueUsername', ['$http', function ($http) { + return { + require:'ngModel', + restrict: 'A', + link:function (scope, el, attrs, ctrl) { + + var check_username_avail = _.debounce(function (username) { + $http.get('/api/user/signup/check_username/' + username) + .success(function (data) { + console.log(data); + if (data.Available === true) { + ctrl.$setValidity('username_avail', true); + //scope.username = data.Username; + } + else + ctrl.$setValidity('username_avail', false); + }); + }, 800); + + // push to the end of all other validity parsers + ctrl.$parsers.push(function (viewValue) { + if (viewValue) { + check_username_avail(viewValue); + return viewValue; + } + }); + } + }; + }]) +; + +if (typeof String.prototype.startsWith !== 'function') { + String.prototype.startsWith = function(str) { + return this.slice(0, str.length) === str; + }; +} + +function LoginCtrl($scope, $location, $rootScope, + loginService, notyService, configService) { + + $scope.username = ''; + $scope.password = ''; + $scope.rememberMe = false; + $scope.isTestServer = $location.host() === 'testserver.notesync.org'; + + $scope.serverConfig = configService.serverConfig; + + if (loginService.userIsLoggedIn()) { + $location.path('/notes/'); + } + + var useStorage = window.localStorage && window.sessionStorage; + if (useStorage) { + $scope.username = window.sessionStorage.getItem('username'); + + $scope.$watch('username', function (newval, oldval) { + if (!newval) + window.sessionStorage.removeItem('username'); + else + window.sessionStorage.setItem('username', newval); + }); + + + $scope.rememberMe = window.sessionStorage.getItem('rememberMe') === 'true'; + $scope.$watch('rememberMe', function (newval, oldval) { + window.sessionStorage.setItem('rememberMe', newval); + }); + } + + if (!$scope.username || $scope.username.length === 0) + $('#inputUsername').focus(); + else + $('#inputPassword').focus(); + + $scope.doLogin = function () { + var remember = $scope.rememberMe; + + loginService.login($scope.username, $scope.password, remember) + .then(function () { + $location.path('/notes/'); + }, function (error_status) { + console.log(error_status); + if (error_status === 412) + notyService.error('Login failed. User ' + $scope.username + ' requires activation by an admin (moderation is enabled)'); + else + notyService.error('Login failed. Check username and password.'); + }); + }; +} +//LoginCtrl.$inject = [ '$scope','$http' ]; + +function LogoutCtrl($location, loginService) { + + loginService.logout(); + $location.path('/login/'); +} + +function MainCtrl ($scope, loginService) { + $scope.isLoggedIn = loginService.userIsLoggedIn(); + + $scope.$on('loginStatus', function(ev, isLoggedIn) { + $scope.isLoggedIn = isLoggedIn; + }); +} +function NoteCtrl($scope,$location, $routeParams, $timeout, $q, $rootScope, + noteService, loginService, notyService, configService) { + + $scope.notebooks = {}; + $scope.notes = []; + $scope.noteService = noteService; + $scope.username = loginService.username; + $scope.enableSyncButton = false; + $scope.config = configService.serverConfig; + + noteService.fetchNotes(); + + // deep watching, will get triggered if a note content's changes, too + $scope.$watch('noteService.notes', function (newval, oldval) { + if (newval && newval.length === 0) return; + + if (oldval && oldval.length === 0 && newval && newval.length > 0) { + // first time the notes become ready + } + + $scope.notebooks = noteService.notebooks; + $scope.notes = newval; + + loadNote(); + + }, true); + + + var initialAutosyncSeconds = 300; + function startAutosyncTimer () { + $timeout.cancel($rootScope.timer_dfd); + $rootScope.autosyncSeconds = initialAutosyncSeconds; + $scope.enableSyncButton = true; + $rootScope.timer_dfd = $timeout(function autosync(){ + if ($rootScope.autosyncSeconds % 10 === 0) + console.log('next sync in: ' + $rootScope.autosyncSeconds + ' seconds'); + if ($rootScope.autosyncSeconds <= 0) { + $scope.sync(); + return; + } + else { + $rootScope.autosyncSeconds--; + $rootScope.timer_dfd = $timeout(autosync, 1000); + } + }, 1000); + } + function stopAutosyncTimer () { + $rootScope.autosyncSeconds = initialAutosyncSeconds; + $timeout.cancel($rootScope.timer_dfd); + $scope.enableSyncButton = false; + } + + function setSyncButtonTooltip () { + // we need to recreate the tooltip every mouseover due to bootstrap internals + $('#sync_btn').mouseenter(function() { + var caption = 'Next autosync in ' + $rootScope.autosyncSeconds + ' seconds or press to perform manual sync'; + $('#sync_btn').data('tooltip', false); + if ($scope.enableSyncButton) { + $('#sync_btn').tooltip({ title: caption }); + $('#sync_btn').tooltip('show'); + } + }); + } + setSyncButtonTooltip (); + + function setWindowCloseMessage () { + window.onbeforeunload = function () { + if ($scope.enableSyncButton) { + return 'There are unsaved notes, please push the synchronize button!'; + } else { + } + }; + } + setWindowCloseMessage(); + + + $scope.$watch('noteService.needsSyncing', function (newval, oldval) { + $scope.enableSyncButton = newval; + if (newval === true && oldval === false) { + startAutosyncTimer(); + } + }); + + function loadNote () { + var guid = $routeParams.guid; + + if (!guid) return; + var n = noteService.getNoteByGuid(guid); + if (!n) return; + if ($scope.selectedNote && n.guid === $scope.selectedNote.guid) return; + $scope.selectedNote = n; + $scope.setWysiText(n['note-content']); + } + + $scope.onNoteChange = function (note) { + noteService.markAsTainted(note); + }; + + $scope.selectNote = function (note) { + var guid = note.guid; + $location.path('/notes/' + guid); + }; + + $scope.sync = function () { + if ($scope.enableSyncButton === false) + return; + + $('#sync_btn').tooltip('hide'); + stopAutosyncTimer(); + $scope.flushWysi(); + // HACK we give 100ms before we sync to wait for the editor to flush + $timeout(function () { + noteService.uploadChanges().then(function (note_changes) { + notyService.success('Successfully synced ' + note_changes.length + ' notes.', 2000); + },function () { + notyService.error('Error occured during syncing!'); + }); + + }, 100); + }; + + $scope.deleteNote = function () { + noteService.deleteNote($scope.selectedNote); + $location.path('/notes/'); + }; + + $scope.newNote = function () { + var note = noteService.newNote(); + noteService.markAsTainted(note); + $scope.selectNote(note); + }; +} + +function SignupCtrl($scope, $location, $http, $timeout, notyService) { + $scope.username = ''; + $scope.password1 = ''; + $scope.password2 = ''; + $scope.email = ''; + $scope.toc = false; + + $scope.$watch(combinedPassword, function (newval, oldval) { + if (newval === oldval) + return; + passwordMatch(); + }); + + $scope.$watch('toc', function (newval) { + if (newval === true) + $scope.formSignup.$setValidity('toc', true); + else + $scope.formSignup.$setValidity('toc', false); + }); + + function combinedPassword () { + return $scope.password1 + ' ' + $scope.password2; + } + + function passwordMatch () { + if ($scope.password1 === $scope.password2) + $scope.formSignup.$setValidity('passwdmatch', true); + else + $scope.formSignup.$setValidity('passwdmatch', false); + } + + $scope.signUp = function () { + var new_user = { + Username: $scope.username, + Password: $scope.password1, + EmailAddress: $scope.email + }; + $http.post('/api/user/signup/new/', new_user).success(function (data) { + window.sessionStorage.setItem('username', $scope.username); + notyService.success('Signup successfull! You will be redirected to the login page...'); + $timeout(function () { + $location.path('#/login/'); + }, 2000); + }).error(function (data, status, headers, config) { + notyService.error('ERROR: ' + status); + }); + }; +} + +app.factory('loginService', function($q, $http, $rootScope) { + var loginService = { + username: '', + accessToken: '' + }; + + var useStorage = window.sessionStorage && window.localStorage; + if (useStorage) { + loginService.accessToken = window.localStorage.getItem('accessToken'); + loginService.username = window.localStorage.getItem('username'); + } + + loginService.login = function (user, pass, remember) { + var deferred = $q.defer(); + var expiry = 1440; // 1d + + if (remember) + expiry = 14 * 1440; // 14d + + var credentials = { + Username: user, + Password: pass, + Expiry: expiry + }; + + $http.post('/oauth/temporary_access_token', credentials) + .success(function (data, status, headers, config) { + loginService.accessToken = data.AccessToken; + loginService.username = user; + + if (useStorage && remember) { + window.localStorage.setItem('username', user); + window.localStorage.setItem('accessToken', data.AccessToken); + } + $rootScope.$broadcast('loginStatus', true); + deferred.resolve(); + }) + .error(function (data, status) { + deferred.reject(status); + $rootScope.$broadcast('loginStatus', false); + }); + return deferred.promise; + }; + + loginService.logout = function () { + loginService.accessToken = ''; + loginService.username = ''; + + if (useStorage) { + window.localStorage.removeItem('accessToken'); + window.sessionStorage.removeItem('accessToken'); + } + $rootScope.$broadcast('loginStatus', false); + }; + + loginService.userIsLoggedIn = function () { + // TODO check for expiry + var logged = !(loginService.accessToken === '' || + loginService.accessToken === undefined); + + if (useStorage && logged) { + var ret = window.localStorage.getItem('accessToken') && true; + return ret; + } + else return logged; + }; + + loginService.isLoggedIn = loginService.userIsLoggedIn(); + + return loginService; +}); + +app.factory('noteService', function($http, $rootScope, $q, loginService) { + + var noteService = {}; + var notes, latest_sync_revision, manifest; + + function initialize () { + notes = null; + + latest_sync_revision = 0; + manifest = { + taintedNotes: [], + deletedNotes: [], + }; + } + + initialize(); + + Object.defineProperty(noteService, 'notebooks', { + get: function () { + return buildNotebooks(notes); + } + }); + + Object.defineProperty(noteService, 'notes', { + get: function () { + return filterDeletedNotes(notes); + } + }); + + Object.defineProperty(noteService, 'needsSyncing', { + get: function () { + return manifest.taintedNotes.length > 0 || manifest.deletedNotes.length > 0; + } + }); + + $rootScope.$on('loginStatus', function(ev, isLoggedIn) { + if (!isLoggedIn) { + console.log('cleaning note service'); + initialize(); + } + }); + + function getNotebookFromNote (note) { + var nb_name = null; + _.each(note.tags, function (tag) { + if (tag.startsWith('system:notebook:')) { + nb_name = tag.substring(16); + } + }); + return nb_name; + } + + function notesByNotebook (notes, notebook_name) { + if (notebook_name) { + return _.filter(notes, function (note) { + var nb = getNotebookFromNote(note); + return nb === notebook_name; + }); + } else { + // return notes that don't have a notebook + return _.filter(notes, function (note) { + return getNotebookFromNote(note) === null; + }); + } + } + + function buildNotebooks (notes) { + var notebooks = {}; + var notebook_names = []; + + notebooks.Unsorted = notesByNotebook(notes); + + _.each(notes, function (note) { + var nb = getNotebookFromNote (note); + if (nb) + notebook_names.push(nb); + }); + notebook_names = _.uniq(notebook_names); + + _.each(notebook_names, function(name) { + notebooks[name] = notesByNotebook(notes, name); + }); + + // filter out notes marked as deleted & empty notebooks + var filtered_nb = {}; + for (var nb in notebooks) { + var filtered = filterDeletedNotes(notebooks[nb]); + if (filtered.length > 0) + filtered_nb[nb] = filtered; + } + + return filtered_nb; + } + + function filterDeletedNotes(notes) { + var filtered = _.filter(notes, function(note) { + return !_.contains(manifest.deletedNotes, note.guid); + }); + return filtered; + } + + function guid () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } + + // PUBLIC functions + // + noteService.getNoteByGuid = function (guid) { + if (noteService.notes.length === 0) + return null; + return _.findWhere(notes, {guid: guid}); + }; + + noteService.fetchNotes = function(force) { + + if (notes !== null && !force) return; + + manifest.taintedNotes = []; + manifest.deletedNotes = []; + + $http({ + method: 'GET', + url: '/api/1.0/' + loginService.username + '/notes?include_notes=true¬es_as_html=true', + headers: { 'AccessToken': loginService.accessToken } + }).success(function (data, status, headers, config) { + notes = data.notes; + latest_sync_revision = data['latest-sync-revision']; + }).error(function () { + // console.log('fail'); + }); + }; + + noteService.uploadChanges = function () { + var dfd_complete = $q.defer(); + var note_changes = []; + _.each(manifest.taintedNotes, function(guid) { + var n = noteService.getNoteByGuid(guid); + note_changes.push(n); + }); + _.each(manifest.deletedNotes, function(guid) { + var n = noteService.getNoteByGuid(guid); + n.command = 'delete'; + note_changes.push(n); + }); + + if (note_changes.length > 0) { + latest_sync_revision++; + var req = { + 'latest-sync-revision': latest_sync_revision, + }; + req['note-changes'] = note_changes; + + console.log(req); + + $http({ + method: 'PUT', + url: '/api/1.0/' + loginService.username + '/notes?notes_as_html=true', + headers: { 'AccessToken': loginService.accessToken }, + data: req + }).success(function (data, status, headers, config) { + console.log('successfully synced'); + noteService.fetchNotes(true); + dfd_complete.resolve(note_changes); + }).error(function () { + dfd_complete.reject(); + }); + } else { + console.log ('no changes, not syncing'); + dfd_complete.resolve(); + } + return dfd_complete.promise; + }; + + noteService.deleteNote = function (note) { + if (!_.contains(manifest.deletedNotes, note)) { + manifest.deletedNotes.push(note.guid); + } + }; + + noteService.newNote = function (initial_note) { + var proto = {}; + proto.title = 'New note'; + proto['note-content'] = 'Enter your note.'; + proto['create-date'] = new Date().toISOString(); + proto.guid = guid(); + proto.tags = []; + + var note = $.extend(proto, initial_note); + notes.push(note); + return note; + }; + + noteService.markAsTainted = function (note) { + var now = new Date().toISOString(); + note['last-change-date'] = now; + note['last-metadata-change-date'] = now; + + if (!_.contains(manifest.taintedNotes, note.guid)) { + console.log('marking note ' + note.guid + ' as tainted'); + manifest.taintedNotes.push(note.guid); + } + }; + + return noteService; +}); + +app.directive('wysiwyg', ['$q', function($q){ + + var setupWysiwyg = function (tElement, scope) { + var parserRules = { + classes: { + // for urls + 'internal': 1, + 'url': 1, + 'title': 0 + }, + tags: { + 'div': { + 'remove': 1 + }, + 'strike': {}, + 'del': {}, + 'span': {}, + 'small': {}, + 'i': {}, + 'a': {}, + 'b': {}, + 'br': {}, + 'li': {}, + 'ul': {}, + 'h1': {}, + 'h2': {} + } + }; + + tElement.wysihtml5({ + html: false, + link: false, + image: false, + color: false, + // HACK this identity function prevents the wysi to sometimes remove valid + // tags from the editor; see http://stackoverflow.com/a/17656572/777928 + // this renders our parser rules obsolete + parser: function (html) { return html; }, + parserRules: parserRules, + stylesheets: ['wysihtml5_style.css'], + events: { + change: function () { + /* the change event prevents events, like links clicks :( */ + scope.updateWysiText(); + }, + blur: function() { + scope.updateWysiText(); + }, + load: function () { + var $doc = $(scope.wysiEditor.composer.doc); + $doc.keyup(function (evt) { + scope.updateWysiText(); + }); + $doc.keydown(function (evt) { + scope.updateWysiText(); + }); + } + } + }); + // HACK we modify/remove the bootstrap-wysihtml5 elements from the bar + $('[data-wysihtml5-command=insertOrderedList]').remove(); + $('[data-wysihtml5-command=Outdent]').remove(); + $('[data-wysihtml5-command=Indent]').remove(); + + $('[data-wysihtml5-command=underline]').remove(); + + var huge_btn= $('

      Huge

      '); + $('[data-wysihtml5-command-value=h1]').replaceWith(huge_btn); + + var large_btn= $('

      Large

      '); + $('[data-wysihtml5-command-value=h2]').replaceWith(large_btn); + + var small_btn = $('Small'); + $('[data-wysihtml5-command-value=h3]').replaceWith(small_btn); + + var strike_btn = $('Strike'); + strike_btn.insertAfter($('[data-wysihtml5-command=italic]')); + + var highlight_btn = $('Highlight'); + highlight_btn.insertAfter(strike_btn); + + var fixed_btn = $('Fixed'); + fixed_btn.insertAfter(strike_btn); + + //$('[data-wysihtml5-command-value=h3]').replaceWith('Small​'); + //$('[data-wysihtml5-command-value=h3]').replaceWith('Small​'); + $('.wysihtml5-toolbar').find('a').click(function () { + scope.updateWysiText(); + }); + }; + + function setupTitleInput (tElement, scope) { + // we don't want line breaks in the title so ignore press of enter key + tElement.keypress(function (e){ + if (e.which === 13) { + return false; + } + }); + // HACK update title on keyup + var titleUpdateFn = function () { + scope.$apply(function() { + var txt = tElement.text(); + if (txt !== '') { + scope.selectedNote.title = txt; + } + }); + }; + tElement.keyup(titleUpdateFn); + } + + function setupChangeListeners (tElement, scope) { + + scope.$watch('selectedNote', function (newval, oldval) { + if (!newval) return; + + if (oldval && newval['note-content'] === oldval['note-content'] && + newval.title === oldval.title) { + // nothing changed + return; + } + if (oldval) { + scope.onNoteChange(newval); + } + }, true); + } + + return { + name: 'wysiwyg', + // priority: 1, + // terminal: true, + + scope: false, + //{} = isolate, true = child, false/undefined = no change + + // controller: function ($scope, $element) {}, + // require: '', // Array = multiple requires, ? = optional, ^ = check parent elements + restrict: 'E', // E = Element, A = Attribute, C = Class, M = Comment + // template: '', + templateUrl: 'wysiwyg.html', + replace: true, + transclude: false, + compile: function compile(tElement, tAttrs, transclude) { + return { + post: function preLink(scope, tElement, iAttrs, controller){ + + wysihtml5.commands.highlight = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, 'span', 'highlight', 'highlight'); + }, + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, 'span', 'highlight', 'highlight'); + } + }; + wysihtml5.commands.fixedwidth = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, 'pre'); + }, + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, 'pre'); + } + }; + + + scope.selectedNote = null; + + var textarea = tElement.find('textarea'); + setupWysiwyg(textarea, scope); + setupTitleInput(tElement.find('h2'), scope); + scope.wysiEditor = textarea.data('wysihtml5').editor; + + scope.updateWysiText = _.throttle(function () { + var newtext = textarea.val(); + scope.$apply(function () { + if (scope.selectedNote) + scope.selectedNote['note-content'] = newtext; + //console.log('text changed: ' + newtext); + }); + }, 800, { leading: false }); + + scope.setWysiText = function (text) { + scope.wysiEditor.setValue(text); + }; + + // HACK we sometimes miss any character for any reason? + // allow explicit flush (i.e. before sync) + scope.flushWysi = function () { + var newtext = textarea.val(); + if (scope.selectedNote) + scope.selectedNote['note-content'] = newtext; + //console.log('flushed text: ' + newtext); + }; + + setupChangeListeners (tElement, scope); + }, + pre: function postLink(scope, tElement, iAttrs, controller){ + } + }; + }, + }; +}]); + +angular.module('clientApp').factory('configService', function($http) { + var configService = {}; + var conf = {}; + + Object.defineProperty(configService, 'serverConfig', { + get: function () { + return conf; + } + }); + + $http.get('/api/config').success(function (data) { + conf = $.extend(conf, data); + }); + + return configService; +}); + +app.factory('notyService', function($rootScope) { + var notyService = {}; + + function showNoty (msg, type, timeout) { + timeout = timeout || 5000; + var n = noty({ + text: msg, + layout: 'topCenter', + timeout: timeout, + type: type + }); + } + + $rootScope.$on('$routeChangeStart', function() { + $.noty.clearQueue(); + $.noty.closeAll(); + }); + notyService.error = function (msg, timeout) { + return showNoty(msg, 'error', timeout); + }; + notyService.warn = function (msg, timeout) { + return showNoty(msg, 'warn', timeout); + }; + notyService.success = function (msg, timeout) { + return showNoty(msg, 'success', timeout); + }; + + return notyService; +}); diff --git a/Rainy.UI/dist/common.js b/Rainy.UI/dist/common.js new file mode 100644 index 0000000..5542c5d --- /dev/null +++ b/Rainy.UI/dist/common.js @@ -0,0 +1,937 @@ +/*! jQuery v2.0.3 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery.min.map +*/ +(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.3",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=st(),k=st(),N=st(),E=!1,S=function(e,t){return e===t?(E=!0,0):0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],q=L.pop,H=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){H.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+mt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,r,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function at(e){return e[v]=!0,e}function ut(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function lt(e,t){var n=e.split("|"),r=e.length;while(r--)i.attrHandle[n[r]]=t}function ct(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return at(function(t){return t=+t,at(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.defaultView;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.attachEvent&&r!==r.top&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ut(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=ut(function(e){return e.innerHTML="
      ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=ut(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=Q.test(t.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),ut(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=Q.test(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ut(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=Q.test(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return ct(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?ct(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:at,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?at(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:at(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?at(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:at(function(e){return function(t){return ot(e,t).length>0}}),contains:at(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:at(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},i.pseudos.nth=i.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=ft(t);function dt(){}dt.prototype=i.filters=i.pseudos,i.setFilters=new dt;function gt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[v]&&(r=bt(r)),i&&!i[v]&&(i=bt(i,o)),at(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function wt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=yt(function(e){return e===t},a,!0),p=yt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[yt(vt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return bt(l>1&&vt(f),l>1&&mt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&wt(e.slice(l,r)),o>r&&wt(e=e.slice(r)),o>r&&mt(e))}f.push(n)}return vt(f)}function Tt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=q.call(f));y=xt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?at(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function kt(e,t,r,o){var s,u,l,c,p,f=gt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&mt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}n.sortStable=v.split("").sort(S).join("")===v,n.detectDuplicates=E,c(),n.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||lt("type|href|height|width",function(e,t,n){return n?undefined:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||lt("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?undefined:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||lt(R,function(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}),x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!a||n&&!u||(t=t||[],t=[e,t.slice?t.slice():t],r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){var r;return t===undefined||t&&"string"==typeof t&&n===undefined?(r=this.get(e,t),r!==undefined?r:this.get(e,x.camelCase(t))):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t) +};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,i=0,o=x(this),s=e.match(w)||[];while(t=s[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"","
      "],col:[2,"","
      "],tr:[2,"","
      "],td:[3,"","
      "],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.lastChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[q.expando],o&&(t=q.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);q.cache[o]&&delete q.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=q.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function qt(t){return e.getComputedStyle(t,null)}function Ht(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=q.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=qt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return Ht(this,!0)},hide:function(){return Ht(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Lt(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||qt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=qt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x("');a.attr("src","http://youtube.com/embed/"+s),t.replaceWith(a)}}})}var n={name:"youtube",load:function(){e.md.stage("gimmick").subscribe(function(e){t(),e()})}};e.md.registerGimmick(n)}(jQuery); + + + + + + +
      +
      +
      +
      +
      +
      +
      + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a6d4203 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,43 @@ +Rainy is an open source synchronization server that allows to sync notes with Tomboy and Tomdroid. +Follow @timodoerr for all rainy-related announcements! +* * * +[gimmick:twitterfollow](@timodoerr)   [gimmick:FacebookLike ( layout: 'buttoncount') ](http://www.facebook.com/pages/Rainy-note-sync-server-for-Tomboy/116321368557123)   +* * * + +###### June, 2. 2013 +## Rainy accepted at Google Summer of Code 2013! + +I am glad to announce that I have been accepted as a [Google Summer of Code][gsoc] student to work on Rainy during this summer. The [Mono project][monoproject] is the mentoring organization that will help and mentor me in this enjoyable endeavour. As a result, there are several feature planned to be available by the end of the summer, see the [my original proposal][proposal] for the GSoC. + +If you want to track my progress on the project, checkout [my blog][myblog] and the [GitHub][github-rainy] project website regularly! + + + [myblog]: http://exceptionrethrown.wordpress.com + [gsoc]: https://developers.google.com/open-source/soc/ + [proposal]: http://www.google-melange.com/gsoc/project/google/gsoc2013/dynalon/27001 + [monoproject]: http://mono-project.com + [github-rainy]: httpw://github.com/Dynalon/Rainy + +- - - + +###### March, 15. 2013 +## New website, new release! + +I commited some changes to the website today. Most notable change (besides the switch to the [bootswatch united theme][united]) is this _news_ section. I intend to use it to put news about Rainy releases and important information on the website, apart of [my blog][blog]. + +Documentation was also updated, and includes now instructions of how to perform SSL setup. + +## Introducing the 0.2.X release + +The 0.2.X release series brings the long-awaited SSL support. This means that connections are now finally encrypted, which makes rainy so much more usable and secure. + +Thanks to the work of [Stefan Hammer][stefan], the latest [Tomdroid][tomdroid] releases (stable from the market + development build) do now also support self-signed certificates for SSL connection, so if you upgrade to latest Tomdroid you are ready to sync with Rainy via SSL. +[![](http://launchpadlibrarian.net/79043149/icon-64.png)](https://launchpad.net/tomdroid) + +ATTENTION: The 0.2.X release uses a different database scheme, so if you are using the sqlite backend and upgraded from the 0.1.X series you must delete the `rainy.db` file and start over (your notes from Tomboy/Tomdroid will be saved in the new DB upon first sync). The settings.conf file format has changed, too, so better start from scratch! + + +[united]: http://bootswatch.com/ +[blog]: http://exceptionrethrown.wordpress.com/ +[stefan]: https://plus.google.com/107845688101586158412 +[tomdroid]: https://launchpad.net/tomdroid diff --git a/docs/navigation.md b/docs/navigation.md index a52e589..1a9f833 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -1,8 +1,34 @@ -[Introduction](README.md) +# Rainy + +[News](index.md) +[About](README.md) [Getting started](GETTING_STARTED.md) [Public server access](PUBLIC_SERVER.md) [Download](DOWNLOAD.md) -[OAuth](OAUTH.md) -[Syncing process](SYNC.md) +[Contribute](CONTRIBUTE.md) +[Support](SUPPORT.md) + +[Developer]() + + * [Hacking HOWTO](developer/hacking.md) + * [OAuth docs](developer/oauth.md) + * [Crypto docs](developer/crypto.md) + * [Rainy REST API docs](http://www.notesync.org/apidoc) + +[gimmick:theme(inverse: true)](united) [gimmick:ForkMeOnGithub({ color: 'red', position: 'right'})](https://github.com/Dynalon/Rainy) + + diff --git a/docs/rainy_logo.png b/docs/rainy_logo.png new file mode 100644 index 0000000..941b390 Binary files /dev/null and b/docs/rainy_logo.png differ diff --git a/lib/Npgsql.dll b/lib/Npgsql.dll new file mode 100755 index 0000000..2aef278 Binary files /dev/null and b/lib/Npgsql.dll differ diff --git a/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.Sqlite.dll b/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.Sqlite.dll deleted file mode 100644 index 026289c..0000000 Binary files a/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.Sqlite.dll and /dev/null differ diff --git a/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.dll b/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.dll deleted file mode 100755 index 000f795..0000000 Binary files a/lib/ServiceStack-v3.9.32/ServiceStack.OrmLite.dll and /dev/null differ diff --git a/lib/ServiceStack-v3.9.32/ServiceStack.ServiceInterface.dll b/lib/ServiceStack-v3.9.32/ServiceStack.ServiceInterface.dll deleted file mode 100755 index a5ae8fa..0000000 Binary files a/lib/ServiceStack-v3.9.32/ServiceStack.ServiceInterface.dll and /dev/null differ diff --git a/lib/ServiceStack-v3.9.32/ServiceStack.dll b/lib/ServiceStack-v3.9.32/ServiceStack.dll deleted file mode 100755 index 86cbcff..0000000 Binary files a/lib/ServiceStack-v3.9.32/ServiceStack.dll and /dev/null differ diff --git a/packages/repositories.config b/packages/repositories.config new file mode 100644 index 0000000..c30f585 --- /dev/null +++ b/packages/repositories.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/release/README b/release/README index 67a0180..4fab039 100644 --- a/release/README +++ b/release/README @@ -10,3 +10,10 @@ for a quick usage help. For more documentation, bug report and so on, please visist: https://github.com/Dynalon/Rainy + +LICENSE +======= +Rainy is licensed under the terms of the GNU AGPLv3. The license +conditions can be found here: + + http://www.gnu.org/licenses/agpl-3.0.html diff --git a/tomboy-library b/tomboy-library index fe319ae..a124660 160000 --- a/tomboy-library +++ b/tomboy-library @@ -1 +1 @@ -Subproject commit fe319ae0d07a1634616034aed6adc1264461c253 +Subproject commit a1246601a3907aba4d394e22487ef59734c3cdeb diff --git a/tomboy-library-websync b/tomboy-library-websync new file mode 160000 index 0000000..2e81b99 --- /dev/null +++ b/tomboy-library-websync @@ -0,0 +1 @@ +Subproject commit 2e81b9900c53ba101adb9f8bf7c3e770c76aa7ae diff --git a/tools/NuGet.exe b/tools/NuGet.exe new file mode 100644 index 0000000..c296edf Binary files /dev/null and b/tools/NuGet.exe differ diff --git a/tools/NuGet.exe.license b/tools/NuGet.exe.license new file mode 100644 index 0000000..5c6a825 --- /dev/null +++ b/tools/NuGet.exe.license @@ -0,0 +1,205 @@ +NuGet.exe taken from http://nuget.codeplex.com/releases/view/104497 +(binary without any modification) +NuGet.exe is subject to the following terms and conditions: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.